466 lines · 18.2 KB · TypeScript
RawDownload
// @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);      }),    );  });});