1import { assert } from "@effect/vitest";
2import type { ProviderReplayTranscript } from "@t3tools/contracts";
3
4import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
5import { assertSemanticProjectionIntegrity, projectionFor } from "../shared.ts";
6
7/**
8 * Stop while a native subagent runs: Stop reaches Muse, and the run, the
9 * subagent, its child thread and the thread's background work all settle.
10 */
11export function assertMuseSubagentInterruptOutput(
12 result: OrchestratorV2ScenarioResult,
13 transcript: ProviderReplayTranscript,
14) {
15 const projection = projectionFor(result, transcript.scenario);
16 assertSemanticProjectionIntegrity(projection);
17 assert.deepEqual(
18 projection.runs.map((run) => run.status),
19 ["interrupted"],
20 );
21 const outbound = transcript.entries.flatMap((entry) =>
22 entry.type === "expect_outbound" ? [entry.frame] : [],
23 );
24 assert.isTrue(
25 outbound.some(
26 (frame) =>
27 typeof frame === "object" &&
28 frame !== null &&
29 Reflect.get(frame, "method") === "turn/interrupt",
30 ),
31 "Stop must reach Muse as turn/interrupt",
32 );
33 assert.isFalse(projection.turnItems.some((item) => item.status === "running"));
34 assert.deepEqual(projection.providerThreads[0]?.pendingBackgroundTasks ?? [], []);
35
36 assert.lengthOf(projection.subagents, 1);
37 const subagent = projection.subagents[0]!;
38 assert.equal(subagent.status, "interrupted");
39 if (subagent.childThreadId === null) throw new Error("The subagent has no child thread");
40 const child = result.projections.get(subagent.childThreadId);
41 if (child === undefined) throw new Error("The child thread has no projection");
42 assert.isFalse(child.nodes.some((node) => node.status === "running"));
43 assert.deepEqual(
44 child.messages.map((message) => [message.role, message.text]),
45 [["user", subagent.prompt]],
46 );
47}