1import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
2import { assert, it, vi } from "@effect/vitest";
3import { CheckpointRef, CheckpointScopeId, RunId, ThreadId } from "@t3tools/contracts";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6
7import { checkpointRefForScopeOrdinal } from "../orchestration-v2/CheckpointService.ts";
8import { OrchestratorProjectionError } from "../orchestration-v2/Orchestrator.ts";
9import type { ProjectionCheckpointContext } from "../orchestration-v2/ProjectionStore.ts";
10import * as ThreadManagement from "../orchestration-v2/ThreadManagementService.ts";
11import * as CheckpointDiffQuery from "./CheckpointDiffQuery.ts";
12import * as CheckpointStore from "./CheckpointStore.ts";
13import {
14 CheckpointRefUnavailableError,
15 CheckpointThreadNotFoundError,
16 CheckpointTurnRangeUnavailableError,
17} from "./Errors.ts";
18
19const threadId = ThreadId.make("thread:checkpoint-diff-v2");
20const firstRunId = RunId.make("run:checkpoint-diff-v2:1");
21const secondRunId = RunId.make("run:checkpoint-diff-v2:2");
22const firstScopeId = CheckpointScopeId.make("scope:checkpoint-diff-v2:1");
23const secondScopeId = CheckpointScopeId.make("scope:checkpoint-diff-v2:2");
24const secondRef = CheckpointRef.make("refs/t3/test/second");
25
26function makeProjection(): ProjectionCheckpointContext {
27 return {
28 runs: [
29 { id: firstRunId, ordinal: 1, status: "completed" },
30 { id: secondRunId, ordinal: 2, status: "completed" },
31 ],
32 checkpointScopes: [
33 { id: firstScopeId, runId: firstRunId, kind: "root_run", cwd: "/repo" },
34 { id: secondScopeId, runId: secondRunId, kind: "root_run", cwd: "/repo" },
35 ],
36 checkpoints: [
37 {
38 scopeId: secondScopeId,
39 runId: secondRunId,
40 appRunOrdinal: 2,
41 status: "ready",
42 ref: secondRef,
43 },
44 ],
45 };
46}
47
48function layerFor(input: {
49 readonly projection: Effect.Effect<ProjectionCheckpointContext, OrchestratorProjectionError>;
50 readonly diffCheckpoints?: CheckpointStore.CheckpointStore["Service"]["diffCheckpoints"];
51 readonly authoredPaths?: ReadonlySet<string>;
52}) {
53 return CheckpointDiffQuery.layer.pipe(
54 Layer.provide(
55 Layer.mergeAll(
56 Layer.mock(ThreadManagement.ThreadManagementService)({
57 getCheckpointContext: () => input.projection,
58 }),
59 Layer.mock(CheckpointStore.CheckpointStore)({
60 diffCheckpoints: input.diffCheckpoints ?? (() => Effect.succeed("diff")),
61 listAuthoredPaths: () => Effect.succeed(input.authoredPaths ?? null),
62 }),
63 ),
64 ),
65 Layer.provideMerge(NodeCrypto.layer),
66 );
67}
68
69it.effect("computes V2 run diffs from projected checkpoint scopes", () => {
70 const diffCheckpoints = vi.fn((_input: CheckpointStore.DiffCheckpointsInput) =>
71 Effect.succeed("diff --git a/file b/file"),
72 );
73 const layer = layerFor({ projection: Effect.succeed(makeProjection()), diffCheckpoints });
74
75 return Effect.gen(function* () {
76 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;
77 const result = yield* query.getFullThreadDiff({ threadId, toTurnCount: 2 });
78
79 assert.deepEqual(result, {
80 threadId,
81 fromTurnCount: 0,
82 toTurnCount: 2,
83 diff: "diff --git a/file b/file",
84 });
85 assert.deepEqual(diffCheckpoints.mock.calls[0]?.[0], {
86 cwd: "/repo",
87 fromCheckpointRef: yield* checkpointRefForScopeOrdinal({
88 scopeId: firstScopeId,
89 ordinalWithinScope: 0,
90 }),
91 toCheckpointRef: secondRef,
92 fallbackFromToHead: false,
93 ignoreWhitespace: true,
94 });
95 }).pipe(Effect.provide(layer));
96});
97
98it.effect("leaves Git imports out of large turn diffs", () => {
99 const ownPaths = Array.from({ length: 1_000 }, (_, index) => `src/module-${index}/file.ts`);
100 const diffCheckpoints = vi.fn((input: CheckpointStore.DiffCheckpointsInput) =>
101 Effect.succeed(
102 input.format === "numstat"
103 ? [...ownPaths, "upstream.ts"].map((path) => `1\t0\t${path}\0`).join("")
104 : (input.filePaths ?? ["all"]).join("\n"),
105 ),
106 );
107 const layer = layerFor({
108 projection: Effect.succeed(makeProjection()),
109 diffCheckpoints,
110 authoredPaths: new Set(ownPaths),
111 });
112
113 return Effect.gen(function* () {
114 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;
115 yield* query.getFullThreadDiff({ threadId, toTurnCount: 2 });
116
117 const patchPaths = diffCheckpoints.mock.calls
118 .map(([call]) => call)
119 .filter((call) => call.format !== "numstat")
120 .map((call) => call.filePaths ?? ["all"]);
121 assert.isAbove(patchPaths.length, 1);
122 assert.sameMembers(patchPaths.flat(), ownPaths);
123 }).pipe(Effect.provide(layer));
124});
125
126it.effect("preserves the typed missing-thread error contract", () => {
127 const layer = layerFor({
128 projection: Effect.fail(new OrchestratorProjectionError({ threadId })),
129 });
130
131 return Effect.gen(function* () {
132 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;
133 const error = yield* query
134 .getTurnDiff({ threadId, fromTurnCount: 0, toTurnCount: 1 })
135 .pipe(Effect.flip);
136
137 assert.instanceOf(error, CheckpointThreadNotFoundError);
138 assert.deepEqual(
139 { operation: error.operation, threadId: error.threadId },
140 { operation: "CheckpointDiffQuery.getTurnDiff", threadId },
141 );
142 }).pipe(Effect.provide(layer));
143});
144
145it.effect("preserves the typed unavailable-range error contract", () => {
146 const layer = layerFor({ projection: Effect.succeed(makeProjection()) });
147
148 return Effect.gen(function* () {
149 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;
150 const error = yield* query
151 .getTurnDiff({ threadId, fromTurnCount: 0, toTurnCount: 3 })
152 .pipe(Effect.flip);
153
154 assert.instanceOf(error, CheckpointTurnRangeUnavailableError);
155 assert.deepEqual(
156 {
157 requestedTurnCount: error.requestedTurnCount,
158 availableTurnCount: error.availableTurnCount,
159 },
160 { requestedTurnCount: 3, availableTurnCount: 2 },
161 );
162 }).pipe(Effect.provide(layer));
163});
164
165it.effect("excludes ready checkpoints from rolled-back runs", () => {
166 const projection = makeProjection();
167 const layer = layerFor({
168 projection: Effect.succeed({
169 ...projection,
170 runs: projection.runs.map((run) =>
171 run.id === secondRunId ? { ...run, status: "rolled_back" as const } : run,
172 ),
173 checkpoints: [
174 {
175 ...projection.checkpoints[0]!,
176 scopeId: firstScopeId,
177 runId: firstRunId,
178 appRunOrdinal: 1,
179 ref: CheckpointRef.make("refs/t3/test/first"),
180 },
181 ...projection.checkpoints,
182 ],
183 }),
184 });
185
186 return Effect.gen(function* () {
187 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;
188 const error = yield* query
189 .getTurnDiff({ threadId, fromTurnCount: 0, toTurnCount: 2 })
190 .pipe(Effect.flip);
191
192 assert.instanceOf(error, CheckpointTurnRangeUnavailableError);
193 assert.deepEqual(
194 {
195 requestedTurnCount: error.requestedTurnCount,
196 availableTurnCount: error.availableTurnCount,
197 },
198 { requestedTurnCount: 2, availableTurnCount: 1 },
199 );
200 }).pipe(Effect.provide(layer));
201});
202
203it.effect("preserves the typed missing-baseline-ref error contract", () => {
204 const projection = makeProjection();
205 const layer = layerFor({
206 projection: Effect.succeed({
207 ...projection,
208 checkpointScopes: projection.checkpointScopes.map((scope) => ({
209 ...scope,
210 kind: "tool" as const,
211 })),
212 }),
213 });
214
215 return Effect.gen(function* () {
216 const query = yield* CheckpointDiffQuery.CheckpointDiffQuery;
217 const error = yield* query
218 .getTurnDiff({ threadId, fromTurnCount: 0, toTurnCount: 2 })
219 .pipe(Effect.flip);
220
221 assert.instanceOf(error, CheckpointRefUnavailableError);
222 assert.deepEqual(
223 { checkpoint: error.checkpoint, turnCount: error.turnCount },
224 { checkpoint: "from", turnCount: 0 },
225 );
226 }).pipe(Effect.provide(layer));
227});