1import * as ScriptProgress from "@t3tools/provider-core/server/ScriptProgress";
2import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
3import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import { assert, it } from "@effect/vitest";
6import {
7 EnvironmentId,
8 CommandId,
9 MessageId,
10 ProjectId,
11 ProviderDriverKind,
12 ProviderInstanceId,
13 ThreadId,
14 type OrchestrationV2ThreadProjection,
15} from "@t3tools/contracts";
16import * as Console from "effect/Console";
17import * as Effect from "effect/Effect";
18import * as Layer from "effect/Layer";
19import * as ResetCreditCoordinator from "../provider/resetCreditCoordinator.ts";
20import { FetchHttpClient } from "effect/http";
21import { describe } from "vite-plus/test";
22
23import * as CheckpointStore from "../checkpointing/CheckpointStore.ts";
24import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
25import * as HostPowerMonitor from "../background/HostPowerMonitor.ts";
26import * as ServerConfig from "../config.ts";
27import * as SqlitePersistence from "../persistence/Sqlite.ts";
28import * as AntigravityInstallation from "../provider/AntigravityInstallation.ts";
29import * as CodexInstallation from "../provider/CodexInstallation.ts";
30import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
31import * as ModelManifest from "../provider/ModelManifest.ts";
32import * as ProviderInstanceRegistryHydration from "../provider/ProviderInstanceRegistryHydration.ts";
33import * as ProviderLatestVersions from "@t3tools/provider-core/server/ProviderLatestVersions";
34import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
35import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
36import * as OpenCodeRuntime from "@t3tools/provider-opencode/server/OpenCodeRuntime";
37import * as OpenCodeServerLedger from "@t3tools/provider-opencode/server/OpenCodeServerLedger";
38import * as ServerSettings from "../serverSettings.ts";
39import * as VcsDriverRegistry from "../vcs/VcsDriverRegistry.ts";
40import * as VcsProcess from "../vcs/VcsProcess.ts";
41import * as Orchestrator from "./Orchestrator.ts";
42import * as ProviderTurnStartServiceTestkit from "./ProviderTurnStartService.testkit.ts";
43import * as EffectWorker from "./EffectWorker.ts";
44import * as RuntimeLayer from "./runtimeLayer.ts";
45import * as McpSessionRegistryTestkit from "../mcp/McpSessionRegistry.testkit.ts";
46import { CURSOR_MODEL_SELECTION, SUBAGENT_PROMPT } from "./testkit/fixtures/shared.ts";
47
48const layerPlatformTest = Layer.merge(
49 NodeServices.layer,
50 Layer.mock(SourceControlProviderRegistry.SourceControlProviderRegistry)({
51 resolveLink: () => Effect.die("unused title link"),
52 }),
53);
54
55const layerServerConfig = ServerConfig.layerTest(process.cwd(), {
56 prefix: "t3-cursor-v2-live-",
57});
58
59const layerVcsDriverRegistry = VcsDriverRegistry.layer.pipe(
60 Layer.provide(VcsProcess.layer),
61 Layer.provide(layerServerConfig),
62 Layer.provide(layerPlatformTest),
63);
64
65const layerCheckpointStore = CheckpointStore.layer.pipe(Layer.provide(layerVcsDriverRegistry));
66
67const layerServerSettings = ServerSettings.layerTest({
68 providerInstances: {
69 [ProviderInstanceId.make("cursor")]: {
70 driver: ProviderDriverKind.make("cursor"),
71 enabled: true,
72 },
73 },
74});
75const layerBackgroundPolicy = BackgroundPolicy.layer.pipe(
76 Layer.provide(Layer.effect(HostPowerMonitor.HostPowerMonitor, HostPowerMonitor.make())),
77 Layer.provide(layerServerSettings),
78);
79const layerProviderInstanceRegistry = ProviderInstanceRegistryHydration.layer.pipe(
80 Layer.provide(
81 Layer.mergeAll(
82 layerServerConfig.pipe(Layer.provide(layerPlatformTest)),
83 layerServerSettings,
84 ServerSecretStore.layer.pipe(
85 Layer.provide(layerServerConfig),
86 Layer.provide(layerPlatformTest),
87 ),
88 NodeServices.layer,
89 FetchHttpClient.layer,
90 OpenCodeRuntime.layer.pipe(
91 Layer.provide(OpenCodeServerLedger.layerTest),
92 Layer.provide(layerPlatformTest),
93 ),
94 Layer.succeed(
95 ProviderEventLoggers.ProviderEventLoggers,
96 ProviderEventLoggers.NoOpProviderEventLoggers,
97 ),
98 ProviderLatestVersions.layer,
99 ModelManifest.layerTest,
100 AntigravityInstallation.AntigravityInstallation.layer.pipe(
101 Layer.provide(layerServerConfig.pipe(Layer.provide(layerPlatformTest))),
102 Layer.provide(FetchHttpClient.layer),
103 Layer.provide(layerPlatformTest),
104 ),
105 // The Codex driver now resolves managed ChatGPT installs; these runs never launch Codex.
106 Layer.mock(CodexInstallation.CodexInstallation)({
107 managedDirectory: "unused-managed-installation",
108 }),
109 Layer.succeed(ServerEnvironment.ServerEnvironmentIdentity, {
110 getEnvironmentId: Effect.succeed(
111 EnvironmentId.make("00000000-0000-4000-8000-000000000001"),
112 ),
113 }),
114 ),
115 ),
116);
117
118const layerLive = RuntimeLayer.layer.pipe(
119 Layer.provide(ProviderTurnStartServiceTestkit.layer),
120 Layer.provide(McpSessionRegistryTestkit.layer),
121 Layer.provide(SqlitePersistence.layerMemory),
122 Layer.provide(layerCheckpointStore),
123 Layer.provide(layerServerConfig),
124 Layer.provide(layerServerSettings),
125 Layer.provide(layerProviderInstanceRegistry),
126 Layer.provide(ResetCreditCoordinator.layer),
127 Layer.provide(layerBackgroundPolicy),
128 Layer.provide(layerPlatformTest),
129 Layer.provide(Layer.merge(McpProviderSessions.layer, ScriptProgress.layerUnavailable)),
130);
131
132const waitForIdle = Effect.fn("CursorOrchestratorV2Live.waitForIdle")(function* (
133 threadId: ThreadId,
134) {
135 const orchestrator = yield* Orchestrator.OrchestratorV2;
136 for (let attempt = 0; attempt < 600; attempt += 1) {
137 const projection = yield* orchestrator.getThreadProjection(threadId);
138 if (
139 projection.runs.length > 0 &&
140 projection.runs.every(
141 (run) => !["queued", "starting", "running", "waiting"].includes(run.status),
142 )
143 ) {
144 return projection;
145 }
146 yield* Effect.sleep("500 millis");
147 }
148 return yield* Effect.die(new Error(`Timed out waiting for Cursor thread ${threadId}.`));
149});
150
151describe.runIf(process.env.T3_CURSOR_LIVE_ORCHESTRATOR === "1")(
152 "Cursor V2 live orchestrator",
153 () => {
154 it.live(
155 "forks through portable context using real Cursor agents",
156 () =>
157 Effect.gen(function* () {
158 yield* EffectWorker.runDaemonWithOptions({ concurrency: 2 }).pipe(Effect.forkScoped);
159 const orchestrator = yield* Orchestrator.OrchestratorV2;
160 const projectId = ProjectId.make("project:cursor-live-portable-fork");
161 const sourceThreadId = ThreadId.make("thread:cursor-live-portable-fork:source");
162 const targetThreadId = ThreadId.make("thread:cursor-live-portable-fork:target");
163 const marker = "CURSOR_LIVE_PORTABLE_FORK_7H3Q";
164
165 yield* orchestrator.dispatch({
166 type: "thread.create",
167 createdBy: "user",
168 creationSource: "web",
169 commandId: CommandId.make("command:cursor-live-portable-fork:create"),
170 threadId: sourceThreadId,
171 projectId,
172 title: "Cursor live portable fork source",
173 modelSelection: CURSOR_MODEL_SELECTION,
174 runtimeMode: "full-access",
175 interactionMode: "default",
176 branch: null,
177 worktreePath: process.cwd(),
178 });
179 yield* orchestrator.dispatch({
180 type: "message.dispatch",
181 createdBy: "user",
182 creationSource: "web",
183 commandId: CommandId.make("command:cursor-live-portable-fork:source"),
184 threadId: sourceThreadId,
185 messageId: MessageId.make("message:cursor-live-portable-fork:source"),
186 text: `Remember this opaque marker. Respond with exactly: ${marker}`,
187 attachments: [],
188 modelSelection: CURSOR_MODEL_SELECTION,
189 dispatchMode: { type: "start_immediately" },
190 });
191 const sourceProjection = yield* waitForIdle(sourceThreadId);
192 yield* Console.log("Cursor live source turn completed; dispatching portable fork.");
193
194 yield* orchestrator.dispatch({
195 type: "thread.fork",
196 createdBy: "user",
197 creationSource: "web",
198 commandId: CommandId.make("command:cursor-live-portable-fork:fork"),
199 sourceThreadId,
200 targetThreadId,
201 sourcePoint: { type: "latest_stable" },
202 title: "Cursor live portable fork target",
203 });
204 yield* orchestrator.dispatch({
205 type: "message.dispatch",
206 createdBy: "user",
207 creationSource: "web",
208 commandId: CommandId.make("command:cursor-live-portable-fork:target"),
209 threadId: targetThreadId,
210 messageId: MessageId.make("message:cursor-live-portable-fork:target"),
211 text: "Return the opaque marker from the transferred conversation. Respond with only the marker.",
212 attachments: [],
213 modelSelection: CURSOR_MODEL_SELECTION,
214 dispatchMode: { type: "start_immediately" },
215 });
216 const targetProjection = yield* waitForIdle(targetThreadId);
217 yield* Console.log("Cursor live portable fork target completed.");
218
219 const assistantText = (projection: OrchestrationV2ThreadProjection) =>
220 projection.messages
221 .filter((message) => message.role === "assistant")
222 .map((message) => message.text)
223 .join("\n");
224
225 assert.deepEqual(
226 sourceProjection.runs.map((run) => [run.providerInstanceId, run.status]),
227 [["cursor", "completed"]],
228 );
229 assert.deepEqual(
230 targetProjection.runs.map((run) => [run.providerInstanceId, run.status]),
231 [["cursor", "completed"]],
232 );
233 assert.deepEqual(
234 targetProjection.contextTransfers.map((transfer) => [
235 transfer.type,
236 transfer.status,
237 transfer.resolution?.strategy,
238 ]),
239 [["fork", "consumed", "portable_context"]],
240 );
241 assert.deepEqual(
242 targetProjection.contextHandoffs.map((handoff) => handoff.strategy),
243 ["full_thread_summary"],
244 );
245 assert.include(targetProjection.contextHandoffs[0]?.summaryText ?? "", marker);
246 assert.include(assistantText(targetProjection), marker);
247 }).pipe(Effect.provide(layerLive), Effect.scoped),
248 360_000,
249 );
250
251 it.live(
252 "runs a sandboxed thread after a full access thread in the same server",
253 () =>
254 Effect.gen(function* () {
255 yield* EffectWorker.runDaemonWithOptions({ concurrency: 2 }).pipe(Effect.forkScoped);
256 const orchestrator = yield* Orchestrator.OrchestratorV2;
257 const projectId = ProjectId.make("project:cursor-live-sandbox-after-full-access");
258
259 const runThread = Effect.fn("CursorOrchestratorV2Live.runThread")(function* (input: {
260 readonly name: string;
261 readonly runtimeMode: "full-access" | "approval-required";
262 }) {
263 const threadId = ThreadId.make(`thread:cursor-live-sandbox:${input.name}`);
264 yield* orchestrator.dispatch({
265 type: "thread.create",
266 createdBy: "user",
267 creationSource: "web",
268 commandId: CommandId.make(`command:cursor-live-sandbox:${input.name}:create`),
269 threadId,
270 projectId,
271 title: `Cursor live sandbox ${input.name}`,
272 modelSelection: CURSOR_MODEL_SELECTION,
273 runtimeMode: input.runtimeMode,
274 interactionMode: "default",
275 branch: null,
276 worktreePath: process.cwd(),
277 });
278 yield* orchestrator.dispatch({
279 type: "message.dispatch",
280 createdBy: "user",
281 creationSource: "web",
282 commandId: CommandId.make(`command:cursor-live-sandbox:${input.name}:message`),
283 threadId,
284 messageId: MessageId.make(`message:cursor-live-sandbox:${input.name}`),
285 text: "Respond with exactly: OK. Do not use any tools.",
286 attachments: [],
287 modelSelection: CURSOR_MODEL_SELECTION,
288 dispatchMode: { type: "start_immediately" },
289 });
290 return yield* waitForIdle(threadId);
291 });
292
293 // The SDK decides once per process whether local sandboxing works.
294 // The unsandboxed thread must run first to catch a wrong verdict.
295 const fullAccess = yield* runThread({ name: "full-access", runtimeMode: "full-access" });
296 const supervised = yield* runThread({
297 name: "supervised",
298 runtimeMode: "approval-required",
299 });
300
301 assert.deepEqual(
302 fullAccess.runs.map((run) => run.status),
303 ["completed"],
304 );
305 assert.deepEqual(
306 supervised.runs.map((run) => run.status),
307 ["completed"],
308 );
309 }).pipe(Effect.provide(layerLive), Effect.scoped),
310 360_000,
311 );
312
313 it.live(
314 "spawns native subagents with child thread lineage",
315 () =>
316 Effect.gen(function* () {
317 yield* EffectWorker.runDaemonWithOptions({ concurrency: 2 }).pipe(Effect.forkScoped);
318 const orchestrator = yield* Orchestrator.OrchestratorV2;
319 const projectId = ProjectId.make("project:cursor-live-subagent-lineage");
320 const sourceThreadId = ThreadId.make("thread:cursor-live-subagent-lineage");
321
322 yield* orchestrator.dispatch({
323 type: "thread.create",
324 createdBy: "user",
325 creationSource: "web",
326 commandId: CommandId.make("command:cursor-live-subagent-lineage:create"),
327 threadId: sourceThreadId,
328 projectId,
329 title: "Cursor live subagent lineage",
330 modelSelection: CURSOR_MODEL_SELECTION,
331 runtimeMode: "full-access",
332 interactionMode: "default",
333 branch: null,
334 worktreePath: process.cwd(),
335 });
336 yield* orchestrator.dispatch({
337 type: "message.dispatch",
338 createdBy: "user",
339 creationSource: "web",
340 commandId: CommandId.make("command:cursor-live-subagent-lineage:message"),
341 threadId: sourceThreadId,
342 messageId: MessageId.make("message:cursor-live-subagent-lineage"),
343 text: `${SUBAGENT_PROMPT}. Do not edit files. After both subagents finish, summarize each result briefly.`,
344 attachments: [],
345 modelSelection: CURSOR_MODEL_SELECTION,
346 dispatchMode: { type: "start_immediately" },
347 });
348
349 const parentProjection = yield* waitForIdle(sourceThreadId);
350 yield* Console.log(
351 `Cursor live subagent lineage completed with ${parentProjection.subagents.length} subagents.`,
352 );
353
354 assert.deepEqual(
355 parentProjection.runs.map((run) => [run.providerInstanceId, run.status]),
356 [["cursor", "completed"]],
357 );
358 assert.isAtLeast(parentProjection.subagents.length, 1);
359
360 for (const subagent of parentProjection.subagents) {
361 assert.equal(subagent.origin, "provider_native");
362 assert.equal(subagent.driver, "cursor");
363 assert.equal(subagent.status, "completed");
364 assert.isNotNull(subagent.childThreadId);
365 assert.isNotNull(subagent.result);
366
367 if (subagent.childThreadId === null) {
368 throw new Error(`Cursor live subagent ${subagent.id} is missing a child thread.`);
369 }
370
371 const childProjection = yield* orchestrator.getThreadProjection(subagent.childThreadId);
372 assert.equal(childProjection.thread.lineage.parentThreadId, parentProjection.thread.id);
373 assert.equal(childProjection.thread.lineage.relationshipToParent, "subagent");
374 assert.equal(
375 childProjection.thread.lineage.rootThreadId,
376 parentProjection.thread.lineage.rootThreadId,
377 );
378 assert.lengthOf(childProjection.runs, 0);
379 assert.isTrue(
380 childProjection.messages.some((message) => message.role === "assistant"),
381 `child thread ${subagent.childThreadId} should contain the subagent response`,
382 );
383 }
384 }).pipe(Effect.provide(layerLive), Effect.scoped),
385 360_000,
386 );
387 },
388);