| 1 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; |
| 2 | import { assert, it, vi } from "@effect/vitest"; |
| 3 | import { CheckpointRef, CheckpointScopeId, RunId, ThreadId } from "@t3tools/contracts"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; |
| 6 | |
| 7 | import { checkpointRefForScopeOrdinal } from "../orchestration-v2/CheckpointService.ts"; |
| 8 | import { OrchestratorProjectionError } from "../orchestration-v2/Orchestrator.ts"; |
| 9 | import type { ProjectionCheckpointContext } from "../orchestration-v2/ProjectionStore.ts"; |
| 10 | import * as ThreadManagement from "../orchestration-v2/ThreadManagementService.ts"; |
| 11 | import * as CheckpointDiffQuery from "./CheckpointDiffQuery.ts"; |
| 12 | import * as CheckpointStore from "./CheckpointStore.ts"; |
| 13 | import { |
| 14 | CheckpointRefUnavailableError, |
| 15 | CheckpointThreadNotFoundError, |
| 16 | CheckpointTurnRangeUnavailableError, |
| 17 | } from "./Errors.ts"; |
| 18 | |
| 19 | const threadId = ThreadId.make("thread:checkpoint-diff-v2"); |
| 20 | const firstRunId = RunId.make("run:checkpoint-diff-v2:1"); |
| 21 | const secondRunId = RunId.make("run:checkpoint-diff-v2:2"); |
| 22 | const firstScopeId = CheckpointScopeId.make("scope:checkpoint-diff-v2:1"); |
| 23 | const secondScopeId = CheckpointScopeId.make("scope:checkpoint-diff-v2:2"); |
| 24 | const secondRef = CheckpointRef.make("refs/t3/test/second"); |
| 25 | |
| 26 | function 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 | |
| 48 | function 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 | |
| 69 | it.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 | |
| 98 | it.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 | |
| 126 | it.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 | |
| 145 | it.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 | |
| 165 | it.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 | |
| 203 | it.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 | }); |