1import { assert } from "@effect/vitest";
2import type { ProviderReplayTranscript } from "@t3tools/contracts";
3
4import type { OrchestratorV2ScenarioResult } from "../../OrchestratorScenario.ts";
5import {
6 assertBaseProjection,
7 assertSemanticProjectionIntegrity,
8 assertUserMessagesInclude,
9 assertVisibleTurnItemsMirrorLocalTurnItems,
10 backgroundNotifications,
11 projectionFor,
12} from "../shared.ts";
13import { GROK_BACKGROUND_BASH_PROMPT, GROK_BACKGROUND_BASH_TICKS } from "./input.ts";
14
15// Grok completes a background shell command's tool call with a
16// `BackgroundTaskStarted` acknowledgement while the process keeps running,
17// streams its output afterwards, and ends it only with `x.ai/task_completed`.
18// Like a monitor, the command stays in the run that started it and holds that
19// run open; Grok's own reply to the finished command is a continuation run.
20export function assertGrokBackgroundBashOutput(
21 result: OrchestratorV2ScenarioResult,
22 transcript: ProviderReplayTranscript,
23 commandDescription = "Run three tock echoes in the background",
24) {
25 assertBaseProjection({
26 result,
27 transcript,
28 runCount: 2,
29 runStatuses: ["completed", "completed"],
30 });
31 const projection = projectionFor(result, transcript.scenario);
32 assertSemanticProjectionIntegrity(projection);
33 assertVisibleTurnItemsMirrorLocalTurnItems(projection);
34 assertUserMessagesInclude(projection, [GROK_BACKGROUND_BASH_PROMPT]);
35
36 const [rootRun, wakeRun] = projection.runs;
37 const commands = projection.turnItems.filter((item) => item.type === "command_execution");
38 assert.lengthOf(commands, 1);
39 const command = commands[0];
40 assert.equal(command?.runId, rootRun?.id, "the command belongs to the run that started it");
41 assert.equal(command?.status, "completed");
42 assert.equal(command?.output, GROK_BACKGROUND_BASH_TICKS.map((tick) => `${tick}\n`).join(""));
43
44 // The start acknowledgement does not finish the command: every tick before
45 // the end is shown running.
46 const commandUpdates = result.domainEvents.flatMap((event) =>
47 event.type === "turn-item.updated" &&
48 event.payload.id === command?.id &&
49 event.payload.type === "command_execution"
50 ? [event.payload]
51 : [],
52 );
53 const firstCompleted = commandUpdates.findIndex((update) => update.status === "completed");
54 for (const tick of GROK_BACKGROUND_BASH_TICKS.slice(0, -1)) {
55 const tickUpdate = commandUpdates.findIndex((update) => update.output?.includes(tick));
56 assert.isAtLeast(tickUpdate, 0, `missing command update for ${tick}`);
57 assert.isBelow(tickUpdate, firstCompleted, `the command completed before ${tick}`);
58 assert.equal(commandUpdates[tickUpdate]?.status, "running", `${tick} must show running`);
59 }
60
61 // Run 1 settles only after the command's structured end.
62 const commandCompletedAt = result.domainEvents.findIndex(
63 (event) =>
64 event.type === "turn-item.updated" &&
65 event.payload.id === command?.id &&
66 event.payload.status === "completed",
67 );
68 const rootCompletedAt = result.domainEvents.findIndex(
69 (event) =>
70 event.type === "run.updated" &&
71 event.payload.id === rootRun?.id &&
72 event.payload.status === "completed",
73 );
74 assert.isAtLeast(commandCompletedAt, 0);
75 assert.isAbove(rootCompletedAt, commandCompletedAt, "run 1 completed before the command ended");
76
77 const rootTexts = projection.turnItems.flatMap((item) =>
78 item.runId === rootRun?.id && item.type === "assistant_message" ? [item.text.trim()] : [],
79 );
80 assert.include(rootTexts, "ROOT_DONE");
81
82 // The timeline names the command from Grok's task_completed snapshot.
83 assert.deepEqual(backgroundNotifications(projection), [
84 {
85 summary: `Command "${commandDescription}" finished (exit 0)`,
86 outcome: "completed",
87 source: { kind: "command" },
88 },
89 ]);
90
91 // Grok's own wake reply is a provider continuation, not more of run 1.
92 const wakeMessage = projection.messages.find((message) => message.id === wakeRun?.userMessageId);
93 assert.equal(`${wakeMessage?.createdBy}:${wakeMessage?.creationSource}`, "agent:provider");
94 assert.isNotEmpty(
95 projection.turnItems.filter(
96 (item) => item.runId === wakeRun?.id && item.type === "assistant_message",
97 ),
98 "Grok's reply to the finished command must land in the continuation run",
99 );
100}