From 05db29cf97b020feb75dda154f83fd639ae4c034 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Tue, 21 Jul 2026 22:03:18 +0800 Subject: [PATCH] fix(miner): validate commit SHA format in replay-snapshot path planner (#7796) Co-authored-by: Cursor --- .../loopover-miner/lib/replay-snapshot.js | 8 +- .../loopover-miner/lib/replay-snapshot.ts | 5 +- test/unit/miner-replay-snapshot.test.ts | 105 ++++++++++-------- test/unit/miner-store-seam-rollout.test.ts | 16 +-- 4 files changed, 75 insertions(+), 59 deletions(-) diff --git a/packages/loopover-miner/lib/replay-snapshot.js b/packages/loopover-miner/lib/replay-snapshot.js index 6c9b28c2a3..2ee8c36557 100644 --- a/packages/loopover-miner/lib/replay-snapshot.js +++ b/packages/loopover-miner/lib/replay-snapshot.js @@ -11,6 +11,7 @@ function normalizeDbPath(dbPath) { } const FIELD_SEP = "\x1f"; const README_NAME_PATTERN = /^readme(\.\w+)?$/i; +const COMMIT_SHA_PATTERN = /^[0-9a-f]{7,40}$/i; function normalizeRepoFullName(repoFullName) { if (typeof repoFullName !== "string") throw new Error("invalid_repo_full_name"); @@ -22,7 +23,10 @@ function normalizeRepoFullName(repoFullName) { function normalizeCommitSha(commitSha) { 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. */ export const REPLAY_SNAPSHOT_SUBDIR = ".loopover-replay-snapshots"; @@ -242,4 +246,4 @@ export async function exportReplaySnapshot(input, deps) { export async function removeReplaySnapshotWorktree(exec, repoPath, worktreePath) { return removeWorktree({ exec, repoPath, worktreePath }); } -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVwbGF5LXNuYXBzaG90LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicmVwbGF5LXNuYXBzaG90LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFDakMsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBRWxELE9BQU8sRUFBRSxxQkFBcUIsRUFBRSx1QkFBdUIsRUFBRSx5QkFBeUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBcUQ3RyxNQUFNLGlCQUFpQixHQUFHLHlCQUF5QixDQUFDO0FBQ3BELElBQUksU0FBUyxHQUErQixJQUFJLENBQUM7QUFFakQsTUFBTSxVQUFVLDJCQUEyQixDQUFDLE1BQTBDLE9BQU8sQ0FBQyxHQUFHO0lBQy9GLE9BQU8sdUJBQXVCLENBQUMsaUJBQWlCLEVBQUUsbUNBQW1DLEVBQUUsR0FBRyxDQUFDLENBQUM7QUFDOUYsQ0FBQztBQUVELFNBQVMsZUFBZSxDQUFDLE1BQWM7SUFDckMsT0FBTyx5QkFBeUIsQ0FBQyxNQUFNLEVBQUUsMkJBQTJCLEVBQUUsRUFBRSxpQ0FBaUMsQ0FBQyxDQUFDO0FBQzdHLENBQUM7QUFFRCxNQUFNLFNBQVMsR0FBRyxNQUFNLENBQUM7QUFDekIsTUFBTSxtQkFBbUIsR0FBRyxtQkFBbUIsQ0FBQztBQUVoRCxTQUFTLHFCQUFxQixDQUFDLFlBQW9CO0lBQ2pELElBQUksT0FBTyxZQUFZLEtBQUssUUFBUTtRQUFFLE1BQU0sSUFBSSxLQUFLLENBQUMsd0JBQXdCLENBQUMsQ0FBQztJQUNoRixNQUFNLENBQUMsS0FBSyxFQUFFLElBQUksRUFBRSxLQUFLLENBQUMsR0FBRyxZQUFZLENBQUMsSUFBSSxFQUFFLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQzVELElBQUksQ0FBQyxLQUFLLElBQUksQ0FBQyxJQUFJLElBQUksS0FBSyxLQUFLLFNBQVM7UUFBRSxNQUFNLElBQUksS0FB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 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVwbGF5LXNuYXBzaG90LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsicmVwbGF5LXNuYXBzaG90LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxJQUFJLEVBQUUsTUFBTSxXQUFXLENBQUM7QUFDakMsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBRWxELE9BQU8sRUFBRSxxQkFBcUIsRUFBRSx1QkFBdUIsRUFBRSx5QkFBeUIsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBcUQ3RyxNQUFNLGlCQUFpQixHQUFHLHlCQUF5QixDQUFDO0FBQ3BELElBQUksU0FBUyxHQUErQixJQUFJLENBQUM7QUFFakQsTUFBTSxVQUFVLDJCQUEyQixDQUFDLE1BQTBDLE9BQU8sQ0FBQyxHQUFHO0lBQy9GLE9BQU8sdUJBQXVCLENBQUMsaUJBQWlCLEVBQUUsbUNBQW1DLEVBQUUsR0FBRyxDQUFDLENBQUM7QUFDOUYsQ0FBQztBQUVELFNBQVMsZUFBZSxDQUFDLE1BQWM7SUFDckMsT0FBTyx5QkFBeUIsQ0FBQyxNQUFNLEVBQUUsMkJBQTJCLEVBQUUsRUFBRSxpQ0FBaUMsQ0FBQyxDQUFDO0FBQzdHLENBQUM7QUFFRCxNQUFNLFNBQVMsR0FBRyxNQUFNLENBQUM7QUFDekIsTUFBTSxtQkFBbUIsR0FBRyxtQkFBbUIsQ0FBQztBQUNoRCxNQUFNLGtCQUFrQixHQUFHLG1CQUFtQixDQUFDO0FBRS9DLFNBQVMscUJBQXFCLENBQUMsWUFBb0I7SUFDakQsSUFBSSxPQUFPLFlBQVksS0FBSyxRQUFRO1FBQUUsTUFBTSxJQUFJLEtBQUssQ0FBQyx3QkFBd0IsQ0FBQyxDQUFDO0lBQ2hGLE1BQU0sQ0FBQyxLQUFLLEVBQUUsSUFBSSxFQUFFLEtBQUssQ0FBQyxHQUFHLFlBQVksQ0FBQyxJQUFJLEVBQUUsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDNUQsSUFBSSxDQUFDLEtBQUssSUFBSSxDQUFDLElBQUksSUFB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 \ No newline at end of file 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..0406740d8c 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,19 @@ 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"); }); }); @@ -129,25 +138,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 +166,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 +179,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 +195,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" }]); }); @@ -210,7 +219,7 @@ describe("exportReplaySnapshot (#3010)", () => { 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 +229,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 +266,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 +275,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 +293,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 +302,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 +311,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 +320,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 +329,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 +338,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 +356,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 +378,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 +394,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 +404,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 +415,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" }); }); });