import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { checkpointRefForThreadTurn, FilesystemCheckpointStore } from "./filesystemCheckpointStore"; import { runProcess } from "./processRunner"; async function runGit(cwd: string, args: readonly string[]): Promise { const result = await runProcess("git", args, { cwd, allowNonZeroExit: true, }); if (result.code !== 0) { throw new Error(result.stderr.trim() || `git ${args.join(" ")} failed`); } return result.stdout.trim(); } function exists(filePath: string): boolean { return fs.existsSync(filePath); } async function createRepo(): Promise { const cwd = await mkdtemp(path.join(os.tmpdir(), "t3-fs-checkpoints-test-")); await runGit(cwd, ["init", "--initial-branch=main"]); await runGit(cwd, ["config", "user.email", "test@example.com"]); await runGit(cwd, ["config", "user.name", "Test User"]); await writeFile(path.join(cwd, "README.md"), "v1\n"); await writeFile(path.join(cwd, "tracked.txt"), "tracked-v1\n"); await runGit(cwd, ["add", "."]); await runGit(cwd, ["commit", "-m", "Initial"]); return cwd; } async function createRepoWithoutHead(): Promise { const cwd = await mkdtemp(path.join(os.tmpdir(), "t3-fs-checkpoints-empty-test-")); await runGit(cwd, ["init", "--initial-branch=main"]); return cwd; } describe("FilesystemCheckpointStore", () => { it("captures refs without mutating branch history and restores workspace state", async () => { const cwd = await createRepo(); const store = new FilesystemCheckpointStore(); const threadId = "thr_1"; try { const headBefore = await runGit(cwd, ["rev-parse", "HEAD"]); await store.captureCheckpoint({ cwd, threadId, turnCount: 0 }); const rootRef = checkpointRefForThreadTurn(threadId, 0); const rootCommit = await runGit(cwd, ["rev-parse", "--verify", `${rootRef}^{commit}`]); expect(rootCommit.length).toBeGreaterThan(0); expect(await runGit(cwd, ["rev-parse", "HEAD"])).toBe(headBefore); await writeFile(path.join(cwd, "README.md"), "v2\n"); await rm(path.join(cwd, "tracked.txt")); await writeFile(path.join(cwd, "notes.md"), "notes\n"); await store.captureCheckpoint({ cwd, threadId, turnCount: 1 }); await writeFile(path.join(cwd, "README.md"), "v3\n"); await writeFile(path.join(cwd, "scratch.txt"), "scratch\n"); await writeFile(path.join(cwd, "tracked.txt"), "tracked-new\n"); const restoredRoot = await store.restoreCheckpoint({ cwd, threadId, turnCount: 0 }); expect(restoredRoot).toBe(true); expect(await readFile(path.join(cwd, "README.md"), "utf8")).toBe("v1\n"); expect(await readFile(path.join(cwd, "tracked.txt"), "utf8")).toBe("tracked-v1\n"); expect(exists(path.join(cwd, "notes.md"))).toBe(false); expect(exists(path.join(cwd, "scratch.txt"))).toBe(false); const restoredTurn1 = await store.restoreCheckpoint({ cwd, threadId, turnCount: 1 }); expect(restoredTurn1).toBe(true); expect(await readFile(path.join(cwd, "README.md"), "utf8")).toBe("v2\n"); expect(exists(path.join(cwd, "tracked.txt"))).toBe(false); expect(await readFile(path.join(cwd, "notes.md"), "utf8")).toBe("notes\n"); expect(exists(path.join(cwd, "scratch.txt"))).toBe(false); } finally { await rm(cwd, { recursive: true, force: true }); } }); it("returns false when restoring a missing checkpoint", async () => { const cwd = await createRepo(); const store = new FilesystemCheckpointStore(); try { const restored = await store.restoreCheckpoint({ cwd, threadId: "thr_2", turnCount: 999, }); expect(restored).toBe(false); } finally { await rm(cwd, { recursive: true, force: true }); } }); it("falls back to HEAD when restoring turn 0 and root checkpoint ref is missing", async () => { const cwd = await createRepo(); const store = new FilesystemCheckpointStore(); const threadId = "thr_2b"; try { await writeFile(path.join(cwd, "README.md"), "v2\n"); await store.captureCheckpoint({ cwd, threadId, turnCount: 1 }); await writeFile(path.join(cwd, "README.md"), "v3\n"); await writeFile(path.join(cwd, "scratch.txt"), "scratch\n"); const restored = await store.restoreCheckpoint({ cwd, threadId, turnCount: 0, }); expect(restored).toBe(true); expect(await readFile(path.join(cwd, "README.md"), "utf8")).toBe("v1\n"); expect(exists(path.join(cwd, "scratch.txt"))).toBe(false); } finally { await rm(cwd, { recursive: true, force: true }); } }); it("prunes refs newer than the retained turn", async () => { const cwd = await createRepo(); const store = new FilesystemCheckpointStore(); const threadId = "thr_3"; try { await store.captureCheckpoint({ cwd, threadId, turnCount: 0 }); await writeFile(path.join(cwd, "README.md"), "turn1\n"); await store.captureCheckpoint({ cwd, threadId, turnCount: 1 }); await writeFile(path.join(cwd, "README.md"), "turn2\n"); await store.captureCheckpoint({ cwd, threadId, turnCount: 2 }); await store.pruneAfterTurn({ cwd, threadId, maxTurnCount: 1, }); expect(await store.hasCheckpoint({ cwd, threadId, turnCount: 0 })).toBe(true); expect(await store.hasCheckpoint({ cwd, threadId, turnCount: 1 })).toBe(true); expect(await store.hasCheckpoint({ cwd, threadId, turnCount: 2 })).toBe(false); } finally { await rm(cwd, { recursive: true, force: true }); } }); it("returns a unified patch between checkpoint turns", async () => { const cwd = await createRepo(); const store = new FilesystemCheckpointStore(); const threadId = "thr_4"; try { await store.captureCheckpoint({ cwd, threadId, turnCount: 0 }); await writeFile(path.join(cwd, "README.md"), "v2\n"); await writeFile(path.join(cwd, "new.txt"), "new file\n"); await store.captureCheckpoint({ cwd, threadId, turnCount: 1 }); const diff = await store.diffCheckpoints({ cwd, threadId, fromTurnCount: 0, toTurnCount: 1, }); expect(diff).toContain("diff --git a/README.md b/README.md"); expect(diff).toContain("diff --git a/new.txt b/new.txt"); } finally { await rm(cwd, { recursive: true, force: true }); } }); it("falls back to HEAD as diff base when turn 0 checkpoint is missing", async () => { const cwd = await createRepo(); const store = new FilesystemCheckpointStore(); const threadId = "thr_5"; try { await writeFile(path.join(cwd, "README.md"), "v2\n"); await writeFile(path.join(cwd, "new.txt"), "new file\n"); await store.captureCheckpoint({ cwd, threadId, turnCount: 1 }); const diff = await store.diffCheckpoints({ cwd, threadId, fromTurnCount: 0, toTurnCount: 1, }); expect(diff).toContain("diff --git a/README.md b/README.md"); expect(diff).toContain("diff --git a/new.txt b/new.txt"); } finally { await rm(cwd, { recursive: true, force: true }); } }); it("detects non-repo folders", async () => { const cwd = await mkdtemp(path.join(os.tmpdir(), "t3-fs-checkpoints-nonrepo-")); const store = new FilesystemCheckpointStore(); try { expect(await store.isGitRepository(cwd)).toBe(false); } finally { await rm(cwd, { recursive: true, force: true }); } }); it("returns false when repository path does not exist", async () => { const store = new FilesystemCheckpointStore(); const cwd = path.join(os.tmpdir(), `t3-fs-checkpoints-missing-${Date.now()}`); expect(await store.isGitRepository(cwd)).toBe(false); }); it("restores checkpoint without reset when repository has no HEAD commit", async () => { const cwd = await createRepoWithoutHead(); const store = new FilesystemCheckpointStore(); const threadId = "thr-no-head"; try { await writeFile(path.join(cwd, "README.md"), "first\n"); await store.captureCheckpoint({ cwd, threadId, turnCount: 0 }); await writeFile(path.join(cwd, "README.md"), "second\n"); const restored = await store.restoreCheckpoint({ cwd, threadId, turnCount: 0, }); expect(restored).toBe(true); expect(await readFile(path.join(cwd, "README.md"), "utf8")).toBe("first\n"); } finally { await rm(cwd, { recursive: true, force: true }); } }); });