1import { assert } from "@effect/vitest";
2import type { ProviderReplayTranscript } from "@t3tools/contracts";
3
4import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
5import {
6 assertAssistantTextIncludes,
7 assertBaseProjection,
8 assertSemanticProjectionIntegrity,
9 assertUserMessageInputIntents,
10 assertUserMessagesInclude,
11 assertVisibleTurnItemsMirrorLocalTurnItems,
12 OPENCODE2_CANCELLED_PROMPT,
13 OPENCODE2_QUEUED_PROMPT,
14 OPENCODE2_STEER_PROMPT,
15 OPENCODE2_STEER_TEXT,
16 projectionFor,
17} from "../shared.ts";
18
19/**
20 * The steer goes out as `delivery: "steer"` into the running turn, which the
21 * shell call's step boundary delivers: one provider turn answers both, and it
22 * ends with its execution. T3 keeps the queue, so the queued message is a
23 * second turn and the cancelled one never reaches OpenCode.
24 */
25export function assertOpenCode2InboxOutput(
26 result: OrchestratorV2ScenarioResult,
27 transcript: ProviderReplayTranscript,
28) {
29 assertBaseProjection({
30 result,
31 transcript,
32 runCount: 3,
33 // The cancelled run never started a provider turn.
34 providerTurnCountAtLeast: 2,
35 runStatuses: ["completed", "completed", "cancelled"],
36 });
37 const projection = projectionFor(result, transcript.scenario);
38 assertSemanticProjectionIntegrity(projection);
39 assertVisibleTurnItemsMirrorLocalTurnItems(projection);
40 assertUserMessagesInclude(projection, [
41 OPENCODE2_STEER_PROMPT,
42 OPENCODE2_STEER_TEXT,
43 OPENCODE2_QUEUED_PROMPT,
44 ]);
45 assertUserMessageInputIntents(projection, ["turn_start", "steer", "queued_turn"]);
46 assertAssistantTextIncludes(projection, "STEERED");
47 assertAssistantTextIncludes(projection, "QUEUED_B");
48 assert.notInclude(
49 projection.turnItems.flatMap((item) => (item.type === "user_message" ? [item.text] : [])),
50 OPENCODE2_CANCELLED_PROMPT,
51 );
52
53 // The steer joined the first run's turn; the queued message got its own.
54 const [first, queued] = projection.runs.toSorted((left, right) => left.ordinal - right.ordinal);
55 const turnOf = (run: typeof first) =>
56 projection.providerTurns.filter((turn) => turn.runAttemptId === run?.activeAttemptId);
57 assert.deepEqual(
58 [turnOf(first).map((turn) => turn.status), turnOf(queued).map((turn) => turn.status)],
59 [["completed"], ["completed"]],
60 );
61 const steered = projection.turnItems.find(
62 (item) => item.type === "assistant_message" && item.text.includes("STEERED"),
63 );
64 assert.equal(steered?.runId, first?.id);
65 const shell = projection.turnItems.find((item) => item.type === "command_execution");
66 assert.deepEqual([shell?.status, shell?.runId], ["completed", first?.id]);
67
68 // Each turn remembers the user message it prompted with: fork and rollback cut there.
69 for (const turn of [...turnOf(first), ...turnOf(queued)]) {
70 assert.match(turn.nativeTurnRef?.nativeId ?? "", /^msg_t3_turn_/);
71 }
72}