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 assertTurnItemTypeSequence,
10 assertUserMessagesInclude,
11 assertVisibleTurnItemsMirrorLocalTurnItems,
12 OPENCODE2_TOOL_CALL_PROMPT,
13 projectionFor,
14} from "../shared.ts";
15
16/** OpenCode 2's renamed `read` and `shell` tools, run in one step, then a second step. */
17export function assertOpenCode2ToolCallOutput(
18 result: OrchestratorV2ScenarioResult,
19 transcript: ProviderReplayTranscript,
20) {
21 assertBaseProjection({ result, transcript, runCount: 1, runStatuses: ["completed"] });
22 const projection = projectionFor(result, transcript.scenario);
23 assertSemanticProjectionIntegrity(projection);
24 assertVisibleTurnItemsMirrorLocalTurnItems(projection);
25 // Buffered delivery keeps streaming text out of the read model until
26 // `text.ended`, which OpenCode sends after both tool calls started.
27 assertTurnItemTypeSequence(projection, [
28 "user_message",
29 "dynamic_tool",
30 "command_execution",
31 "assistant_message",
32 "assistant_message",
33 "checkpoint",
34 ]);
35 assertUserMessagesInclude(projection, [OPENCODE2_TOOL_CALL_PROMPT]);
36 assertAssistantTextIncludes(projection, "DONE");
37
38 const read = projection.turnItems.find((item) => item.type === "dynamic_tool");
39 assert.equal(read?.status, "completed");
40 assert.equal(read?.title, "Read hello.txt");
41 assert.include(read?.type === "dynamic_tool" ? read.output : "", "hello from the spike");
42
43 const shell = projection.turnItems.find((item) => item.type === "command_execution");
44 assert.equal(shell?.status, "completed");
45 assert.deepInclude(shell, { input: "echo TOOL_OK", output: "TOOL_OK\n", exitCode: 0 });
46
47 // Two steps: their usage adds up, and the last one is the live context size.
48 const [providerTurn] = projection.providerTurns;
49 assert.deepInclude(providerTurn?.turnTokenUsage, {
50 usageStatus: "complete",
51 inputTokens: 8767 + 487 + 45 + 9311,
52 outputTokens: 59 + 33,
53 });
54 // big-pickle lists a 200k context but takes 160k of input.
55 assert.deepInclude(providerTurn?.tokenUsage, { usedTokens: 45 + 9311 + 33, maxTokens: 160000 });
56}