1import { assert } from "@effect/vitest";
2import type { ProviderReplayTranscript } from "@t3tools/contracts";
3
4import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
5import {
6 assertSemanticProjectionIntegrity,
7 assertVisibleTurnItemsMirrorLocalTurnItems,
8 projectionFor,
9} from "../shared.ts";
10
11/**
12 * The workflow row stays live after its turn and settles when the workflow
13 * does; Muse's own report turn becomes a second, completed run instead of
14 * being interrupted, and nothing is left pending.
15 */
16export function assertMuseWorkflowOutput(
17 result: OrchestratorV2ScenarioResult,
18 transcript: ProviderReplayTranscript,
19) {
20 const projection = projectionFor(result, transcript.scenario);
21 assertSemanticProjectionIntegrity(projection);
22 assertVisibleTurnItemsMirrorLocalTurnItems(projection);
23 assert.deepEqual(
24 projection.runs.map((run) => run.status),
25 ["completed", "completed"],
26 );
27 const outbound = transcript.entries.flatMap((entry) =>
28 entry.type === "expect_outbound" ? [entry.frame] : [],
29 );
30 const methods = outbound.map((frame) =>
31 typeof frame === "object" && frame !== null ? Reflect.get(frame, "method") : undefined,
32 );
33 assert.notInclude(methods, "turn/interrupt", "Muse's report turn must not be interrupted");
34 assert.equal(methods.filter((method) => method === "turn/start").length, 1);
35
36 const workflow = projection.turnItems.find(
37 (item) => item.type === "dynamic_tool" && item.toolName === "workflow",
38 );
39 assert.isDefined(workflow, "the workflow shows as a row");
40 assert.equal(workflow.status, "completed");
41 assert.equal(workflow.runId, projection.runs[0]?.id);
42 // The settled row shows the workflow's own result, not its generic label.
43 const finalSummary = transcript.entries
44 .map((entry) =>
45 entry.type === "emit_inbound"
46 ? Reflect.get(Reflect.get(Object(entry.frame).params ?? {}, "item") ?? {}, "finalSummary")
47 : undefined,
48 )
49 .findLast((summary) => typeof summary === "string");
50 assert.isString(finalSummary, "Muse reports the workflow's result");
51 assert.equal(workflow.type === "dynamic_tool" ? workflow.output : undefined, finalSummary);
52
53 const report = projection.messages.filter(
54 (message) => message.role === "assistant" && message.runId === projection.runs[1]?.id,
55 );
56 assert.isAbove(report.length, 0, "Muse's report reaches the second run");
57
58 // Each workflow agent is a native subagent nested under the workflow row.
59 assert.lengthOf(projection.subagents, 2);
60 for (const agent of projection.subagents) {
61 assert.equal(agent.origin, "provider_native");
62 assert.equal(agent.status, "completed");
63 assert.isNull(agent.childThreadId);
64 }
65 const agentRows = projection.turnItems.filter((item) => item.type === "subagent");
66 assert.lengthOf(agentRows, 2);
67 assert.isTrue(agentRows.every((row) => row.parentItemId === workflow.id));
68 assert.deepEqual(projection.providerThreads[0]?.pendingBackgroundTasks ?? [], []);
69}