| 1 | import { assert } from "@effect/vitest"; |
| 2 | import type { ProviderReplayTranscript } from "@t3tools/contracts"; |
| 3 | |
| 4 | import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts"; |
| 5 | import { |
| 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 | */ |
| 25 | export 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 | } |