diff --git a/packages/loopover-miner/lib/replay-snapshot.ts b/packages/loopover-miner/lib/replay-snapshot.ts index 7cdc14dc6e..56e23dbaea 100644 --- a/packages/loopover-miner/lib/replay-snapshot.ts +++ b/packages/loopover-miner/lib/replay-snapshot.ts @@ -67,6 +67,7 @@ function normalizeDbPath(dbPath: string): string { const FIELD_SEP = "\x1f"; const README_NAME_PATTERN = /^readme(\.\w+)?$/i; +const COMMIT_SHA_PATTERN = /^[0-9a-f]{7,40}$/i; function normalizeRepoFullName(repoFullName: string): string { if (typeof repoFullName !== "string") throw new Error("invalid_repo_full_name"); @@ -77,7 +78,9 @@ function normalizeRepoFullName(repoFullName: string): string { function normalizeCommitSha(commitSha: string): string { if (typeof commitSha !== "string" || !commitSha.trim()) throw new Error("invalid_commit_sha"); - return commitSha.trim(); + const trimmed = commitSha.trim(); + if (!COMMIT_SHA_PATTERN.test(trimmed)) throw new Error("invalid_commit_sha"); + return trimmed.toLowerCase(); } /** Worktree exports live under this dir inside the repo, mirroring worktree-allocator.ts's WORKTREE_SUBDIR. */ diff --git a/test/unit/miner-replay-snapshot.test.ts b/test/unit/miner-replay-snapshot.test.ts index f1ef84628c..f34eb477ec 100644 --- a/test/unit/miner-replay-snapshot.test.ts +++ b/test/unit/miner-replay-snapshot.test.ts @@ -47,8 +47,8 @@ function happyPathScripts(overrides: Array<{ match: (args: readonly string[]) => ...overrides, { match: isWorktreeAdd, result: ok() }, { match: isTargetDate, result: ok("2026-01-05T00:00:00+00:00\n") }, - { match: isHistory, result: ok(`abc123${FIELD_SEP}2026-01-05T00:00:00+00:00${FIELD_SEP}the target commit\n`) }, - { match: isTag, result: ok(`v1.0.0${FIELD_SEP}2026-01-01T00:00:00+00:00${FIELD_SEP}abc000${FIELD_SEP}tag\n`) }, + { match: isHistory, result: ok(`abc1234${FIELD_SEP}2026-01-05T00:00:00+00:00${FIELD_SEP}the target commit\n`) }, + { match: isTag, result: ok(`v1.0.0${FIELD_SEP}2026-01-01T00:00:00+00:00${FIELD_SEP}abc0001${FIELD_SEP}tag\n`) }, { match: isLsTree, result: ok("README.md\nsrc\npackage.json\n") }, { match: isShow, result: ok("# hello\n") }, ]; @@ -72,10 +72,29 @@ afterEach(() => { describe("planReplaySnapshotPath (#3010) — pure, deterministic", () => { it("same (repoPath, commitSha) always yields the same path", () => { - const a = planReplaySnapshotPath({ repoPath: "/repo", commitSha: "abc123" }); - expect(a.replaceAll("\\", "/")).toBe(`/repo/${REPLAY_SNAPSHOT_SUBDIR}/abc123`); - expect(planReplaySnapshotPath({ repoPath: "/repo", commitSha: "abc123" })).toBe(a); - expect(planReplaySnapshotPath({ repoPath: "/repo", commitSha: "def456" })).not.toBe(a); + const a = planReplaySnapshotPath({ repoPath: "/repo", commitSha: "abc1234" }); + expect(a.replaceAll("\\", "/")).toBe(`/repo/${REPLAY_SNAPSHOT_SUBDIR}/abc1234`); + expect(planReplaySnapshotPath({ repoPath: "/repo", commitSha: "abc1234" })).toBe(a); + expect(planReplaySnapshotPath({ repoPath: "/repo", commitSha: "def4567" })).not.toBe(a); + }); + + it("REGRESSION: rejects a path-traversal-shaped commitSha instead of escaping the sandbox directory", () => { + expect(() => + planReplaySnapshotPath({ + repoPath: "/home/miner/.config/loopover-miner/repos/acme/widgets", + commitSha: "../../../../../../tmp/evil-worktree", + }), + ).toThrow("invalid_commit_sha"); + }); + + it("rejects a commit SHA shorter than 7 hex characters", () => { + expect(() => planReplaySnapshotPath({ repoPath: "/repo", commitSha: "abc123" })).toThrow("invalid_commit_sha"); + }); + + it("normalizes uppercase hex in commit SHAs to lowercase snapshot paths", () => { + expect(planReplaySnapshotPath({ repoPath: "/repo", commitSha: "ABC1234" }).replaceAll("\\", "/")).toBe( + `/repo/${REPLAY_SNAPSHOT_SUBDIR}/abc1234`, + ); }); }); @@ -129,25 +148,25 @@ describe("exportReplaySnapshot (#3010)", () => { const store = tempStore(); const snapshot = await exportReplaySnapshot( - { repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, + { repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store }, ); expect(snapshot.repoFullName).toBe("acme/widgets"); - expect(snapshot.commitSha).toBe("abc123"); + expect(snapshot.commitSha).toBe("abc1234"); expect(snapshot.targetDate).toBe("2026-01-05T00:00:00+00:00"); - expect(snapshot.commits).toEqual([{ sha: "abc123", date: "2026-01-05T00:00:00+00:00", subject: "the target commit" }]); - expect(snapshot.tags).toEqual([{ name: "v1.0.0", date: "2026-01-01T00:00:00+00:00", targetSha: "abc000" }]); + expect(snapshot.commits).toEqual([{ sha: "abc1234", date: "2026-01-05T00:00:00+00:00", subject: "the target commit" }]); + expect(snapshot.tags).toEqual([{ name: "v1.0.0", date: "2026-01-01T00:00:00+00:00", targetSha: "abc0001" }]); expect(snapshot.readme).toEqual({ filename: "README.md", content: "# hello\n" }); }); it("returns the cached snapshot on a repeat export of the same (repo, commit) pair, without calling git again", async () => { const store = tempStore(); const first = scriptedExec(happyPathScripts()); - await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec: first.exec, store }); + await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec: first.exec, store }); const second = scriptedExec([]); // no scripts at all -- any call would throw "no script matched" - const result = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec: second.exec, store }); + const result = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec: second.exec, store }); expect(result.commits).toHaveLength(1); expect(second.calls).toHaveLength(0); @@ -157,7 +176,7 @@ describe("exportReplaySnapshot (#3010)", () => { const { exec } = scriptedExec(happyPathScripts([{ match: isTag, result: ok("") }])); const store = tempStore(); - const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store }); + const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store }); expect(snapshot.tags).toEqual([]); }); @@ -170,7 +189,7 @@ describe("exportReplaySnapshot (#3010)", () => { const { exec } = scriptedExec(happyPathScripts([{ match: isTag, result: ok(tagStdout) }])); const store = tempStore(); - const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store }); + const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store }); expect(snapshot.tags).toEqual([ { name: "v1.0.0", date: "2025-12-01T00:00:00+00:00", targetSha: "sha1" }, @@ -186,7 +205,7 @@ describe("exportReplaySnapshot (#3010)", () => { const { exec } = scriptedExec(happyPathScripts([{ match: isTag, result: ok(tagStdout) }])); const store = tempStore(); - const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store }); + const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store }); expect(snapshot.tags).toEqual([{ name: "v1.0.0", date: "2025-12-01T00:00:00+00:00", targetSha: "sha1" }]); }); @@ -194,23 +213,23 @@ describe("exportReplaySnapshot (#3010)", () => { it("a commit at the very first commit of history: git log returns exactly one entry, no parents to walk", async () => { const { exec } = scriptedExec( happyPathScripts([ - { match: isHistory, result: ok(`root000${FIELD_SEP}2020-01-01T00:00:00+00:00${FIELD_SEP}initial commit\n`) }, + { match: isHistory, result: ok(`a000001${FIELD_SEP}2020-01-01T00:00:00+00:00${FIELD_SEP}initial commit\n`) }, { match: isTargetDate, result: ok("2020-01-01T00:00:00+00:00\n") }, { match: isTag, result: ok("") }, // no tag can predate the very first commit ]), ); const store = tempStore(); - const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "root000" }, { exec, store }); + const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "a000001" }, { exec, store }); - expect(snapshot.commits).toEqual([{ sha: "root000", date: "2020-01-01T00:00:00+00:00", subject: "initial commit" }]); + expect(snapshot.commits).toEqual([{ sha: "a000001", date: "2020-01-01T00:00:00+00:00", subject: "initial commit" }]); }); it("no README present at the commit: readme is null, and show is never called for a nonexistent file", async () => { const { exec, calls } = scriptedExec(happyPathScripts([{ match: isLsTree, result: ok("src\npackage.json\n") }])); const store = tempStore(); - const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store }); + const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store }); expect(snapshot.readme).toBeNull(); expect(calls.some((c) => c.args[0] === "show")).toBe(false); @@ -220,28 +239,28 @@ describe("exportReplaySnapshot (#3010)", () => { const { exec } = scriptedExec(happyPathScripts([{ match: isLsTree, result: ok("src\nReadme.rst\npackage.json\n") }])); const store = tempStore(); - const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store }); + const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store }); expect(snapshot.readme?.filename).toBe("Readme.rst"); }); it("throws a freshness violation, never persists, AND removes the worktree it already created -- a retry for the same pair must not hit a stale 'path already exists'", async () => { - const { exec, calls } = scriptedExec(happyPathScripts([{ match: isTag, result: ok(`v-future${FIELD_SEP}2026-06-01T00:00:00+00:00${FIELD_SEP}abc000${FIELD_SEP}tag\n`) }])); + const { exec, calls } = scriptedExec(happyPathScripts([{ match: isTag, result: ok(`v-future${FIELD_SEP}2026-06-01T00:00:00+00:00${FIELD_SEP}abc0001${FIELD_SEP}tag\n`) }])); const store = tempStore(); - await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store })).rejects.toThrow( + await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store })).rejects.toThrow( /replay_snapshot_freshness_violation/, ); - expect(store.getSnapshot("acme/widgets", "abc123")).toBeNull(); + expect(store.getSnapshot("acme/widgets", "abc1234")).toBeNull(); const removeCall = calls.find((c) => c.args[0] === "worktree" && c.args[1] === "remove"); - expect(removeCall?.args).toEqual(["worktree", "remove", "--force", "/repo/.loopover-replay-snapshots/abc123"]); + expect(removeCall?.args).toEqual(["worktree", "remove", "--force", "/repo/.loopover-replay-snapshots/abc1234"]); }); it("removes the worktree and rethrows the ORIGINAL error (not a cleanup error) when a git read after the worktree exists fails", async () => { const { exec, calls } = scriptedExec(happyPathScripts([{ match: isHistory, result: { code: 1, stderr: "fatal: history read failed" } }])); const store = tempStore(); - await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store })).rejects.toThrow( + await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store })).rejects.toThrow( /git_log_history_failed/, ); const removeCall = calls.find((c) => c.args[0] === "worktree" && c.args[1] === "remove"); @@ -257,7 +276,7 @@ describe("exportReplaySnapshot (#3010)", () => { ); const store = tempStore(); - await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store })).rejects.toThrow( + await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store })).rejects.toThrow( /git_log_history_failed/, ); }); @@ -266,16 +285,16 @@ describe("exportReplaySnapshot (#3010)", () => { const { exec } = scriptedExec(happyPathScripts([{ match: isWorktreeAdd, result: { code: 0 } }])); const store = tempStore(); - const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store }); + const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store }); - expect(snapshot.commitSha).toBe("abc123"); + expect(snapshot.commitSha).toBe("abc1234"); }); it("throws when git worktree add fails", async () => { const { exec } = scriptedExec(happyPathScripts([{ match: isWorktreeAdd, result: { code: 1, stderr: "fatal: invalid reference" } }])); const store = tempStore(); - await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "bogus" }, { exec, store })).rejects.toThrow( + await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "deadbeef" }, { exec, store })).rejects.toThrow( /git_worktree_add_failed.*fatal: invalid reference/, ); }); @@ -284,7 +303,7 @@ describe("exportReplaySnapshot (#3010)", () => { const { exec } = scriptedExec(happyPathScripts([{ match: isTargetDate, result: { code: 128, stderr: "fatal: bad revision" } }])); const store = tempStore(); - await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "bogus" }, { exec, store })).rejects.toThrow( + await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "deadbeef" }, { exec, store })).rejects.toThrow( /git_log_target_failed/, ); }); @@ -293,7 +312,7 @@ describe("exportReplaySnapshot (#3010)", () => { const { exec } = scriptedExec(happyPathScripts([{ match: isTargetDate, result: ok("") }])); const store = tempStore(); - await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "bogus" }, { exec, store })).rejects.toThrow( + await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "deadbeef" }, { exec, store })).rejects.toThrow( /git_log_target_failed: no commit found/, ); }); @@ -302,7 +321,7 @@ describe("exportReplaySnapshot (#3010)", () => { const { exec } = scriptedExec(happyPathScripts([{ match: isHistory, result: { code: 1, stderr: "fatal: history read failed" } }])); const store = tempStore(); - await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store })).rejects.toThrow( + await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store })).rejects.toThrow( /git_log_history_failed/, ); }); @@ -311,7 +330,7 @@ describe("exportReplaySnapshot (#3010)", () => { const { exec } = scriptedExec(happyPathScripts([{ match: isTag, result: { code: 1, stderr: "fatal: tag read failed" } }])); const store = tempStore(); - await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store })).rejects.toThrow( + await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store })).rejects.toThrow( /git_tag_merged_failed/, ); }); @@ -320,7 +339,7 @@ describe("exportReplaySnapshot (#3010)", () => { const { exec } = scriptedExec(happyPathScripts([{ match: isLsTree, result: { code: 1, stderr: "fatal: ls-tree failed" } }])); const store = tempStore(); - await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store })).rejects.toThrow( + await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store })).rejects.toThrow( /git_ls_tree_failed/, ); }); @@ -329,7 +348,7 @@ describe("exportReplaySnapshot (#3010)", () => { const { exec } = scriptedExec(happyPathScripts([{ match: isShow, result: { code: 1, stderr: "fatal: show failed" } }])); const store = tempStore(); - await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store })).rejects.toThrow( + await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store })).rejects.toThrow( /git_show_readme_failed/, ); }); @@ -347,18 +366,18 @@ describe("exportReplaySnapshot (#3010)", () => { const { exec } = scriptedExec(happyPathScripts([{ match: isWorktreeAdd, result: { code: 1 } }])); const store = tempStore(); - await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store })).rejects.toThrow( + await expect(exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store })).rejects.toThrow( /git_worktree_add_failed: exit_1/, ); }); it("tolerates a commit-history line missing the subject field, defaulting it to an empty string", async () => { - const { exec } = scriptedExec(happyPathScripts([{ match: isHistory, result: ok(`abc123${FIELD_SEP}2026-01-05T00:00:00+00:00\n`) }])); + const { exec } = scriptedExec(happyPathScripts([{ match: isHistory, result: ok(`abc1234${FIELD_SEP}2026-01-05T00:00:00+00:00\n`) }])); const store = tempStore(); - const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec, store }); + const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec, store }); - expect(snapshot.commits).toEqual([{ sha: "abc123", date: "2026-01-05T00:00:00+00:00", subject: "" }]); + expect(snapshot.commits).toEqual([{ sha: "abc1234", date: "2026-01-05T00:00:00+00:00", subject: "" }]); }); it("fails closed on a malformed input", async () => { @@ -369,12 +388,12 @@ describe("exportReplaySnapshot (#3010)", () => { await expect(exportReplaySnapshot(null as never, deps)).rejects.toThrow("invalid_replay_snapshot_input"); await expect(exportReplaySnapshot({ commitSha: "a" } as never, deps)).rejects.toThrow("invalid_repo_full_name"); await expect(exportReplaySnapshot({ repoFullName: "acme/widgets" } as never, deps)).rejects.toThrow("invalid_commit_sha"); - await expect(exportReplaySnapshot({ repoFullName: "acme/widgets", commitSha: "abc123" } as never, deps)).rejects.toThrow("invalid_repo_path"); + await expect(exportReplaySnapshot({ repoFullName: "acme/widgets", commitSha: "abc1234" } as never, deps)).rejects.toThrow("invalid_repo_path"); }); it("fails closed when exec is missing or invalid", async () => { const store = tempStore(); - const candidate = { repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }; + const candidate = { repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }; await expect(exportReplaySnapshot(candidate, null as never)).rejects.toThrow("invalid_exec"); await expect(exportReplaySnapshot(candidate, { store } as never)).rejects.toThrow("invalid_exec"); }); @@ -385,7 +404,7 @@ describe("exportReplaySnapshot (#3010)", () => { vi.stubEnv("LOOPOVER_MINER_REPLAY_SNAPSHOT_DB", join(root, "default.sqlite3")); const { exec } = scriptedExec(happyPathScripts()); - const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc123" }, { exec }); + const snapshot = await exportReplaySnapshot({ repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "abc1234" }, { exec }); expect(snapshot.repoFullName).toBe("acme/widgets"); vi.unstubAllEnvs(); @@ -395,9 +414,9 @@ describe("exportReplaySnapshot (#3010)", () => { describe("removeReplaySnapshotWorktree (#3010)", () => { it("delegates to the shared removeWorktree primitive", async () => { const { exec, calls } = scriptedExec([{ match: () => true, result: ok() }]); - const result = await removeReplaySnapshotWorktree(exec, "/repo", "/repo/.loopover-replay-snapshots/abc123"); + const result = await removeReplaySnapshotWorktree(exec, "/repo", "/repo/.loopover-replay-snapshots/abc1234"); expect(result).toEqual({ ok: true, removed: true }); - expect(calls[0]?.args).toEqual(["worktree", "remove", "--force", "/repo/.loopover-replay-snapshots/abc123"]); + expect(calls[0]?.args).toEqual(["worktree", "remove", "--force", "/repo/.loopover-replay-snapshots/abc1234"]); }); }); @@ -406,24 +425,24 @@ describe("openReplaySnapshotStore (#3010) — round-trip persistence", () => { const store = tempStore(); const saved = store.saveSnapshot({ repoFullName: "acme/widgets", - commitSha: "abc123", - worktreePath: "/repo/.loopover-replay-snapshots/abc123", + commitSha: "abc1234", + worktreePath: "/repo/.loopover-replay-snapshots/abc1234", targetDate: "2026-01-05T00:00:00+00:00", - commits: [{ sha: "abc123", date: "2026-01-05T00:00:00+00:00", subject: "t" }], - tags: [{ name: "v1", date: "2026-01-01T00:00:00+00:00", targetSha: "abc000" }], + commits: [{ sha: "abc1234", date: "2026-01-05T00:00:00+00:00", subject: "t" }], + tags: [{ name: "v1", date: "2026-01-01T00:00:00+00:00", targetSha: "abc0001" }], readme: { filename: "README.md", content: "# hi\n" }, }); expect(saved.readme).toEqual({ filename: "README.md", content: "# hi\n" }); - expect(store.getSnapshot("acme/widgets", "abc123")).toEqual(saved); + expect(store.getSnapshot("acme/widgets", "abc1234")).toEqual(saved); }); it("round-trips a snapshot with no README as null, not a partial object", () => { const store = tempStore(); const saved = store.saveSnapshot({ repoFullName: "acme/widgets", - commitSha: "abc123", - worktreePath: "/repo/.loopover-replay-snapshots/abc123", + commitSha: "abc1234", + worktreePath: "/repo/.loopover-replay-snapshots/abc1234", targetDate: "2026-01-05T00:00:00+00:00", commits: [], tags: [], diff --git a/test/unit/miner-store-seam-rollout.test.ts b/test/unit/miner-store-seam-rollout.test.ts index d668e45c08..5a5b63a5c7 100644 --- a/test/unit/miner-store-seam-rollout.test.ts +++ b/test/unit/miner-store-seam-rollout.test.ts @@ -99,23 +99,23 @@ describe("SqliteDriver seam rollout — non-transactional store round-trips (#72 it("replay-snapshot: saveSnapshot writes through the seam and getSnapshot reads the identical bundle back", () => { const store = track(openReplaySnapshotStore(tempPath("replay-snapshot.sqlite3"))); - expect(store.getSnapshot("acme/widgets", "abc123")).toBeNull(); + expect(store.getSnapshot("acme/widgets", "abc1234")).toBeNull(); const saved = store.saveSnapshot({ repoFullName: "acme/widgets", - commitSha: "abc123", - worktreePath: "/tmp/wt/abc123", + commitSha: "abc1234", + worktreePath: "/tmp/wt/abc1234", targetDate: "2026-07-01T00:00:00.000Z", - commits: [{ sha: "abc123", date: "2026-07-01T00:00:00.000Z", subject: "root" }], - tags: [{ name: "v1.0.0", date: "2026-07-01T00:00:00.000Z", targetSha: "abc123" }], + commits: [{ sha: "abc1234", date: "2026-07-01T00:00:00.000Z", subject: "root" }], + tags: [{ name: "v1.0.0", date: "2026-07-01T00:00:00.000Z", targetSha: "abc1234" }], readme: { filename: "README.md", content: "# hi" }, }); - expect(saved).toMatchObject({ repoFullName: "acme/widgets", commitSha: "abc123" }); + expect(saved).toMatchObject({ repoFullName: "acme/widgets", commitSha: "abc1234" }); - const read = store.getSnapshot("acme/widgets", "abc123"); + const read = store.getSnapshot("acme/widgets", "abc1234"); expect(read).toEqual(saved); // JSON-encoded columns survive the round-trip intact. - expect(read?.commits).toEqual([{ sha: "abc123", date: "2026-07-01T00:00:00.000Z", subject: "root" }]); + expect(read?.commits).toEqual([{ sha: "abc1234", date: "2026-07-01T00:00:00.000Z", subject: "root" }]); expect(read?.readme).toEqual({ filename: "README.md", content: "# hi" }); }); });