1import { assert } from "@effect/vitest";
2import type { ProviderReplayTranscript } from "@t3tools/contracts";
3
4import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
5import { assertMultiTurnOutput } from "./codex_output.ts";
6import { projectionFor } from "../shared.ts";
7
8function isReplayFrameWithType(
9 frame: unknown,
10 type: string,
11): frame is { readonly type: string; readonly options?: Record<string, unknown> } {
12 return (
13 typeof frame === "object" &&
14 frame !== null &&
15 "type" in frame &&
16 (frame as { readonly type?: unknown }).type === type
17 );
18}
19
20export function assertMultiTurnClaudeOutput(
21 result: OrchestratorV2ScenarioResult,
22 transcript: ProviderReplayTranscript,
23) {
24 assertMultiTurnOutput(result, transcript);
25
26 const projection = projectionFor(result, transcript.scenario);
27 assert.lengthOf(projection.providerThreads, 1);
28 const providerThread = projection.providerThreads[0];
29 assert.isDefined(providerThread);
30 assert.isNotNull(providerThread.nativeThreadRef);
31 assert.deepEqual(
32 projection.runs.map((run) => run.providerThreadId),
33 projection.runs.map(() => providerThread.id),
34 );
35
36 const outboundFrames = transcript.entries.flatMap((entry) =>
37 entry.type === "expect_outbound" ? [entry.frame] : [],
38 );
39 const queryOpenFrames = outboundFrames.filter((frame) =>
40 isReplayFrameWithType(frame, "query.open"),
41 );
42 const promptOfferFrames = outboundFrames.filter((frame) =>
43 isReplayFrameWithType(frame, "prompt.offer"),
44 );
45
46 assert.lengthOf(promptOfferFrames, 2);
47 if (transcript.scenario === "multi_turn_restart") {
48 assert.lengthOf(queryOpenFrames, 2);
49 assert.isString(queryOpenFrames[1]?.options?.resume);
50 } else {
51 assert.lengthOf(queryOpenFrames, 1);
52 }
53}