1import { assert } from "@effect/vitest";
2import type { ProviderReplayTranscript } from "@t3tools/contracts";
3
4import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
5import {
6 assertAssistantTextIncludes,
7 assertBaseProjection,
8 assertExecutionNodeKinds,
9 assertNoExtraAppRunsForProviderChildren,
10 assertRunProviderTurnCardinality,
11 assertSemanticProjectionIntegrity,
12 assertTurnItemTypes,
13 assertUserMessagesInclude,
14 projectionFor,
15 SUBAGENT_PROMPT,
16} from "../shared.ts";
17
18export function assertCursorSubagentOutput(
19 result: OrchestratorV2ScenarioResult,
20 transcript: ProviderReplayTranscript,
21) {
22 assertBaseProjection({
23 result,
24 transcript,
25 runCount: 1,
26 runStatuses: ["completed"],
27 });
28
29 const projection = projectionFor(result, transcript.scenario);
30 assertSemanticProjectionIntegrity(projection);
31 assertExecutionNodeKinds(projection, ["root_turn", "subagent"]);
32 assertTurnItemTypes(projection, ["user_message", "subagent", "assistant_message"]);
33 assertRunProviderTurnCardinality({ projection, rootRunCount: 1 });
34 assertNoExtraAppRunsForProviderChildren({ projection, expectedAppRuns: 1 });
35 assertUserMessagesInclude(projection, [SUBAGENT_PROMPT]);
36 assertAssistantTextIncludes(projection, "cursor-read-only-fixture");
37 assertAssistantTextIncludes(projection, "ES2022");
38
39 const lifecycleItems = projection.turnItems.filter(
40 (item) =>
41 item.type === "reasoning" || item.type === "assistant_message" || item.type === "subagent",
42 );
43 assert.deepEqual(
44 lifecycleItems.map((item) => item.type),
45 ["reasoning", "assistant_message", "subagent", "subagent", "reasoning", "assistant_message"],
46 "Cursor progress, subagents, and the final response must retain provider order",
47 );
48 const assistantMessages = lifecycleItems.filter((item) => item.type === "assistant_message");
49 assert.lengthOf(assistantMessages, 2);
50 assert.equal(
51 assistantMessages[0]?.text,
52 "Spawning two subagents in parallel to read `package.json` and `tsconfig.json`.\n",
53 );
54 assert.include(assistantMessages[1]?.text ?? "", "Both subagents finished.");
55
56 assert.lengthOf(projection.subagents, 2);
57 assert.lengthOf(result.shellSnapshot.threads, 3);
58 assert.deepEqual(
59 projection.subagents.map((subagent) => subagent.status),
60 ["completed", "completed"],
61 );
62
63 for (const subagent of projection.subagents) {
64 assert.equal(subagent.origin, "provider_native");
65 assert.equal(subagent.createdBy, "agent");
66 assert.equal(subagent.driver, "cursor");
67 assert.isNull(subagent.providerThreadId);
68 assert.isNotNull(subagent.childThreadId);
69 assert.isNotNull(subagent.nativeTaskRef);
70 assert.isNotNull(subagent.completedAt);
71 assert.isNotNull(subagent.result);
72 if (subagent.childThreadId === null) {
73 throw new Error(`Subagent ${subagent.id} is missing its child thread`);
74 }
75
76 const childProjection = result.projections.get(subagent.childThreadId);
77 assert.isDefined(childProjection);
78 assert.equal(childProjection.thread.lineage.parentThreadId, projection.thread.id);
79 assert.equal(childProjection.thread.lineage.relationshipToParent, "subagent");
80 assert.isNull(childProjection.thread.activeProviderThreadId);
81 assert.lengthOf(childProjection.runs, 0);
82 assert.lengthOf(childProjection.providerThreads, 0);
83 assert.lengthOf(childProjection.providerTurns, 0);
84 assertExecutionNodeKinds(childProjection, ["root_turn", "tool_call"]);
85 assertTurnItemTypes(childProjection, ["user_message", "dynamic_tool", "assistant_message"]);
86 assertUserMessagesInclude(childProjection, [subagent.prompt]);
87 assert.isTrue(
88 childProjection.turnItems.some(
89 (item) =>
90 item.type === "assistant_message" &&
91 subagent.result !== null &&
92 item.text.includes(subagent.result.slice(0, 40)),
93 ),
94 );
95 }
96}