1import { assert } from "@effect/vitest";
2import type { ProviderReplayTranscript } from "@t3tools/contracts";
3
4import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
5import {
6 assertAssistantTextIncludes,
7 assertBaseProjection,
8 assertNoExtraAppRunsForProviderChildren,
9 assertProviderNativeSubagentRootTurns,
10 assertSemanticProjectionIntegrity,
11 assertUserMessagesInclude,
12 OPENCODE2_SUBAGENT_PROMPT,
13 projectionFor,
14} from "../shared.ts";
15
16const CHILD_PROMPT = "List the files in the current directory and report their names.";
17
18/**
19 * A foreground `subagent` call: the child session is a subagent thread with
20 * its own runless turn, its tools and answer stay there, and the parent's
21 * turn waits for it before summarizing.
22 */
23export function assertOpenCode2SubagentOutput(
24 result: OrchestratorV2ScenarioResult,
25 transcript: ProviderReplayTranscript,
26) {
27 assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] });
28 const projection = projectionFor(result, transcript.scenario);
29 assertSemanticProjectionIntegrity(projection);
30 assertNoExtraAppRunsForProviderChildren({ projection, expectedAppRuns: 1 });
31 assertProviderNativeSubagentRootTurns(result);
32 assertUserMessagesInclude(projection, [OPENCODE2_SUBAGENT_PROMPT]);
33 assertAssistantTextIncludes(projection, "hello.txt");
34
35 assert.lengthOf(projection.subagents, 1);
36 const subagent = projection.subagents[0]!;
37 assert.deepInclude(subagent, {
38 origin: "provider_native",
39 status: "completed",
40 prompt: CHILD_PROMPT,
41 title: "List files in current directory",
42 runId: projection.runs[0]!.id,
43 });
44 assert.include(subagent.result ?? "", "hello.txt");
45 assert.notInclude(subagent.result ?? "", "<subagent", "the result is the answer, not the tag");
46 // The parent shows the call as its subagent item, not as a tool.
47 assert.deepEqual(
48 projection.turnItems.flatMap((item) => (item.type === "subagent" ? [item.status] : [])),
49 ["completed"],
50 );
51 assert.isFalse(
52 projection.turnItems.some(
53 (item) => item.type === "dynamic_tool" || item.type === "file_search",
54 ),
55 "the child's read and glob calls must not land on the parent",
56 );
57
58 const child = result.projections.get(subagent.childThreadId!);
59 assert.isDefined(child);
60 assert.equal(child.thread.lineage.relationshipToParent, "subagent");
61 assert.deepEqual(
62 child.providerTurns.map((turn) => turn.status),
63 ["completed"],
64 );
65 assertUserMessagesInclude(child, [CHILD_PROMPT]);
66 assert.isAtLeast(
67 child.turnItems.filter((item) => item.type === "dynamic_tool" || item.type === "file_search")
68 .length,
69 2,
70 );
71 assertAssistantTextIncludes(child, "hello.txt");
72 // The parent's usage is its own session's; it notes that a subagent ran.
73 assert.equal(projection.providerTurns[0]?.turnTokenUsage?.hasSubagents, true);
74}