1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodePath from "node:path";
3
4import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
5import * as NodeServices from "@effect/platform-node/NodeServices";
6import { it } from "@effect/vitest";
7import { CheckpointRef, ThreadId, type VcsError } from "@t3tools/contracts";
8import * as HostProcess from "@t3tools/shared/HostProcess";
9import * as Effect from "effect/Effect";
10import * as FileSystem from "effect/FileSystem";
11import * as Layer from "effect/Layer";
12import * as PlatformError from "effect/PlatformError";
13import * as Scope from "effect/Scope";
14import { describe, expect } from "vite-plus/test";
15
16import * as Base64Url from "effect/encoding/Base64Url";
17import { parseTurnDiffFilesFromNumstat } from "./Diffs.ts";
18import * as CheckpointStore from "./CheckpointStore.ts";
19import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
20import * as VcsProcess from "../vcs/VcsProcess.ts";
21import * as ServerConfig from "../config.ts";
22
23function checkpointRefForThreadTurn(threadId: ThreadId, turnCount: number): CheckpointRef {
24 return CheckpointRef.make(`refs/t3/checkpoints/${Base64Url.encode(threadId)}/turn/${turnCount}`);
25}
26
27const layerServerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), {
28 prefix: "t3-checkpoint-store-test-",
29});
30const layerVcsProcessTest = VcsProcess.layer.pipe(
31 Layer.provide(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)),
32);
33const layerVcsDriverTest = VcsDriverRegistry.layer.pipe(Layer.provide(layerVcsProcessTest));
34const layerCheckpointStoreTest = CheckpointStore.layer.pipe(
35 Layer.provideMerge(layerVcsDriverTest),
36 Layer.provideMerge(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)),
37);
38const layerTest = layerCheckpointStoreTest.pipe(
39 Layer.provideMerge(layerVcsProcessTest),
40 Layer.provideMerge(layerVcsDriverTest),
41 Layer.provideMerge(layerServerConfig),
42 Layer.provideMerge(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)),
43);
44
45function makeTmpDir(
46 prefix = "checkpoint-store-test-",
47): Effect.Effect<string, PlatformError.PlatformError, FileSystem.FileSystem | Scope.Scope> {
48 return Effect.gen(function* () {
49 const fileSystem = yield* FileSystem.FileSystem;
50 return yield* fileSystem.makeTempDirectoryScoped({ prefix });
51 });
52}
53
54function writeTextFile(
55 filePath: string,
56 contents: string,
57): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem> {
58 return Effect.gen(function* () {
59 const fileSystem = yield* FileSystem.FileSystem;
60 yield* fileSystem.writeFileString(filePath, contents);
61 });
62}
63
64function git(
65 cwd: string,
66 args: ReadonlyArray<string>,
67 options: { readonly committedAt?: string; readonly allowNonZeroExit?: boolean } = {},
68): Effect.Effect<string, VcsError, VcsProcess.VcsProcess> {
69 return Effect.gen(function* () {
70 const vcsProcess = yield* VcsProcess.VcsProcess;
71 const result = yield* vcsProcess.run({
72 operation: "CheckpointStore.test.git",
73 command: "git",
74 cwd,
75 args,
76 timeoutMs: 10_000,
77 ...(options.committedAt === undefined
78 ? {}
79 : { env: { ...process.env, GIT_COMMITTER_DATE: options.committedAt } }),
80 ...(options.allowNonZeroExit === undefined
81 ? {}
82 : { allowNonZeroExit: options.allowNonZeroExit }),
83 });
84 return result.stdout.trim();
85 });
86}
87
88function initRepoWithCommit(
89 cwd: string,
90): Effect.Effect<
91 void,
92 VcsError | PlatformError.PlatformError,
93 VcsProcess.VcsProcess | FileSystem.FileSystem
94> {
95 return Effect.gen(function* () {
96 yield* git(cwd, ["init"]);
97 yield* git(cwd, ["config", "user.email", "test@test.com"]);
98 yield* git(cwd, ["config", "user.name", "Test"]);
99 yield* writeTextFile(NodePath.join(cwd, "README.md"), "# test\n");
100 yield* git(cwd, ["add", "."]);
101 yield* git(cwd, ["commit", "-m", "initial commit"]);
102 });
103}
104
105function buildLargeText(lineCount = 5_000): string {
106 return Array.from({ length: lineCount }, (_, index) => `line ${String(index).padStart(5, "0")}`)
107 .join("\n")
108 .concat("\n");
109}
110
111it.layer(layerTest)("CheckpointStore.layer", (it) => {
112 it.effect("detects a nested workspace without its own .git entry", () =>
113 Effect.gen(function* () {
114 const tmp = yield* makeTmpDir();
115 yield* initRepoWithCommit(tmp);
116 const fileSystem = yield* FileSystem.FileSystem;
117 const nested = NodePath.join(tmp, "packages", "nested");
118 yield* fileSystem.makeDirectory(nested, { recursive: true });
119 const checkpointStore = yield* CheckpointStore.CheckpointStore;
120 expect(yield* checkpointStore.isGitRepository(nested)).toBe(true);
121 }),
122 );
123 it.effect("fails the repository probe that other callers read as no repository", () =>
124 Effect.gen(function* () {
125 const tmp = yield* makeTmpDir();
126 yield* git(tmp, ["init"]);
127 yield* writeTextFile(NodePath.join(tmp, ".git", "config"), "invalid config\n");
128 const registry = yield* VcsDriverRegistry.VcsDriverRegistry;
129 const checkpointStore = yield* CheckpointStore.CheckpointStore;
130
131 expect(yield* registry.detect({ cwd: tmp })).toBeNull();
132 const probe = yield* Effect.flip(checkpointStore.isGitRepository(tmp));
133 expect(probe._tag).toBe("VcsProcessExitError");
134 }),
135 );
136 describe("diffCheckpoints", () => {
137 it.effect("returns full oversized checkpoint diffs without truncation", () =>
138 Effect.gen(function* () {
139 const tmp = yield* makeTmpDir();
140 yield* initRepoWithCommit(tmp);
141 const checkpointStore = yield* CheckpointStore.CheckpointStore;
142 const threadId = ThreadId.make("thread-checkpoint-store");
143 const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
144 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
145
146 yield* checkpointStore.captureCheckpoint({
147 cwd: tmp,
148 checkpointRef: fromCheckpointRef,
149 });
150 yield* writeTextFile(NodePath.join(tmp, "README.md"), buildLargeText());
151 yield* checkpointStore.captureCheckpoint({
152 cwd: tmp,
153 checkpointRef: toCheckpointRef,
154 });
155
156 const diff = yield* checkpointStore.diffCheckpoints({
157 cwd: tmp,
158 fromCheckpointRef,
159 toCheckpointRef,
160 ignoreWhitespace: true,
161 });
162
163 expect(diff).toContain("diff --git");
164 expect(diff).not.toContain("[truncated]");
165 expect(diff).toContain("+line 04999");
166 }),
167 );
168
169 it.effect("keeps the patch format clients parse when the repository changes it", () =>
170 Effect.gen(function* () {
171 const tmp = yield* makeTmpDir();
172 yield* initRepoWithCommit(tmp);
173 yield* git(tmp, ["config", "diff.noprefix", "true"]);
174 yield* git(tmp, ["config", "diff.suppressBlankEmpty", "true"]);
175 yield* git(tmp, ["config", "diff.submodule", "log"]);
176 const sub = NodePath.join(tmp, "sub");
177 yield* git(tmp, ["init", "sub"]);
178 yield* initRepoWithCommit(sub);
179 const checkpointStore = yield* CheckpointStore.CheckpointStore;
180 const threadId = ThreadId.make("thread-checkpoint-store-noprefix");
181 const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
182 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
183
184 yield* writeTextFile(NodePath.join(tmp, "README.md"), "# test\n\nbefore\n");
185 yield* checkpointStore.captureCheckpoint({
186 cwd: tmp,
187 checkpointRef: fromCheckpointRef,
188 });
189 yield* writeTextFile(NodePath.join(tmp, "README.md"), "# test\n\nafter\n");
190 yield* git(sub, ["commit", "--allow-empty", "-m", "moved"]);
191 yield* checkpointStore.captureCheckpoint({
192 cwd: tmp,
193 checkpointRef: toCheckpointRef,
194 });
195
196 const diff = yield* checkpointStore.diffCheckpoints({
197 cwd: tmp,
198 fromCheckpointRef,
199 toCheckpointRef,
200 ignoreWhitespace: false,
201 });
202
203 expect(diff).toContain("diff --git a/README.md b/README.md");
204 expect(diff).toContain(" # test\n \n-before\n+after\n");
205 expect(diff).toContain("diff --git a/sub b/sub");
206 expect(diff).toContain("+Subproject commit ");
207 }),
208 );
209
210 it.effect("can hide indentation churn when changes wrap existing lines", () =>
211 Effect.gen(function* () {
212 const tmp = yield* makeTmpDir();
213 yield* initRepoWithCommit(tmp);
214 const checkpointStore = yield* CheckpointStore.CheckpointStore;
215 const threadId = ThreadId.make("thread-checkpoint-store-whitespace");
216 const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
217 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
218
219 const componentPath = NodePath.join(tmp, "Component.tsx");
220 yield* writeTextFile(
221 componentPath,
222 [
223 "export function View() {",
224 " return (",
225 " <section>",
226 " <h1>Title</h1>",
227 " <p>Body</p>",
228 " </section>",
229 " );",
230 "}",
231 "",
232 ].join("\n"),
233 );
234 yield* checkpointStore.captureCheckpoint({
235 cwd: tmp,
236 checkpointRef: fromCheckpointRef,
237 });
238 yield* writeTextFile(
239 componentPath,
240 [
241 "export function View() {",
242 " return (",
243 " <section>",
244 " {isReady ? (",
245 " <div>",
246 " <h1>Title</h1>",
247 " <p>Body</p>",
248 " </div>",
249 " ) : null}",
250 " </section>",
251 " );",
252 "}",
253 "",
254 ].join("\n"),
255 );
256 yield* checkpointStore.captureCheckpoint({
257 cwd: tmp,
258 checkpointRef: toCheckpointRef,
259 });
260
261 const normalDiff = yield* checkpointStore.diffCheckpoints({
262 cwd: tmp,
263 fromCheckpointRef,
264 toCheckpointRef,
265 ignoreWhitespace: false,
266 });
267 const whitespaceIgnoredDiff = yield* checkpointStore.diffCheckpoints({
268 cwd: tmp,
269 fromCheckpointRef,
270 toCheckpointRef,
271 ignoreWhitespace: true,
272 });
273
274 expect(normalDiff).toContain("diff --git");
275 expect(normalDiff).toContain("- <h1>Title</h1>");
276 expect(normalDiff).toContain("+ <h1>Title</h1>");
277 expect(whitespaceIgnoredDiff).toContain("diff --git");
278 expect(whitespaceIgnoredDiff).toContain("+ {isReady ? (");
279 expect(whitespaceIgnoredDiff).toContain("+ <div>");
280 expect(whitespaceIgnoredDiff).not.toContain("- <h1>Title</h1>");
281 expect(whitespaceIgnoredDiff).not.toContain("+ <h1>Title</h1>");
282
283 for (const ignoreWhitespace of [false, true]) {
284 const numstat = yield* checkpointStore.diffCheckpoints({
285 cwd: tmp,
286 fromCheckpointRef,
287 toCheckpointRef,
288 ignoreWhitespace,
289 format: "numstat",
290 });
291 expect(parseTurnDiffFilesFromNumstat(numstat)).toEqual([
292 {
293 path: "Component.tsx",
294 additions: ignoreWhitespace ? 4 : 6,
295 deletions: ignoreWhitespace ? 0 : 2,
296 },
297 ]);
298 }
299 }),
300 );
301 });
302
303 describe("checkpoint file summaries", () => {
304 it.effect("counts changes whose full patch exceeds the output limit", () =>
305 Effect.gen(function* () {
306 const tmp = yield* makeTmpDir();
307 yield* initRepoWithCommit(tmp);
308 const checkpointStore = yield* CheckpointStore.CheckpointStore;
309 const threadId = ThreadId.make("large-checkpoint-summary");
310 const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
311 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
312 const filePath = NodePath.join(tmp, "README.md");
313 const lineCount = 20_000;
314 yield* writeTextFile(filePath, `${"before".repeat(50)}\n`.repeat(lineCount));
315 yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: fromCheckpointRef });
316 yield* writeTextFile(filePath, `${"after".repeat(60)}\n`.repeat(lineCount));
317 yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef });
318
319 const numstat = yield* checkpointStore.diffCheckpoints({
320 cwd: tmp,
321 fromCheckpointRef,
322 toCheckpointRef,
323 ignoreWhitespace: false,
324 format: "numstat",
325 });
326
327 expect(parseTurnDiffFilesFromNumstat(numstat)).toEqual([
328 { path: "README.md", additions: lineCount, deletions: lineCount },
329 ]);
330 expect(numstat.length).toBeLessThan(100);
331 }),
332 );
333
334 it.effect("preserves file paths and turn ranges without changing the user index", () =>
335 Effect.gen(function* () {
336 const tmp = yield* makeTmpDir();
337 yield* initRepoWithCommit(tmp);
338 yield* git(tmp, ["config", "diff.renames", "copies"]);
339 const fileSystem = yield* FileSystem.FileSystem;
340 const checkpointStore = yield* CheckpointStore.CheckpointStore;
341 const threadId = ThreadId.make("checkpoint-summary-paths");
342 const baseline = checkpointRefForThreadTurn(threadId, 0);
343 const firstTurn = checkpointRefForThreadTurn(threadId, 1);
344 const secondTurn = checkpointRefForThreadTurn(threadId, 2);
345 const copiedText = Array.from({ length: 20 }, (_, index) => `copy line ${index}\n`).join(
346 "",
347 );
348 const platform = yield* HostProcess.Platform;
349 const renamedPath = platform === "win32" ? "renamed café.txt" : "renamed\tcafé\nname.txt";
350 const addedPath = platform === "win32" ? "new café.txt" : "new\tfile\n名.txt";
351 for (const [path, contents] of Object.entries({
352 "copy-source.txt": copiedText,
353 "deleted.txt": "delete me\n",
354 "rename-old.txt": "before\nkeep one\nkeep two\nkeep three\n",
355 "binary.bin": "\0before",
356 })) {
357 yield* writeTextFile(NodePath.join(tmp, path), contents);
358 }
359 yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: baseline });
360
361 yield* fileSystem.rename(
362 NodePath.join(tmp, "rename-old.txt"),
363 NodePath.join(tmp, renamedPath),
364 );
365 yield* fileSystem.remove(NodePath.join(tmp, "deleted.txt"));
366 for (const [path, contents] of Object.entries({
367 "copy-source.txt": `${copiedText}one more\n`,
368 "copied.txt": copiedText,
369 [renamedPath]: "after\nkeep one\nkeep two\nkeep three\n",
370 "binary.bin": "\0after",
371 "empty.txt": "",
372 [addedPath]: "first\nsecond\n",
373 })) {
374 yield* writeTextFile(NodePath.join(tmp, path), contents);
375 }
376 yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: firstTurn });
377 const userIndex = yield* fileSystem.readFile(NodePath.join(tmp, ".git/index"));
378 const input = {
379 cwd: tmp,
380 fromCheckpointRef: baseline,
381 toCheckpointRef: firstTurn,
382 ignoreWhitespace: false,
383 format: "numstat" as const,
384 };
385 const firstSummary = parseTurnDiffFilesFromNumstat(
386 yield* checkpointStore.diffCheckpoints(input),
387 );
388 const expectedFiles = [
389 { path: "binary.bin", additions: 0, deletions: 0 },
390 { path: "copied.txt", previousPath: "copy-source.txt", additions: 0, deletions: 0 },
391 { path: "copy-source.txt", additions: 1, deletions: 0 },
392 { path: "deleted.txt", additions: 0, deletions: 1 },
393 { path: "empty.txt", additions: 0, deletions: 0 },
394 { path: addedPath, additions: 2, deletions: 0 },
395 { path: renamedPath, previousPath: "rename-old.txt", additions: 1, deletions: 1 },
396 ].toSorted((left, right) => left.path.localeCompare(right.path));
397 expect(firstSummary).toEqual(expectedFiles);
398
399 yield* fileSystem.remove(NodePath.join(tmp, "empty.txt"));
400 yield* writeTextFile(NodePath.join(tmp, "copy-source.txt"), "replacement\n");
401 yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: secondTurn });
402 const secondSummary = parseTurnDiffFilesFromNumstat(
403 yield* checkpointStore.diffCheckpoints({
404 ...input,
405 fromCheckpointRef: firstTurn,
406 toCheckpointRef: secondTurn,
407 }),
408 );
409 expect(secondSummary).toEqual([
410 { path: "copy-source.txt", additions: 1, deletions: 21 },
411 { path: "empty.txt", additions: 0, deletions: 0 },
412 ]);
413
414 const inclusiveSummary = parseTurnDiffFilesFromNumstat(
415 yield* checkpointStore.diffCheckpoints({ ...input, toCheckpointRef: secondTurn }),
416 );
417 expect(inclusiveSummary).toEqual(
418 expectedFiles
419 .filter((file) => file.path !== "empty.txt")
420 .map((file) =>
421 file.path === "copy-source.txt" ? { ...file, additions: 1, deletions: 20 } : file,
422 ),
423 );
424 expect(
425 yield* checkpointStore.diffCheckpoints({ ...input, toCheckpointRef: baseline }),
426 ).toBe("");
427 expect(yield* fileSystem.readFile(NodePath.join(tmp, ".git/index"))).toEqual(userIndex);
428 }),
429 );
430
431 it.effect("uses HEAD for a missing baseline only when requested", () =>
432 Effect.gen(function* () {
433 const tmp = yield* makeTmpDir();
434 yield* initRepoWithCommit(tmp);
435 const checkpointStore = yield* CheckpointStore.CheckpointStore;
436 const threadId = ThreadId.make("checkpoint-summary-fallback");
437 const fromCheckpointRef = checkpointRefForThreadTurn(threadId, 0);
438 const toCheckpointRef = checkpointRefForThreadTurn(threadId, 1);
439 yield* writeTextFile(NodePath.join(tmp, "README.md"), "changed\n");
440 yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: toCheckpointRef });
441 const input = {
442 cwd: tmp,
443 fromCheckpointRef,
444 toCheckpointRef,
445 ignoreWhitespace: false,
446 format: "numstat" as const,
447 };
448
449 const error = yield* Effect.flip(checkpointStore.diffCheckpoints(input));
450 expect(error._tag).toBe("VcsProcessExitError");
451 const numstat = yield* checkpointStore.diffCheckpoints({
452 ...input,
453 fallbackFromToHead: true,
454 });
455 expect(parseTurnDiffFilesFromNumstat(numstat)).toEqual([
456 { path: "README.md", additions: 1, deletions: 1 },
457 ]);
458 }),
459 );
460 it.effect("reads repository paths from a subdirectory workspace under diff.relative", () =>
461 Effect.gen(function* () {
462 const tmp = yield* makeTmpDir();
463 yield* initRepoWithCommit(tmp);
464 yield* git(tmp, ["config", "diff.relative", "true"]);
465 const fileSystem = yield* FileSystem.FileSystem;
466 const cwd = NodePath.join(tmp, "app");
467 yield* fileSystem.makeDirectory(cwd);
468 const checkpointStore = yield* CheckpointStore.CheckpointStore;
469 const threadId = ThreadId.make("checkpoint-file-paths-subdirectory");
470 const refs = {
471 cwd,
472 fromCheckpointRef: checkpointRefForThreadTurn(threadId, 0),
473 toCheckpointRef: checkpointRefForThreadTurn(threadId, 1),
474 };
475 yield* checkpointStore.captureCheckpoint({ cwd, checkpointRef: refs.fromCheckpointRef });
476 yield* writeTextFile(NodePath.join(cwd, "kept.txt"), "kept\n");
477 yield* git(cwd, ["add", "kept.txt"]);
478 yield* git(cwd, ["commit", "-m", "kept"]);
479 yield* writeTextFile(NodePath.join(tmp, "README.md"), "# hidden\n");
480 yield* checkpointStore.captureCheckpoint({ cwd, checkpointRef: refs.toCheckpointRef });
481
482 const patch = yield* checkpointStore.diffCheckpoints({
483 ...refs,
484 ignoreWhitespace: false,
485 filePaths: ["app/kept.txt"],
486 });
487 expect(patch).toContain("+++ b/app/kept.txt");
488 expect(patch).toContain("+kept");
489 expect(patch).not.toContain("hidden");
490 expect(yield* checkpointStore.listAuthoredPaths(refs)).toEqual(new Set(["app/kept.txt"]));
491 }),
492 );
493 });
494
495 describe("listAuthoredPaths", () => {
496 it.effect("leaves out paths that only a merged upstream changed", () =>
497 Effect.gen(function* () {
498 const tmp = yield* makeTmpDir();
499 yield* initRepoWithCommit(tmp);
500 const checkpointStore = yield* CheckpointStore.CheckpointStore;
501 const threadId = ThreadId.make("checkpoint-authored-paths");
502 const refs = {
503 cwd: tmp,
504 fromCheckpointRef: checkpointRefForThreadTurn(threadId, 0),
505 toCheckpointRef: checkpointRefForThreadTurn(threadId, 1),
506 };
507 const commitBeforeTurn = (file: string, contents: string) =>
508 Effect.gen(function* () {
509 yield* writeTextFile(NodePath.join(tmp, file), contents);
510 yield* git(tmp, ["add", "."]);
511 yield* git(tmp, ["commit", "-m", file], { committedAt: "2020-01-01T00:00:00Z" });
512 });
513
514 yield* git(tmp, ["checkout", "-b", "upstream"]);
515 yield* commitBeforeTurn("upstream.txt", "upstream\n");
516 yield* commitBeforeTurn("shared.txt", "upstream\n");
517 yield* git(tmp, ["checkout", "-"]);
518 yield* commitBeforeTurn("shared.txt", "ours\n");
519 yield* checkpointStore.captureCheckpoint({
520 cwd: tmp,
521 checkpointRef: refs.fromCheckpointRef,
522 });
523
524 yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: refs.toCheckpointRef });
525 expect(yield* checkpointStore.listAuthoredPaths(refs)).toBeNull();
526
527 yield* git(tmp, ["merge", "upstream", "--no-edit"], { allowNonZeroExit: true });
528 yield* writeTextFile(NodePath.join(tmp, "shared.txt"), "upstream\n");
529 yield* git(tmp, ["commit", "-am", "merge upstream"]);
530 yield* writeTextFile(NodePath.join(tmp, "README.md"), "# edited\n");
531 yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: refs.toCheckpointRef });
532
533 const numstat = yield* checkpointStore.diffCheckpoints({
534 ...refs,
535 ignoreWhitespace: false,
536 format: "numstat",
537 });
538 expect(parseTurnDiffFilesFromNumstat(numstat).map((file) => file.path)).toEqual([
539 "README.md",
540 "shared.txt",
541 "upstream.txt",
542 ]);
543 expect(yield* checkpointStore.listAuthoredPaths(refs)).toEqual(
544 new Set(["README.md", "shared.txt"]),
545 );
546 }),
547 );
548
549 it.effect("keeps paths from signed commits under log.showSignature", () =>
550 Effect.gen(function* () {
551 const tmp = yield* makeTmpDir();
552 yield* initRepoWithCommit(tmp);
553 const signingKey = NodePath.join(yield* makeTmpDir("checkpoint-signing-key-"), "key");
554 yield* (yield* VcsProcess.VcsProcess).run({
555 operation: "CheckpointStore.test.sshKeygen",
556 command: "ssh-keygen",
557 cwd: tmp,
558 args: ["-q", "-t", "ed25519", "-N", "", "-f", signingKey],
559 timeoutMs: 10_000,
560 });
561 yield* git(tmp, ["config", "gpg.format", "ssh"]);
562 yield* git(tmp, ["config", "user.signingkey", `${signingKey}.pub`]);
563 yield* git(tmp, ["config", "log.showSignature", "true"]);
564 const checkpointStore = yield* CheckpointStore.CheckpointStore;
565 const threadId = ThreadId.make("checkpoint-authored-paths-signed");
566 const refs = {
567 cwd: tmp,
568 fromCheckpointRef: checkpointRefForThreadTurn(threadId, 0),
569 toCheckpointRef: checkpointRefForThreadTurn(threadId, 1),
570 };
571 yield* checkpointStore.captureCheckpoint({
572 cwd: tmp,
573 checkpointRef: refs.fromCheckpointRef,
574 });
575 yield* writeTextFile(NodePath.join(tmp, "kept.txt"), "kept\n");
576 yield* git(tmp, ["add", "kept.txt"]);
577 yield* git(tmp, ["commit", "-S", "-m", "kept"]);
578 yield* checkpointStore.captureCheckpoint({ cwd: tmp, checkpointRef: refs.toCheckpointRef });
579
580 expect(yield* checkpointStore.listAuthoredPaths(refs)).toEqual(new Set(["kept.txt"]));
581 }),
582 );
583 });
584});