717 lines · 28.9 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 { HostProcessPlatform } from "@t3tools/shared/hostProcess";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 { parseTurnDiffFilesFromNumstat } from "./Diffs.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>,): 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,      timeoutMs: 10_000,    });    return result.stdout.trim();  });} function gitAllowNonZero(cwd: string, args: ReadonlyArray<string>) {  return Effect.gen(function* () {    const process = yield* VcsProcess.VcsProcess;    return yield* process.run({      operation: "CheckpointStore.test.gitAllowNonZero",      command: "git",      cwd,      args,      timeoutMs: 10_000,      allowNonZeroExit: true,    });  });} function gitWithStdin(cwd: string, args: ReadonlyArray<string>, stdin: string) {  return Effect.gen(function* () {    const process = yield* VcsProcess.VcsProcess;    const result = yield* process.run({      operation: "CheckpointStore.test.gitWithStdin",      command: "git",      cwd,      args,      stdin,      timeoutMs: 10_000,    });    return result.stdout.trim();  });} 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);      }),    );  });   it.effect("detects a nested workspace without its own .git entry", () =>    Effect.gen(function* () {      const tmp = yield* makeTmpDir();      yield* initRepoWithCommit(tmp);      const fileSystem = yield* FileSystem.FileSystem;      const nested = NodePath.join(tmp, "packages", "nested");      yield* fileSystem.makeDirectory(nested, { recursive: true });      const checkpointStore = yield* CheckpointStore.CheckpointStore;      expect(yield* checkpointStore.isGitRepository(nested)).toBe(true);    }),  );   describe("readWorkspaceFingerprint", () => {    it.effect("tracks the exact tracked and untracked tree affected by restore", () =>      Effect.gen(function* () {        const tmp = yield* makeTmpDir();        yield* initRepoWithCommit(tmp);        const checkpointStore = yield* CheckpointStore.CheckpointStore;         const initial = yield* checkpointStore.readWorkspaceFingerprint(tmp);        yield* writeTextFile(NodePath.join(tmp, "README.md"), "# changed\n");        const trackedChange = yield* checkpointStore.readWorkspaceFingerprint(tmp);        yield* writeTextFile(NodePath.join(tmp, "new-file.txt"), "new\n");        const untrackedChange = yield* checkpointStore.readWorkspaceFingerprint(tmp);         expect(trackedChange).not.toBe(initial);        expect(untrackedChange).not.toBe(trackedChange);        expect(yield* git(tmp, ["status", "--short"])).toContain("README.md");        expect(yield* git(tmp, ["status", "--short"])).toContain("new-file.txt");      }),    );     it.effect("tracks staged-only changes without modifying the user's index", () =>      Effect.gen(function* () {        const tmp = yield* makeTmpDir();        yield* initRepoWithCommit(tmp);        const checkpointStore = yield* CheckpointStore.CheckpointStore;        const readme = NodePath.join(tmp, "README.md");         const initial = yield* checkpointStore.readWorkspaceFingerprint(tmp);        yield* writeTextFile(readme, "# staged\n");        yield* git(tmp, ["add", "README.md"]);        yield* writeTextFile(readme, "# test\n");        const stagedBefore = yield* git(tmp, ["diff", "--cached", "--", "README.md"]);        const stagedOnly = yield* checkpointStore.readWorkspaceFingerprint(tmp);        const stagedAfter = yield* git(tmp, ["diff", "--cached", "--", "README.md"]);         expect(stagedOnly).not.toBe(initial);        expect(stagedAfter).toBe(stagedBefore);        expect(yield* git(tmp, ["diff", "--", "README.md"])).not.toBe("");         const oddPath = "odd\tname\n.txt";        yield* writeTextFile(NodePath.join(tmp, oddPath), "odd staged path\n");        yield* git(tmp, ["add", "--", oddPath]);        expect(yield* checkpointStore.readWorkspaceFingerprint(tmp)).not.toBe(stagedOnly);      }),    );     it.effect("fingerprints an index whose staged listing exceeds the process output cap", () =>      Effect.gen(function* () {        const tmp = yield* makeTmpDir();        yield* initRepoWithCommit(tmp);        const checkpointStore = yield* CheckpointStore.CheckpointStore;        const initial = yield* checkpointStore.readWorkspaceFingerprint(tmp);        const fileSystem = yield* FileSystem.FileSystem;         yield* Effect.forEach(          Array.from({ length: 4_000 }, (_, index) => index),          (index) =>            fileSystem.writeFileString(              NodePath.join(tmp, `${String(index).padStart(4, "0")}-${"x".repeat(220)}.txt`),              "staged\n",            ),          { concurrency: 64, discard: true },        );        yield* git(tmp, ["add", "."]);         expect(yield* checkpointStore.readWorkspaceFingerprint(tmp)).not.toBe(initial);      }),    );     it.effect("ignores unmerged index stages outside a nested restore cwd", () =>      Effect.gen(function* () {        const tmp = yield* makeTmpDir();        yield* initRepoWithCommit(tmp);        const nested = NodePath.join(tmp, "packages", "nested");        const sibling = NodePath.join(tmp, "sibling.txt");        const fileSystem = yield* FileSystem.FileSystem;        yield* fileSystem.makeDirectory(nested, { recursive: true });        yield* writeTextFile(NodePath.join(nested, "file.txt"), "nested\n");        yield* writeTextFile(sibling, "base\n");        yield* git(tmp, ["add", "."]);        yield* git(tmp, ["commit", "-m", "add scoped files"]);        const baseBranch = yield* git(tmp, ["branch", "--show-current"]);         yield* git(tmp, ["checkout", "-b", "conflict-side"]);        yield* writeTextFile(sibling, "side\n");        yield* git(tmp, ["commit", "-am", "change sibling on side"]);        yield* git(tmp, ["checkout", baseBranch]);        yield* writeTextFile(sibling, "main\n");        yield* git(tmp, ["commit", "-am", "change sibling on main"]);         const checkpointStore = yield* CheckpointStore.CheckpointStore;        const beforeConflict = yield* checkpointStore.readWorkspaceFingerprint(nested);        const merge = yield* gitAllowNonZero(tmp, ["merge", "conflict-side"]);        expect(merge.exitCode).not.toBe(0);        expect(yield* git(tmp, ["ls-files", "--unmerged", "--", "sibling.txt"])).not.toBe("");        expect(yield* checkpointStore.readWorkspaceFingerprint(nested)).toBe(beforeConflict);      }),    );     it.effect("changes when an in-scope conflict stage changes", () =>      Effect.gen(function* () {        const tmp = yield* makeTmpDir();        yield* initRepoWithCommit(tmp);        const nested = NodePath.join(tmp, "packages", "nested");        const nestedFile = NodePath.join(nested, "file.txt");        const replacement = NodePath.join(tmp, "replacement-stage.txt");        const fileSystem = yield* FileSystem.FileSystem;        yield* fileSystem.makeDirectory(nested, { recursive: true });        yield* writeTextFile(nestedFile, "base\n");        yield* git(tmp, ["add", "."]);        yield* git(tmp, ["commit", "-m", "add nested conflict file"]);        const baseBranch = yield* git(tmp, ["branch", "--show-current"]);         yield* git(tmp, ["checkout", "-b", "nested-conflict-side"]);        yield* writeTextFile(nestedFile, "side\n");        yield* git(tmp, ["commit", "-am", "change nested file on side"]);        yield* git(tmp, ["checkout", baseBranch]);        yield* writeTextFile(nestedFile, "main\n");        yield* git(tmp, ["commit", "-am", "change nested file on main"]);        expect((yield* gitAllowNonZero(tmp, ["merge", "nested-conflict-side"])).exitCode).not.toBe(          0,        );         const checkpointStore = yield* CheckpointStore.CheckpointStore;        const beforeStageChange = yield* checkpointStore.readWorkspaceFingerprint(nested);        const stages = (yield* git(tmp, ["ls-files", "--stage", "--", "packages/nested/file.txt"]))          .split("\n")          .filter((line) => line.length > 0);        expect(stages).toHaveLength(3);        expect(stages.map((line) => line.split(/\s+/, 3)[2])).toEqual(["1", "2", "3"]);         yield* writeTextFile(replacement, "replacement stage two\n");        const replacementOid = yield* git(tmp, ["hash-object", "-w", "--", replacement]);        yield* gitWithStdin(          tmp,          ["update-index", "--index-info"],          `100644 ${replacementOid} 2\tpackages/nested/file.txt\n`,        );         expect(yield* checkpointStore.readWorkspaceFingerprint(nested)).not.toBe(beforeStageChange);      }),    );     it.effect("limits staged-state fingerprints to a nested restore cwd", () =>      Effect.gen(function* () {        const tmp = yield* makeTmpDir();        yield* initRepoWithCommit(tmp);        const nested = NodePath.join(tmp, "packages", "nested");        const sibling = NodePath.join(tmp, "sibling.txt");        const nestedFile = NodePath.join(nested, "file.txt");        const fileSystem = yield* FileSystem.FileSystem;        yield* fileSystem.makeDirectory(nested, { recursive: true });        yield* writeTextFile(nestedFile, "nested\n");        yield* writeTextFile(sibling, "sibling\n");        yield* git(tmp, ["add", "."]);        yield* git(tmp, ["commit", "-m", "nested files"]);        const checkpointStore = yield* CheckpointStore.CheckpointStore;         const initial = yield* checkpointStore.readWorkspaceFingerprint(nested);        yield* writeTextFile(sibling, "sibling staged\n");        yield* git(tmp, ["add", "sibling.txt"]);        yield* writeTextFile(sibling, "sibling\n");        expect(yield* checkpointStore.readWorkspaceFingerprint(nested)).toBe(initial);         yield* git(tmp, ["commit", "-m", "change committed sibling"]);        expect(yield* checkpointStore.readWorkspaceFingerprint(nested)).toBe(initial);         yield* writeTextFile(nestedFile, "nested staged\n");        yield* git(tmp, ["add", "packages/nested/file.txt"]);        yield* writeTextFile(nestedFile, "nested\n");        expect(yield* checkpointStore.readWorkspaceFingerprint(nested)).not.toBe(initial);      }),    );     it.effect("fingerprints an absent nested worktree subtree", () =>      Effect.gen(function* () {        const tmp = yield* makeTmpDir();        yield* initRepoWithCommit(tmp);        const nested = NodePath.join(tmp, "packages", "nested");        const nestedFile = NodePath.join(nested, "file.txt");        const fileSystem = yield* FileSystem.FileSystem;        yield* fileSystem.makeDirectory(nested, { recursive: true });        yield* writeTextFile(nestedFile, "nested\n");        yield* git(tmp, ["add", "."]);        yield* git(tmp, ["commit", "-m", "add nested subtree"]);        const checkpointStore = yield* CheckpointStore.CheckpointStore;         const initial = yield* checkpointStore.readWorkspaceFingerprint(nested);        yield* fileSystem.remove(nestedFile);         expect(yield* checkpointStore.readWorkspaceFingerprint(nested)).not.toBe(initial);      }),    );     it.effect("fingerprints a nested cwd containing a tab", () =>      Effect.gen(function* () {        const tmp = yield* makeTmpDir();        yield* initRepoWithCommit(tmp);        const nested = NodePath.join(tmp, "packages", "nested\tworkspace");        const nestedFile = NodePath.join(nested, "file.txt");        const fileSystem = yield* FileSystem.FileSystem;        yield* fileSystem.makeDirectory(nested, { recursive: true });        yield* writeTextFile(nestedFile, "nested\n");        yield* git(tmp, ["add", "."]);        yield* git(tmp, ["commit", "-m", "add tabbed nested workspace"]);        const checkpointStore = yield* CheckpointStore.CheckpointStore;         const initial = yield* checkpointStore.readWorkspaceFingerprint(nested);        yield* writeTextFile(nestedFile, "changed\n");         expect(yield* checkpointStore.readWorkspaceFingerprint(nested)).not.toBe(initial);      }),    );  });   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("fails closed when a complete oversized checkpoint diff is required", () =>      Effect.gen(function* () {        const tmp = yield* makeTmpDir();        yield* initRepoWithCommit(tmp);        const checkpointStore = yield* CheckpointStore.CheckpointStore;        const threadId = ThreadId.make("thread-checkpoint-store-complete-output");        const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);        const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);         yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef });        yield* writeTextFile(NodePath.join(tmp, "large.txt"), "x".repeat(10_100_000));        yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef });         const error = yield* checkpointStore          .diffCheckpoints({            cwd: tmp,            fromCheckpointRef,            toCheckpointRef,            ignoreWhitespace: false,            requireCompleteOutput: true,          })          .pipe(Effect.flip);         expect(error._tag).toBe("VcsProcessOutputLimitError");      }),    );     it.effect("keeps a/ and b/ patch prefixes when the repository disables them", () =>      Effect.gen(function* () {        const tmp = yield* makeTmpDir();        yield* initRepoWithCommit(tmp);        yield* git(tmp, ["config", "diff.noprefix", "true"]);        const checkpointStore = yield* CheckpointStore.CheckpointStore;        const threadId = ThreadId.make("thread-checkpoint-store-noprefix");        const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);        const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);         yield* checkpointStore.captureCheckpoint({          cwd: tmp,          checkpointRef: fromCheckpointRef,        });        yield* writeTextFile(NodePath.join(tmp, "README.md"), "# changed\n");        yield* checkpointStore.captureCheckpoint({          cwd: tmp,          checkpointRef: toCheckpointRef,        });         const diff = yield* checkpointStore.diffCheckpoints({          cwd: tmp,          fromCheckpointRef,          toCheckpointRef,          ignoreWhitespace: false,        });         expect(diff).toContain("diff --git a/README.md b/README.md");      }),    );     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 ? (",

Lines 1–500 of 717.