// @effect-diagnostics nodeBuiltinImport:offimport * as NodePath from "node:path"; import * as NodeServices from "@effect/platform-node/NodeServices";import { it } from "@effect/vitest";import { ThreadId, type VcsError } from "@t3tools/contracts";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as PlatformError from "effect/PlatformError";import * as Scope from "effect/Scope";import { describe, expect } from "vite-plus/test"; import { checkpointRefForThreadTurn } from "./Utils.ts";import * as CheckpointStore from "./CheckpointStore.ts";import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";import * as VcsProcess from "../vcs/VcsProcess.ts";import * as ServerConfig from "../config.ts"; const ServerConfigLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-checkpoint-store-test-",});const VcsProcessTestLayer = VcsProcess.layer.pipe(Layer.provide(NodeServices.layer));const VcsDriverTestLayer = VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcessTestLayer));const CheckpointStoreTestLayer = CheckpointStore.layer.pipe( Layer.provideMerge(VcsDriverTestLayer), Layer.provideMerge(NodeServices.layer),);const TestLayer = CheckpointStoreTestLayer.pipe( Layer.provideMerge(VcsProcessTestLayer), Layer.provideMerge(VcsDriverTestLayer), Layer.provideMerge(ServerConfigLayer), Layer.provideMerge(NodeServices.layer),); function makeTmpDir( prefix = "checkpoint-store-test-",): Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope> { return Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; return yield* fileSystem.makeTempDirectoryScoped({ prefix }); });} function writeTextFile( filePath: string, contents: string,): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem> { return Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; yield* fileSystem.writeFileString(filePath, contents); });} function git( cwd: string, args: ReadonlyArray<string>, env?: NodeJS.ProcessEnv,): Effect.Effect<string, VcsError, VcsProcess.VcsProcess> { return Effect.gen(function* () { const process = yield* VcsProcess.VcsProcess; const result = yield* process.run({ operation: "CheckpointStore.test.git", command: "git", cwd, args, ...(env ? { env: { ...global.process.env, ...env } } : {}), timeoutMs: 10_000, }); return result.stdout.trim(); });} /** Author and committer dates both in the past: a genuinely pre-existing commit. */const PRE_EXISTING_COMMIT_ENV: NodeJS.ProcessEnv = { GIT_AUTHOR_DATE: "2020-01-01T00:00:00Z", GIT_COMMITTER_DATE: "2020-01-01T00:00:00Z",}; function initRepoWithCommit( cwd: string,): Effect.Effect< void, VcsError | PlatformError.PlatformError, VcsProcess.VcsProcess | FileSystem.FileSystem> { return Effect.gen(function* () { yield* git(cwd, ["init"]); yield* git(cwd, ["config", "user.email", "test@test.com"]); yield* git(cwd, ["config", "user.name", "Test"]); yield* writeTextFile(NodePath.join(cwd, "README.md"), "# test\n"); yield* git(cwd, ["add", "."]); yield* git(cwd, ["commit", "-m", "initial commit"]); });} function buildLargeText(lineCount = 5_000): string { return Array.from({ length: lineCount }, (_, index) => `line ${String(index).padStart(5, "0")}`) .join("\n") .concat("\n");} it.layer(TestLayer)("CheckpointStore.layer", (it) => { describe("isGitRepository", () => { it.effect("returns false when no Git repository is detected", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); const checkpointStore = yield* CheckpointStore.CheckpointStore; expect(yield* checkpointStore.isGitRepository(tmp)).toBe(false); }), ); it.effect("returns true when a Git repository is detected", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); yield* initRepoWithCommit(tmp); const checkpointStore = yield* CheckpointStore.CheckpointStore; expect(yield* checkpointStore.isGitRepository(tmp)).toBe(true); }), ); }); describe("diffCheckpoints", () => { it.effect("returns full oversized checkpoint diffs without truncation", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); yield* initRepoWithCommit(tmp); const checkpointStore = yield* CheckpointStore.CheckpointStore; const threadId = ThreadId.make("thread-checkpoint-store"); const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0); const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef, }); yield* writeTextFile(NodePath.join(tmp, "README.md"), buildLargeText()); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef, }); const diff = yield* checkpointStore.diffCheckpoints({ cwd: tmp, fromCheckpointRef, toCheckpointRef, ignoreWhitespace: true, }); expect(diff).toContain("diff --git"); expect(diff).not.toContain("[truncated]"); expect(diff).toContain("+line 04999"); }), ); it.effect("can hide indentation churn when changes wrap existing lines", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); yield* initRepoWithCommit(tmp); const checkpointStore = yield* CheckpointStore.CheckpointStore; const threadId = ThreadId.make("thread-checkpoint-store-whitespace"); const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0); const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); const componentPath = NodePath.join(tmp, "Component.tsx"); yield* writeTextFile( componentPath, [ "export function View() {", " return (", " <section>", " <h1>Title</h1>", " <p>Body</p>", " </section>", " );", "}", "", ].join("\n"), ); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef, }); yield* writeTextFile( componentPath, [ "export function View() {", " return (", " <section>", " {isReady ? (", " <div>", " <h1>Title</h1>", " <p>Body</p>", " </div>", " ) : null}", " </section>", " );", "}", "", ].join("\n"), ); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef, }); const normalDiff = yield* checkpointStore.diffCheckpoints({ cwd: tmp, fromCheckpointRef, toCheckpointRef, ignoreWhitespace: false, }); const whitespaceIgnoredDiff = yield* checkpointStore.diffCheckpoints({ cwd: tmp, fromCheckpointRef, toCheckpointRef, ignoreWhitespace: true, }); expect(normalDiff).toContain("diff --git"); expect(normalDiff).toContain("- <h1>Title</h1>"); expect(normalDiff).toContain("+ <h1>Title</h1>"); expect(whitespaceIgnoredDiff).toContain("diff --git"); expect(whitespaceIgnoredDiff).toContain("+ {isReady ? ("); expect(whitespaceIgnoredDiff).toContain("+ <div>"); expect(whitespaceIgnoredDiff).not.toContain("- <h1>Title</h1>"); expect(whitespaceIgnoredDiff).not.toContain("+ <h1>Title</h1>"); }), ); }); describe("attributeCheckpointDiff", () => { it.effect("returns an empty map when HEAD did not move", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); yield* initRepoWithCommit(tmp); const checkpointStore = yield* CheckpointStore.CheckpointStore; const threadId = ThreadId.make("thread-attr-no-head-move"); const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0); const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef }); yield* writeTextFile(NodePath.join(tmp, "edited.ts"), "export const edited = true;\n"); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef }); const attribution = yield* checkpointStore.attributeCheckpointDiff({ cwd: tmp, fromCheckpointRef, toCheckpointRef, }); expect(attribution).not.toBe(null); expect(attribution!.size).toBe(0); }), ); it.effect("attributes a branch switch to git while keeping tool edits as agent work", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); yield* initRepoWithCommit(tmp); const defaultBranch = yield* git(tmp, ["rev-parse", "--abbrev-ref", "HEAD"]); // A second branch with a pre-existing commit, created before the turn. yield* git(tmp, ["checkout", "-b", "feature"]); yield* writeTextFile(NodePath.join(tmp, "feature.ts"), "export const feature = 1;\n"); yield* git(tmp, ["add", "."]); yield* git(tmp, ["commit", "-m", "feature work"], PRE_EXISTING_COMMIT_ENV); yield* git(tmp, ["checkout", defaultBranch]); const checkpointStore = yield* CheckpointStore.CheckpointStore; const threadId = ThreadId.make("thread-attr-branch-switch"); const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0); const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef }); // The "turn": switch branches (history-driven) and also edit a file. yield* git(tmp, ["checkout", "feature"]); yield* writeTextFile(NodePath.join(tmp, "edited.ts"), "export const edited = true;\n"); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef }); const attribution = yield* checkpointStore.attributeCheckpointDiff({ cwd: tmp, fromCheckpointRef, toCheckpointRef, }); expect(attribution).not.toBe(null); expect(attribution!.get("feature.ts")).toBe("git"); expect(attribution!.get("edited.ts")).toBe("agent"); }), ); it.effect("keeps files committed during the turn as agent work", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); yield* initRepoWithCommit(tmp); const checkpointStore = yield* CheckpointStore.CheckpointStore; const threadId = ThreadId.make("thread-attr-own-commit"); const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0); const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef }); // The "turn": edit and commit — HEAD moves, but by a turn-authored commit. yield* writeTextFile(NodePath.join(tmp, "committed.ts"), "export const done = true;\n"); yield* git(tmp, ["add", "."]); yield* git(tmp, ["commit", "-m", "agent commit"]); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef }); const attribution = yield* checkpointStore.attributeCheckpointDiff({ cwd: tmp, fromCheckpointRef, toCheckpointRef, }); expect(attribution).not.toBe(null); expect(attribution!.get("committed.ts")).toBe("agent"); }), ); it.effect("attributes a cherry-pick of a pre-existing commit to git", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); yield* initRepoWithCommit(tmp); const defaultBranch = yield* git(tmp, ["rev-parse", "--abbrev-ref", "HEAD"]); // Pre-existing commit on another branch, authored before the turn. yield* git(tmp, ["checkout", "-b", "source"]); yield* writeTextFile(NodePath.join(tmp, "picked.ts"), "export const picked = 1;\n"); yield* git(tmp, ["add", "."]); yield* git( tmp, [ "-c", "user.email=other@test.com", "-c", "user.name=Other", "commit", "-m", "source work", ], PRE_EXISTING_COMMIT_ENV, ); const pickedOid = yield* git(tmp, ["rev-parse", "HEAD"]); yield* git(tmp, ["checkout", defaultBranch]); const checkpointStore = yield* CheckpointStore.CheckpointStore; const threadId = ThreadId.make("thread-attr-cherry-pick"); const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0); const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef }); yield* git(tmp, ["cherry-pick", pickedOid]); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef }); const attribution = yield* checkpointStore.attributeCheckpointDiff({ cwd: tmp, fromCheckpointRef, toCheckpointRef, }); expect(attribution).not.toBe(null); expect(attribution!.get("picked.ts")).toBe("git"); }), ); it.effect("keeps an amend of a pre-turn commit as agent work", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); yield* initRepoWithCommit(tmp); // Pre-turn commit with an old author date, as amend preserves it. yield* writeTextFile(NodePath.join(tmp, "amended.ts"), "export const value = 1;\n"); yield* git(tmp, ["add", "."]); yield* git(tmp, ["commit", "-m", "pre-turn work", "--date", "2020-01-01T00:00:00Z"]); const checkpointStore = yield* CheckpointStore.CheckpointStore; const threadId = ThreadId.make("thread-attr-amend"); const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0); const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef }); // The "turn": edit the file and amend it into the pre-turn commit. // The author date stays 2020, but the content is new agent work. yield* writeTextFile(NodePath.join(tmp, "amended.ts"), "export const value = 2;\n"); yield* git(tmp, ["add", "."]); yield* git(tmp, ["commit", "--amend", "--no-edit"]); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef }); const attribution = yield* checkpointStore.attributeCheckpointDiff({ cwd: tmp, fromCheckpointRef, toCheckpointRef, }); expect(attribution).not.toBe(null); expect(attribution!.get("amended.ts")).toBe("agent"); }), ); it.effect("keeps a whitespace-only amend of a pre-turn commit as agent work", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); yield* initRepoWithCommit(tmp); // Pre-turn commit; its pre-amend state stays reachable via reflog, // which is exactly the patch-id collision candidate. yield* writeTextFile(NodePath.join(tmp, "style.ts"), "export const x = 1;\n"); yield* git(tmp, ["add", "."]); yield* git(tmp, ["commit", "-m", "pre-turn work"], PRE_EXISTING_COMMIT_ENV); const checkpointStore = yield* CheckpointStore.CheckpointStore; const threadId = ThreadId.make("thread-attr-ws-amend"); const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0); const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef }); // The "turn": whitespace-only reformat amended into the pre-turn // commit. `git patch-id --stable` would hash this identically to the // original (it strips whitespace); --verbatim must not. yield* writeTextFile(NodePath.join(tmp, "style.ts"), "export const x = 1;\n"); yield* git(tmp, ["add", "."]); yield* git(tmp, ["commit", "--amend", "--no-edit"]); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef }); const attribution = yield* checkpointStore.attributeCheckpointDiff({ cwd: tmp, fromCheckpointRef, toCheckpointRef, }); expect(attribution).not.toBe(null); expect(attribution!.get("style.ts")).toBe("agent"); }), ); it.effect("returns null for legacy checkpoints without head metadata", () => Effect.gen(function* () { const tmp = yield* makeTmpDir(); yield* initRepoWithCommit(tmp); const checkpointStore = yield* CheckpointStore.CheckpointStore; const threadId = ThreadId.make("thread-attr-legacy"); const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0); const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1); // Simulate a pre-attribution checkpoint: commit the tree with the old // single-line message format. const treeOid = yield* git(tmp, ["write-tree"]); const legacyCommit = yield* git(tmp, [ "commit-tree", treeOid, "-m", `t3 checkpoint ref=${fromCheckpointRef}`, ]); yield* git(tmp, ["update-ref", fromCheckpointRef, legacyCommit]); yield* writeTextFile(NodePath.join(tmp, "edited.ts"), "export const edited = true;\n"); yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef }); const attribution = yield* checkpointStore.attributeCheckpointDiff({ cwd: tmp, fromCheckpointRef, toCheckpointRef, }); expect(attribution).toBe(null); }), ); });});
// @effect-diagnostics nodeBuiltinImport:off
import * as NodePath from "node:path";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { it } from "@effect/vitest";
import { ThreadId, type VcsError } from "@t3tools/contracts";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as PlatformError from "effect/PlatformError";
import * as Scope from "effect/Scope";
import { describe, expect } from "vite-plus/test";
import { checkpointRefForThreadTurn } from "./Utils.ts";
import * as CheckpointStore from "./CheckpointStore.ts";
import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
import * as VcsProcess from "../vcs/VcsProcess.ts";
import * as ServerConfig from "../config.ts";
const ServerConfigLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {
prefix: "t3-checkpoint-store-test-",
});
const VcsProcessTestLayer = VcsProcess.layer.pipe(Layer.provide(NodeServices.layer));
const VcsDriverTestLayer = VcsDriverRegistry.layer.pipe(Layer.provide(VcsProcessTestLayer));
const CheckpointStoreTestLayer = CheckpointStore.layer.pipe(
Layer.provideMerge(VcsDriverTestLayer),
Layer.provideMerge(NodeServices.layer),
);
const TestLayer = CheckpointStoreTestLayer.pipe(
Layer.provideMerge(VcsProcessTestLayer),
Layer.provideMerge(VcsDriverTestLayer),
Layer.provideMerge(ServerConfigLayer),
Layer.provideMerge(NodeServices.layer),
);
function makeTmpDir(
prefix = "checkpoint-store-test-",
): Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope> {
return Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
return yield* fileSystem.makeTempDirectoryScoped({ prefix });
});
}
function writeTextFile(
filePath: string,
contents: string,
): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem> {
return Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
yield* fileSystem.writeFileString(filePath, contents);
});
}
function git(
cwd: string,
args: ReadonlyArray<string>,
env?: NodeJS.ProcessEnv,
): Effect.Effect<string, VcsError, VcsProcess.VcsProcess> {
return Effect.gen(function* () {
const process = yield* VcsProcess.VcsProcess;
const result = yield* process.run({
operation: "CheckpointStore.test.git",
command: "git",
cwd,
args,
...(env ? { env: { ...global.process.env, ...env } } : {}),
timeoutMs: 10_000,
});
return result.stdout.trim();
});
}
/** Author and committer dates both in the past: a genuinely pre-existing commit. */
const PRE_EXISTING_COMMIT_ENV: NodeJS.ProcessEnv = {
GIT_AUTHOR_DATE: "2020-01-01T00:00:00Z",
GIT_COMMITTER_DATE: "2020-01-01T00:00:00Z",
};
function initRepoWithCommit(
cwd: string,
): Effect.Effect<
void,
VcsError | PlatformError.PlatformError,
VcsProcess.VcsProcess | FileSystem.FileSystem
> {
return Effect.gen(function* () {
yield* git(cwd, ["init"]);
yield* git(cwd, ["config", "user.email", "test@test.com"]);
yield* git(cwd, ["config", "user.name", "Test"]);
yield* writeTextFile(NodePath.join(cwd, "README.md"), "# test\n");
yield* git(cwd, ["add", "."]);
yield* git(cwd, ["commit", "-m", "initial commit"]);
});
}
function buildLargeText(lineCount = 5_000): string {
return Array.from({ length: lineCount }, (_, index) => `line ${String(index).padStart(5, "0")}`)
.join("\n")
.concat("\n");
}
it.layer(TestLayer)("CheckpointStore.layer", (it) => {
describe("isGitRepository", () => {
it.effect("returns false when no Git repository is detected", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
const checkpointStore = yield* CheckpointStore.CheckpointStore;
expect(yield* checkpointStore.isGitRepository(tmp)).toBe(false);
}),
);
it.effect("returns true when a Git repository is detected", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
const checkpointStore = yield* CheckpointStore.CheckpointStore;
expect(yield* checkpointStore.isGitRepository(tmp)).toBe(true);
}),
);
});
describe("diffCheckpoints", () => {
it.effect("returns full oversized checkpoint diffs without truncation", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
const checkpointStore = yield* CheckpointStore.CheckpointStore;
const threadId = ThreadId.make("thread-checkpoint-store");
const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
yield* checkpointStore.captureCheckpoint({
cwd: tmp,
checkpointRef: fromCheckpointRef,
});
yield* writeTextFile(NodePath.join(tmp, "README.md"), buildLargeText());
yield* checkpointStore.captureCheckpoint({
cwd: tmp,
checkpointRef: toCheckpointRef,
});
const diff = yield* checkpointStore.diffCheckpoints({
cwd: tmp,
fromCheckpointRef,
toCheckpointRef,
ignoreWhitespace: true,
});
expect(diff).toContain("diff --git");
expect(diff).not.toContain("[truncated]");
expect(diff).toContain("+line 04999");
}),
);
it.effect("can hide indentation churn when changes wrap existing lines", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
const checkpointStore = yield* CheckpointStore.CheckpointStore;
const threadId = ThreadId.make("thread-checkpoint-store-whitespace");
const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
const componentPath = NodePath.join(tmp, "Component.tsx");
yield* writeTextFile(
componentPath,
[
"export function View() {",
" return (",
" <section>",
" <h1>Title</h1>",
" <p>Body</p>",
" </section>",
" );",
"}",
"",
].join("\n"),
);
yield* checkpointStore.captureCheckpoint({
cwd: tmp,
checkpointRef: fromCheckpointRef,
});
yield* writeTextFile(
componentPath,
[
"export function View() {",
" return (",
" <section>",
" {isReady ? (",
" <div>",
" <h1>Title</h1>",
" <p>Body</p>",
" </div>",
" ) : null}",
" </section>",
" );",
"}",
"",
].join("\n"),
);
yield* checkpointStore.captureCheckpoint({
cwd: tmp,
checkpointRef: toCheckpointRef,
});
const normalDiff = yield* checkpointStore.diffCheckpoints({
cwd: tmp,
fromCheckpointRef,
toCheckpointRef,
ignoreWhitespace: false,
});
const whitespaceIgnoredDiff = yield* checkpointStore.diffCheckpoints({
cwd: tmp,
fromCheckpointRef,
toCheckpointRef,
ignoreWhitespace: true,
});
expect(normalDiff).toContain("diff --git");
expect(normalDiff).toContain("- <h1>Title</h1>");
expect(normalDiff).toContain("+ <h1>Title</h1>");
expect(whitespaceIgnoredDiff).toContain("diff --git");
expect(whitespaceIgnoredDiff).toContain("+ {isReady ? (");
expect(whitespaceIgnoredDiff).toContain("+ <div>");
expect(whitespaceIgnoredDiff).not.toContain("- <h1>Title</h1>");
expect(whitespaceIgnoredDiff).not.toContain("+ <h1>Title</h1>");
}),
);
});
describe("attributeCheckpointDiff", () => {
it.effect("returns an empty map when HEAD did not move", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
const checkpointStore = yield* CheckpointStore.CheckpointStore;
const threadId = ThreadId.make("thread-attr-no-head-move");
const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef });
yield* writeTextFile(NodePath.join(tmp, "edited.ts"), "export const edited = true;\n");
yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef });
const attribution = yield* checkpointStore.attributeCheckpointDiff({
cwd: tmp,
fromCheckpointRef,
toCheckpointRef,
});
expect(attribution).not.toBe(null);
expect(attribution!.size).toBe(0);
}),
);
it.effect("attributes a branch switch to git while keeping tool edits as agent work", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
const defaultBranch = yield* git(tmp, ["rev-parse", "--abbrev-ref", "HEAD"]);
// A second branch with a pre-existing commit, created before the turn.
yield* git(tmp, ["checkout", "-b", "feature"]);
yield* writeTextFile(NodePath.join(tmp, "feature.ts"), "export const feature = 1;\n");
yield* git(tmp, ["add", "."]);
yield* git(tmp, ["commit", "-m", "feature work"], PRE_EXISTING_COMMIT_ENV);
yield* git(tmp, ["checkout", defaultBranch]);
const checkpointStore = yield* CheckpointStore.CheckpointStore;
const threadId = ThreadId.make("thread-attr-branch-switch");
const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef });
// The "turn": switch branches (history-driven) and also edit a file.
yield* git(tmp, ["checkout", "feature"]);
yield* writeTextFile(NodePath.join(tmp, "edited.ts"), "export const edited = true;\n");
yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef });
const attribution = yield* checkpointStore.attributeCheckpointDiff({
cwd: tmp,
fromCheckpointRef,
toCheckpointRef,
});
expect(attribution).not.toBe(null);
expect(attribution!.get("feature.ts")).toBe("git");
expect(attribution!.get("edited.ts")).toBe("agent");
}),
);
it.effect("keeps files committed during the turn as agent work", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
const checkpointStore = yield* CheckpointStore.CheckpointStore;
const threadId = ThreadId.make("thread-attr-own-commit");
const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef });
// The "turn": edit and commit — HEAD moves, but by a turn-authored commit.
yield* writeTextFile(NodePath.join(tmp, "committed.ts"), "export const done = true;\n");
yield* git(tmp, ["add", "."]);
yield* git(tmp, ["commit", "-m", "agent commit"]);
yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef });
const attribution = yield* checkpointStore.attributeCheckpointDiff({
cwd: tmp,
fromCheckpointRef,
toCheckpointRef,
});
expect(attribution).not.toBe(null);
expect(attribution!.get("committed.ts")).toBe("agent");
}),
);
it.effect("attributes a cherry-pick of a pre-existing commit to git", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
const defaultBranch = yield* git(tmp, ["rev-parse", "--abbrev-ref", "HEAD"]);
// Pre-existing commit on another branch, authored before the turn.
yield* git(tmp, ["checkout", "-b", "source"]);
yield* writeTextFile(NodePath.join(tmp, "picked.ts"), "export const picked = 1;\n");
yield* git(tmp, ["add", "."]);
yield* git(
tmp,
[
"-c",
"user.email=other@test.com",
"-c",
"user.name=Other",
"commit",
"-m",
"source work",
],
PRE_EXISTING_COMMIT_ENV,
);
const pickedOid = yield* git(tmp, ["rev-parse", "HEAD"]);
yield* git(tmp, ["checkout", defaultBranch]);
const checkpointStore = yield* CheckpointStore.CheckpointStore;
const threadId = ThreadId.make("thread-attr-cherry-pick");
const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef });
yield* git(tmp, ["cherry-pick", pickedOid]);
yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef });
const attribution = yield* checkpointStore.attributeCheckpointDiff({
cwd: tmp,
fromCheckpointRef,
toCheckpointRef,
});
expect(attribution).not.toBe(null);
expect(attribution!.get("picked.ts")).toBe("git");
}),
);
it.effect("keeps an amend of a pre-turn commit as agent work", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
// Pre-turn commit with an old author date, as amend preserves it.
yield* writeTextFile(NodePath.join(tmp, "amended.ts"), "export const value = 1;\n");
yield* git(tmp, ["add", "."]);
yield* git(tmp, ["commit", "-m", "pre-turn work", "--date", "2020-01-01T00:00:00Z"]);
const checkpointStore = yield* CheckpointStore.CheckpointStore;
const threadId = ThreadId.make("thread-attr-amend");
const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef });
// The "turn": edit the file and amend it into the pre-turn commit.
// The author date stays 2020, but the content is new agent work.
yield* writeTextFile(NodePath.join(tmp, "amended.ts"), "export const value = 2;\n");
yield* git(tmp, ["add", "."]);
yield* git(tmp, ["commit", "--amend", "--no-edit"]);
yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef });
const attribution = yield* checkpointStore.attributeCheckpointDiff({
cwd: tmp,
fromCheckpointRef,
toCheckpointRef,
});
expect(attribution).not.toBe(null);
expect(attribution!.get("amended.ts")).toBe("agent");
}),
);
it.effect("keeps a whitespace-only amend of a pre-turn commit as agent work", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
// Pre-turn commit; its pre-amend state stays reachable via reflog,
// which is exactly the patch-id collision candidate.
yield* writeTextFile(NodePath.join(tmp, "style.ts"), "export const x = 1;\n");
yield* git(tmp, ["add", "."]);
yield* git(tmp, ["commit", "-m", "pre-turn work"], PRE_EXISTING_COMMIT_ENV);
const checkpointStore = yield* CheckpointStore.CheckpointStore;
const threadId = ThreadId.make("thread-attr-ws-amend");
const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef });
// The "turn": whitespace-only reformat amended into the pre-turn
// commit. `git patch-id --stable` would hash this identically to the
// original (it strips whitespace); --verbatim must not.
yield* writeTextFile(NodePath.join(tmp, "style.ts"), "export const x = 1;\n");
yield* git(tmp, ["add", "."]);
yield* git(tmp, ["commit", "--amend", "--no-edit"]);
yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef });
const attribution = yield* checkpointStore.attributeCheckpointDiff({
cwd: tmp,
fromCheckpointRef,
toCheckpointRef,
});
expect(attribution).not.toBe(null);
expect(attribution!.get("style.ts")).toBe("agent");
}),
);
it.effect("returns null for legacy checkpoints without head metadata", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
const checkpointStore = yield* CheckpointStore.CheckpointStore;
const threadId = ThreadId.make("thread-attr-legacy");
const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
// Simulate a pre-attribution checkpoint: commit the tree with the old
// single-line message format.
const treeOid = yield* git(tmp, ["write-tree"]);
const legacyCommit = yield* git(tmp, [
"commit-tree",
treeOid,
"-m",
`t3 checkpoint ref=${fromCheckpointRef}`,
]);
yield* git(tmp, ["update-ref", fromCheckpointRef, legacyCommit]);
yield* writeTextFile(NodePath.join(tmp, "edited.ts"), "export const edited = true;\n");
yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef });
const attribution = yield* checkpointStore.attributeCheckpointDiff({
cwd: tmp,
fromCheckpointRef,
toCheckpointRef,
});
expect(attribution).toBe(null);
}),
);
});
});