1import { assert } from "@effect/vitest";
2import type { OrchestrationV2ThreadProjection, ProviderReplayTranscript } from "@t3tools/contracts";
3
4import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
5import {
6 assertBaseProjection,
7 assertProviderNativeSubagentRootTurns,
8 assertSemanticProjectionIntegrity,
9 assertUserMessagesInclude,
10 OPENCODE2_NESTED_BACKGROUND_PROMPT,
11 projectionFor,
12} from "../shared.ts";
13
14const texts = (projection: OrchestrationV2ThreadProjection | undefined) =>
15 (projection?.turnItems ?? []).flatMap((item) =>
16 item.type === "assistant_message" ? [item.text.trim()] : [],
17 );
18
19/**
20 * A foreground subagent starts a background one and returns, so the
21 * thread's turn ends while that grandchild runs. Ending the foreground call
22 * leaves the grandchild running: the run waits on it, its report reaches
23 * the subagent that started it, and that subagent's answer is a second turn
24 * on its own thread. Only then does the run complete.
25 */
26export function assertOpenCode2NestedBackgroundOutput(
27 result: OrchestratorV2ScenarioResult,
28 transcript: ProviderReplayTranscript,
29) {
30 assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] });
31 const projection = projectionFor(result, transcript.scenario);
32 assertSemanticProjectionIntegrity(projection);
33 assertProviderNativeSubagentRootTurns(result);
34 assertUserMessagesInclude(projection, [OPENCODE2_NESTED_BACKGROUND_PROMPT]);
35 assert.deepEqual(texts(projection), ["ROOT_OK"]);
36
37 const [middle] = projection.subagents;
38 assert.lengthOf(projection.subagents, 1);
39 assert.deepInclude(middle, { status: "completed", result: "MIDDLE_OK" });
40 const middleThread = result.projections.get(middle!.childThreadId!);
41 assert.isDefined(middleThread);
42 // The grandchild ran to its own end instead of being stopped with its caller.
43 assert.lengthOf(middleThread.subagents, 1);
44 const grandchild = middleThread.subagents[0]!;
45 assert.deepInclude(grandchild, { status: "completed", result: "GRANDCHILD_OK" });
46 assert.deepEqual(texts(result.projections.get(grandchild.childThreadId!)), ["GRANDCHILD_OK"]);
47 // The middle subagent answered the report in a turn of its own.
48 assert.deepEqual(texts(middleThread), ["MIDDLE_OK", "GRANDCHILD_OK"]);
49
50 // The run waited on the grandchild: it completed after the grandchild did.
51 const indexOf = (predicate: (event: (typeof result.domainEvents)[number]) => boolean) =>
52 result.domainEvents.findIndex(predicate);
53 const rootTurnEnded = indexOf(
54 (event) =>
55 event.type === "run.updated" &&
56 event.payload.id === projection.runs[0]?.id &&
57 event.payload.status === "waiting",
58 );
59 const grandchildDone = indexOf(
60 (event) =>
61 event.type === "subagent.updated" &&
62 event.payload.id === grandchild.id &&
63 event.payload.status === "completed",
64 );
65 const runCompleted = indexOf(
66 (event) =>
67 event.type === "run.updated" &&
68 event.payload.id === projection.runs[0]?.id &&
69 event.payload.status === "completed",
70 );
71 assert.isAtLeast(rootTurnEnded, 0);
72 assert.isAbove(grandchildDone, rootTurnEnded, "the thread's turn ends before the grandchild");
73 assert.isAbove(runCompleted, grandchildDone, "the run waits for the grandchild");
74
75 const shell = result.shellSnapshot.threads.find((thread) => thread.id === projection.thread.id);
76 assert.deepEqual(shell?.pendingBackgroundTasks ?? [], []);
77}