1import { assert, it, vi } from "@effect/vitest";
2import {
3 CheckpointScopeId,
4 RunId,
5 ThreadId,
6 type OrchestrationV2ThreadShell,
7} from "@t3tools/contracts";
8import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";
10
11import * as PullRequestService from "../pullRequest/PullRequestService.ts";
12import * as VcsStatusBroadcaster from "../vcs/VcsStatusBroadcaster.ts";
13import * as WorkspaceEntries from "../workspace/WorkspaceEntries.ts";
14import * as CheckpointCapture from "./CheckpointCaptureService.ts";
15import * as ProjectionStore from "./ProjectionStore.ts";
16import * as RunFinalization from "./RunFinalizationService.ts";
17
18it.effect("refreshes workspace after checkpoint capture without reading history", () => {
19 const threadId = ThreadId.make("thread_finalize");
20 const runId = RunId.make("run_finalize");
21 const scopeId = CheckpointScopeId.make("scope_finalize");
22 const capture = vi.fn(() => Effect.void);
23 const refresh = vi.fn(() => Effect.void);
24 const checkpointContext = {
25 runs: [],
26 checkpointScopes: [{ id: scopeId, runId, kind: "root_run" as const, cwd: "/repo" }],
27 checkpoints: [],
28 };
29 const layer = RunFinalization.layer.pipe(
30 Layer.provide(
31 Layer.mergeAll(
32 Layer.mock(CheckpointCapture.CheckpointCaptureServiceV2)({ execute: capture }),
33 Layer.mock(ProjectionStore.ProjectionStoreV2)({
34 getThreadProjection: () =>
35 Effect.die("workspace refresh must not load transcript history"),
36 getCheckpointContext: () => Effect.succeed(checkpointContext),
37 }),
38 Layer.succeed(RunFinalization.RunFinalizationObserver, {
39 refresh,
40 refreshAfterTurn: () => Effect.void,
41 }),
42 ),
43 ),
44 );
45 return Effect.gen(function* () {
46 const service = yield* RunFinalization.RunFinalizationService;
47 yield* service.finalize({ threadId, runId, scopeId });
48 assert.equal(capture.mock.calls.length, 1);
49 assert.deepEqual(refresh.mock.calls[0], [{ cwd: "/repo", threadId, runId }]);
50 }).pipe(Effect.provide(layer));
51});
52
53it.effect.each(
54 (
55 [
56 {
57 label: "discovers a new PR for the completed run's branch",
58 branch: "feature",
59 checkedOut: "feature",
60 activeRun: null,
61 expected: ["/repo"],
62 },
63 {
64 label: "leaves the default branch's PR cache alone",
65 branch: "main",
66 checkedOut: "main",
67 activeRun: null,
68 expected: [],
69 },
70 {
71 label: "does not refresh another thread's checkout",
72 branch: "feature",
73 checkedOut: "other",
74 activeRun: null,
75 expected: [],
76 },
77 {
78 label: "does not refresh during a newer active run",
79 branch: "feature",
80 checkedOut: "feature",
81 activeRun: "newer-run",
82 expected: [],
83 },
84 ] as const
85 ).map((scenario) => [scenario.label, scenario] as const),
86)("%s", ([, scenario]) => {
87 const refreshed: string[] = [];
88 const threadId = ThreadId.make("thread-pr-refresh");
89 const runId = RunId.make("completed-run");
90 const layer = RunFinalization.layerObserver.pipe(
91 Layer.provide(
92 Layer.mergeAll(
93 Layer.mock(WorkspaceEntries.WorkspaceEntries)({ refresh: () => Effect.void }),
94 Layer.mock(PullRequestService.PullRequestService)({
95 refreshAfterTurn: () => Effect.void,
96 }),
97 Layer.mock(VcsStatusBroadcaster.VcsStatusBroadcaster)({
98 refreshLocalStatus: () =>
99 Effect.succeed({
100 isRepo: true,
101 hasPrimaryRemote: true,
102 isDefaultRef: scenario.checkedOut === "main",
103 refName: scenario.checkedOut,
104 hasWorkingTreeChanges: false,
105 workingTree: { files: [], insertions: 0, deletions: 0 },
106 }),
107 refreshStatus: () =>
108 Effect.die("turn completion must preserve known PRs and lookup backoff"),
109 refreshPullRequestStatus: (cwd) =>
110 Effect.sync(() => {
111 refreshed.push(cwd);
112 return null;
113 }),
114 }),
115 Layer.mock(ProjectionStore.ProjectionStoreV2)({
116 getThreadShell: () =>
117 Effect.succeed({
118 id: threadId,
119 branch: scenario.branch,
120 activeRunId: scenario.activeRun === null ? null : RunId.make(scenario.activeRun),
121 } as OrchestrationV2ThreadShell),
122 }),
123 ),
124 ),
125 );
126 return Effect.gen(function* () {
127 const observer = yield* RunFinalization.RunFinalizationObserver;
128 yield* observer.refresh({ cwd: "/repo", threadId, runId });
129 assert.deepEqual(refreshed, [...scenario.expected]);
130 }).pipe(Effect.provide(layer));
131});