diff --git a/packages/loopover-miner/lib/replay-snapshot.ts b/packages/loopover-miner/lib/replay-snapshot.ts index 7cdc14dc6e..a2a4ea1b9f 100644 --- a/packages/loopover-miner/lib/replay-snapshot.ts +++ b/packages/loopover-miner/lib/replay-snapshot.ts @@ -75,9 +75,14 @@ function normalizeRepoFullName(repoFullName: string): string { return `${owner}/${repo}`; } +/** Same format guard as replay-task-generation.ts — reject path-traversal / non-hex values before path.join (#7796). */ +const COMMIT_SHA_PATTERN = /^[0-9a-f]{7,40}$/i; + 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..f68073f41c 100644 --- a/test/unit/miner-replay-snapshot.test.ts +++ b/test/unit/miner-replay-snapshot.test.ts @@ -47,7 +47,7 @@ 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: 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}abc000${FIELD_SEP}tag\n`) }, { match: isLsTree, result: ok("README.md\nsrc\npackage.json\n") }, { match: isShow, result: ok("# hello\n") }, @@ -72,10 +72,32 @@ 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("normalizes hex case so the planned path is stable (#7796)", () => { + const lower = planReplaySnapshotPath({ repoPath: "/repo", commitSha: "abcdef0" }); + const upper = planReplaySnapshotPath({ repoPath: "/repo", commitSha: "ABCDEF0" }); + expect(upper).toBe(lower); + expect(upper.replaceAll("\\", "/")).toBe(`/repo/${REPLAY_SNAPSHOT_SUBDIR}/abcdef0`); + }); + + it("rejects a path-traversal-shaped commitSha before path.join (#7796)", () => { + expect(() => + planReplaySnapshotPath({ + repoPath: "/home/miner/.config/loopover-miner/repos/acme/widgets", + commitSha: "../../../../../../tmp/evil-worktree", + }), + ).toThrow("invalid_commit_sha"); + }); + + it("rejects non-hex and too-short commitSha values (#7796)", () => { + expect(() => planReplaySnapshotPath({ repoPath: "/repo", commitSha: "not-a-sha!" })).toThrow("invalid_commit_sha"); + expect(() => planReplaySnapshotPath({ repoPath: "/repo", commitSha: "abc12" })).toThrow("invalid_commit_sha"); + expect(() => planReplaySnapshotPath({ repoPath: "/repo", commitSha: " " })).toThrow("invalid_commit_sha"); }); }); @@ -124,19 +146,31 @@ describe("validateSnapshotFreshness (#3010) — pure fail-fast check", () => { }); describe("exportReplaySnapshot (#3010)", () => { + it("rejects a path-traversal commitSha before any git work (#7796)", async () => { + const { exec, calls } = scriptedExec([]); + const store = tempStore(); + await expect( + exportReplaySnapshot( + { repoPath: "/repo", repoFullName: "acme/widgets", commitSha: "../../../../tmp/evil" }, + { exec, store }, + ), + ).rejects.toThrow("invalid_commit_sha"); + expect(calls).toHaveLength(0); + }); + it("exports a fresh snapshot: worktree, target date, commit history, reachable tags, and README", async () => { const { exec } = scriptedExec(happyPathScripts()); 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.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: "abc000" }]); expect(snapshot.readme).toEqual({ filename: "README.md", content: "# hello\n" }); }); @@ -144,10 +178,10 @@ describe("exportReplaySnapshot (#3010)", () => { 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 +191,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 +204,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 +220,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 +228,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(`abcd000${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: "abcd000" }, { exec, store }); - expect(snapshot.commits).toEqual([{ sha: "root000", date: "2020-01-01T00:00:00+00:00", subject: "initial commit" }]); + expect(snapshot.commits).toEqual([{ sha: "abcd000", 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,7 +254,7 @@ 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"); }); @@ -229,19 +263,19 @@ describe("exportReplaySnapshot (#3010)", () => { 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 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 +291,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 +300,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: "abcdef0" }, { exec, store })).rejects.toThrow( /git_worktree_add_failed.*fatal: invalid reference/, ); }); @@ -284,7 +318,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: "abcdef0" }, { exec, store })).rejects.toThrow( /git_log_target_failed/, ); }); @@ -293,7 +327,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: "abcdef0" }, { exec, store })).rejects.toThrow( /git_log_target_failed: no commit found/, ); }); @@ -302,7 +336,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 +345,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 +354,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 +363,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 +381,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 +403,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 +419,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 +429,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 +440,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" }], + 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: "abc000" }], 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: [], @@ -435,6 +469,6 @@ describe("openReplaySnapshotStore (#3010) — round-trip persistence", () => { it("getSnapshot returns null for an unknown (repo, commit) pair", () => { const store = tempStore(); - expect(store.getSnapshot("acme/widgets", "nope")).toBeNull(); + expect(store.getSnapshot("acme/widgets", "deadbee")).toBeNull(); }); });