1import { it } from "@effect/vitest";
2import * as Deferred from "effect/Deferred";
3import * as Effect from "effect/Effect";
4import * as Fiber from "effect/Fiber";
5import * as Schema from "effect/Schema";
6import { describe, expect } from "vite-plus/test";
7
8import {
9 XAiExitPlanModeRequest,
10 XAI_EMPTY_PLAN_MARKDOWN,
11 isGrokPlanMarkdownPath,
12 extractXAiExitPlanMarkdown,
13 extractGrokPlanMarkdownFromToolCallData,
14 extractXAiAcpBackgroundToolMutation,
15 extractXAiAcpSubagentEndNotice,
16 extractXAiAcpSubagentUpdate,
17 extractXAiAskUserQuestions,
18 extractXAiBackgroundTaskCompletion,
19 extractXAiKilledBackgroundTasks,
20 extractXAiMonitorTaskId,
21 isGenericAcpToolTitle,
22 isXAiMonitorTool,
23 isXAiPersistentMonitor,
24 makeXAiAskUserQuestionResponse,
25 makeXAiPromptCompletionRuntime,
26 normalizeXAiAcpToolCallState,
27 registerXAiBackgroundTaskTracking,
28 registerXAiSubagentFinished,
29 resolveXAiAcpToolTitle,
30 xAiBackgroundTaskLifecycleMutation,
31 xAiPromptCompleteFromSessionUpdate,
32 xAiSubagentFinishedNotice,
33 XAiAskUserQuestionRequest,
34} from "./xaiAcpExtension.ts";
35import * as AcpSessionRuntime from "@t3tools/provider-acp/server/AcpSessionRuntime";
36import {
37 type AcpToolCallState,
38 mergeToolCallState,
39 parseSessionUpdateEvent,
40} from "@t3tools/provider-acp/server/runtimeModel";
41
42const decodeXAiAskUserQuestionRequest = Schema.decodeUnknownSync(XAiAskUserQuestionRequest);
43
44describe("xAiPromptCompleteFromSessionUpdate", () => {
45 it("maps live turn_completed snake_case payloads", () => {
46 expect(
47 xAiPromptCompleteFromSessionUpdate({
48 sessionId: "019f4428-4bf1-7e52-b7c6-c29b506543b1",
49 update: {
50 sessionUpdate: "turn_completed",
51 prompt_id: "t3-xai-prompt-1",
52 stop_reason: "end_turn",
53 },
54 }),
55 ).toEqual({
56 sessionId: "019f4428-4bf1-7e52-b7c6-c29b506543b1",
57 promptId: "t3-xai-prompt-1",
58 stopReason: "end_turn",
59 });
60 });
61
62 it("ignores non-turn updates and task-completed prompt ids", () => {
63 expect(
64 xAiPromptCompleteFromSessionUpdate({
65 sessionId: "root",
66 update: { sessionUpdate: "hook_execution", prompt_id: "t3-xai-prompt-1" },
67 }),
68 ).toBeNull();
69 expect(
70 xAiPromptCompleteFromSessionUpdate({
71 sessionId: "root",
72 update: {
73 sessionUpdate: "turn_completed",
74 prompt_id: "task-completed-call-abc",
75 stop_reason: "end_turn",
76 },
77 }),
78 ).toBeNull();
79 expect(
80 xAiPromptCompleteFromSessionUpdate({
81 sessionId: "root",
82 update: { sessionUpdate: "turn_completed", stop_reason: "end_turn" },
83 }),
84 ).toBeNull();
85 });
86});
87
88// Runs a recorded Grok tool_call_update through the adapter's parse path.
89function parseRecordedToolCallUpdate(update: Record<string, unknown>): AcpToolCallState {
90 const [event] = parseSessionUpdateEvent({
91 sessionId: "01a0d660-c1bb-7842-91a3-02d91dc8b0d2",
92 update,
93 } as Parameters<typeof parseSessionUpdateEvent>[0]).events;
94 if (event?._tag !== "ToolCallUpdated") throw new Error("expected a tool call update");
95 return event.toolCall;
96}
97
98describe("XAiAcpExtension", () => {
99 it("recognizes Grok Task starts as native subagents", () => {
100 expect(
101 extractXAiAcpSubagentUpdate({
102 toolCallId: "task-1",
103 title: "Task",
104 status: "inProgress",
105 data: {
106 rawInput: {
107 description: "Explore server architecture",
108 prompt: "Audit apps/server.",
109 subagent_type: "generalPurpose",
110 model: "composer-2.5-fast",
111 },
112 },
113 }),
114 ).toEqual({
115 nativeTaskId: "task-1",
116 prompt: "Audit apps/server.",
117 title: "Explore server architecture",
118 model: "composer-2.5-fast",
119 status: "running",
120 childSessionId: null,
121 result: null,
122 suppressNormalTool: true,
123 });
124 });
125
126 it("extracts Grok child session lineage from completed Task output", () => {
127 expect(
128 extractXAiAcpSubagentUpdate({
129 toolCallId: "task-1",
130 title: "Task",
131 status: "completed",
132 data: {
133 rawInput: {
134 description: "Explore server architecture",
135 prompt: "Audit apps/server.",
136 subagent_type: "generalPurpose",
137 },
138 rawOutput: {
139 type: "Text",
140 text: [
141 "Server audit complete.",
142 "",
143 "Agent ID: 019f0220-e192-7c41-9e9d-b406bc3459c8 (resume supported)",
144 ].join("\n"),
145 },
146 },
147 }),
148 ).toEqual({
149 nativeTaskId: "task-1",
150 prompt: "Audit apps/server.",
151 title: "Explore server architecture",
152 model: null,
153 status: "completed",
154 childSessionId: "019f0220-e192-7c41-9e9d-b406bc3459c8",
155 result: "Server audit complete.",
156 suppressNormalTool: true,
157 });
158 });
159
160 it("keeps async spawn_subagent ACKs running and binds subagent_id", () => {
161 expect(
162 extractXAiAcpSubagentUpdate({
163 toolCallId: "call-spawn-1",
164 title: "spawn_subagent",
165 status: "completed",
166 data: {
167 rawInput: {
168 description: "Demo subagent wait and ls",
169 prompt: "Wait then ls.",
170 subagent_type: "general-purpose",
171 },
172 rawOutput: {
173 type: "Text",
174 text: [
175 "Subagent started in background.",
176 "subagent_id: 019f44a6-4820-7402-925d-bc862ee711dd",
177 "type: general-purpose",
178 "description: Demo subagent wait and ls",
179 "",
180 'Use get_command_or_subagent_output with task_ids=["019f44a6-4820-7402-925d-bc862ee711dd"] and timeout_ms to wait for results.',
181 ].join("\n"),
182 },
183 },
184 }),
185 ).toEqual({
186 nativeTaskId: "call-spawn-1",
187 prompt: "Wait then ls.",
188 title: "Demo subagent wait and ls",
189 model: null,
190 status: "running",
191 childSessionId: "019f44a6-4820-7402-925d-bc862ee711dd",
192 result: null,
193 suppressNormalTool: true,
194 });
195 });
196
197 it("hydrates subagent completion from get_command_or_subagent_output", () => {
198 expect(
199 extractXAiAcpSubagentUpdate({
200 toolCallId: "call-get-1",
201 title: "get_command_or_subagent_output",
202 status: "completed",
203 data: {
204 rawInput: {
205 task_ids: ["019f44a6-4820-7402-925d-bc862ee711dd"],
206 timeout_ms: 30000,
207 },
208 rawOutput: {
209 type: "Text",
210 text: [
211 "=== Task 019f44a6-4820-7402-925d-bc862ee711dd ===",
212 "Status: completed",
213 "Exit Code: 0",
214 "",
215 "=== Output ===",
216 "SUBAGENT_MARKER: 35 entries",
217 ].join("\n"),
218 },
219 },
220 }),
221 ).toEqual({
222 nativeTaskId: "call-get-1",
223 prompt: "",
224 title: null,
225 model: null,
226 status: "completed",
227 childSessionId: "019f44a6-4820-7402-925d-bc862ee711dd",
228 result: "SUBAGENT_MARKER: 35 entries",
229 suppressNormalTool: true,
230 });
231 });
232
233 it("attributes multi-task get_output envelopes to the result task_id", () => {
234 expect(
235 extractXAiAcpSubagentUpdate({
236 toolCallId: "call-get-multi",
237 title: "get_command_or_subagent_output",
238 status: "completed",
239 data: {
240 rawInput: {
241 task_ids: [
242 "019f44a6-4820-7402-925d-bc862ee711dd",
243 "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
244 ],
245 timeout_ms: 30000,
246 },
247 rawOutput: {
248 type: "TaskOutput",
249 Result: {
250 task_id: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
251 status: "completed",
252 exit_code: 0,
253 output: "SECOND_SUB_DONE\n",
254 },
255 },
256 },
257 }),
258 ).toMatchObject({
259 childSessionId: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
260 status: "completed",
261 result: "SECOND_SUB_DONE",
262 });
263 });
264
265 it("does not treat a subagent_id mentioned in result text as the completed identity", () => {
266 const requestedTaskId = "019f44a6-4820-7402-925d-bc862ee711dd";
267 const mentionedTaskId = "019f44b0-1b73-7f32-bb1b-1ff696f536e3";
268 const toolCall = {
269 toolCallId: "call-get-mentioned-id",
270 title: "get_command_or_subagent_output",
271 status: "completed" as const,
272 data: {
273 rawInput: {
274 variant: "TaskOutput",
275 task_ids: [requestedTaskId],
276 },
277 rawOutput: {
278 type: "TaskOutput",
279 Result: {
280 status: "completed",
281 exit_code: 0,
282 output: `Audited subagent_id: ${mentionedTaskId}; no issues found.`,
283 },
284 },
285 },
286 };
287
288 expect(extractXAiAcpSubagentUpdate(toolCall)).toMatchObject({
289 childSessionId: requestedTaskId,
290 status: "completed",
291 });
292 expect(extractXAiBackgroundTaskCompletion(toolCall)).toEqual([
293 {
294 taskId: requestedTaskId,
295 status: "completed",
296 appendOutput: `Audited subagent_id: ${mentionedTaskId}; no issues found.`,
297 },
298 ]);
299 });
300
301 it("hydrates structured ACP TaskOutput tool envelopes", () => {
302 expect(
303 extractXAiAcpSubagentUpdate({
304 toolCallId: "call-get-2",
305 title: "[subagent:general-purpose] Sleep then return SUBAGENT_DONE (019f44b0)",
306 status: "completed",
307 data: {
308 rawInput: {
309 variant: "TaskOutput",
310 task_ids: ["019f44b0-1b73-7f32-bb1b-1ff696f536e3"],
311 timeout_ms: 20000,
312 },
313 rawOutput: {
314 type: "TaskOutput",
315 Result: {
316 task_id: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
317 status: "completed",
318 exit_code: 0,
319 output:
320 "SUBAGENT_DONE\n\n<subagent_meta>id=019f44b0-1b73-7f32-bb1b-1ff696f536e3</subagent_meta>\n",
321 },
322 },
323 },
324 }),
325 ).toEqual({
326 nativeTaskId: "call-get-2",
327 prompt: "",
328 title: null,
329 model: null,
330 status: "completed",
331 childSessionId: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
332 result: "SUBAGENT_DONE",
333 suppressNormalTool: true,
334 });
335 });
336
337 it("does not leak raw machine tags when get_output is only meta/result blocks", () => {
338 const metaOnlyToolCall = {
339 toolCallId: "call-get-meta-only",
340 title: "get_command_or_subagent_output",
341 status: "completed" as const,
342 data: {
343 rawInput: {
344 variant: "TaskOutput",
345 task_ids: ["019f44b0-1b73-7f32-bb1b-1ff696f536e3"],
346 timeout_ms: 20000,
347 },
348 rawOutput: {
349 type: "TaskOutput",
350 Result: {
351 task_id: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
352 status: "completed",
353 exit_code: 0,
354 output:
355 "<subagent_meta>id=019f44b0-1b73-7f32-bb1b-1ff696f536e3</subagent_meta>\n<subagent_result>done</subagent_result>\n",
356 },
357 },
358 },
359 };
360 expect(extractXAiAcpSubagentUpdate(metaOnlyToolCall)).toEqual({
361 nativeTaskId: "call-get-meta-only",
362 prompt: "",
363 title: null,
364 model: null,
365 status: "completed",
366 childSessionId: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
367 result: null,
368 suppressNormalTool: true,
369 });
370 expect(extractXAiBackgroundTaskCompletion(metaOnlyToolCall)).toEqual([
371 {
372 taskId: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
373 status: "completed",
374 appendOutput: "",
375 },
376 ]);
377 });
378
379 it("keeps monitor start ACKs running and extracts task ids", () => {
380 const toolCall = {
381 toolCallId: "call-mon-1",
382 title: "monitor",
383 status: "completed" as const,
384 data: {
385 rawInput: {
386 command: "echo mon_line_1",
387 description: "Demo monitor",
388 },
389 rawOutput: {
390 type: "Text",
391 text: "Monitor started (task 019f44a5-87d1-7640-8e35-6a4667ffc873, timeout 36000000ms).\nYou will be notified on each event.",
392 },
393 },
394 };
395 expect(normalizeXAiAcpToolCallState(toolCall).status).toBe("inProgress");
396 expect(extractXAiMonitorTaskId(toolCall)).toBe("019f44a5-87d1-7640-8e35-6a4667ffc873");
397 });
398
399 it("tracks text Monitor ACKs with generic titles and description-only input", () => {
400 const toolCall = {
401 toolCallId: "call-mon-generic-text",
402 title: "Tool",
403 status: "completed" as const,
404 data: {
405 rawInput: {
406 command: "echo mon_line_1",
407 description: "Demo monitor without variant",
408 },
409 rawOutput: {
410 type: "Text",
411 text: "Monitor started (task 019f44a5-87d1-7640-8e35-6a4667ffc873, timeout 36000000ms).\nYou will be notified on each event.",
412 },
413 },
414 };
415
416 const normalized = normalizeXAiAcpToolCallState(toolCall);
417 expect(isXAiMonitorTool(normalized)).toBe(true);
418 expect(normalized.status).toBe("inProgress");
419 expect(extractXAiMonitorTaskId(normalized)).toBe("019f44a5-87d1-7640-8e35-6a4667ffc873");
420 });
421
422 it("keeps Monitor ticks running while Grok reports them in progress", () => {
423 // Recorded from Grok 1.0.41: every tick is an in_progress update whose
424 // Bash-shaped rawOutput already carries exit_code 0. The monitor ends only
425 // through `_x.ai/task_completed`, never through a completed status.
426 const command = "for i in 1 2 3; do sleep 8; echo tick $i; done";
427 const started = parseRecordedToolCallUpdate({
428 sessionUpdate: "tool_call_update",
429 toolCallId: "call-6b025a4e-621c-4526-a579-ba86ada2669c-0",
430 kind: "other",
431 title: "Start monitor: Watch three ticks eight seconds apart",
432 locations: [],
433 rawInput: {
434 variant: "Monitor",
435 command,
436 description: "Watch three ticks eight seconds apart",
437 timeout_ms: 36000000,
438 persistent: false,
439 },
440 });
441 const tick = parseRecordedToolCallUpdate({
442 sessionUpdate: "tool_call_update",
443 toolCallId: "call-6b025a4e-621c-4526-a579-ba86ada2669c-0",
444 status: "in_progress",
445 content: [{ type: "content", content: { type: "text", text: "tick 1\n" } }],
446 rawOutput: {
447 type: "Bash",
448 output: [116, 105, 99, 107, 32, 49, 10],
449 output_for_prompt: "tick 1\n",
450 exit_code: 0,
451 command,
452 truncated: false,
453 signal: null,
454 timed_out: false,
455 description: null,
456 current_dir: "<workspace>",
457 output_file: "",
458 total_bytes: 7,
459 },
460 });
461 const toolCall = normalizeXAiAcpToolCallState(mergeToolCallState(started, tick));
462 expect(isXAiMonitorTool(toolCall)).toBe(true);
463 expect(toolCall.status).toBe("inProgress");
464 });
465
466 it("finishes monitors from the recorded task_completed snapshot", () => {
467 // Fields from the grok_monitor recording (Grok 1.0.41).
468 const completed = {
469 sessionId: "01a0d660-c1bb-7842-91a3-02d91dc8b0d2",
470 update: {
471 sessionUpdate: "task_completed",
472 task_snapshot: {
473 task_id: "01a0d660-f98d-7ef3-a291-97af1c2b6455",
474 command: "for i in 1 2 3; do sleep 8; echo tick $i; done",
475 display_command: "[monitor] Watch three tick echoes",
476 output: "tick 1\ntick 2\ntick 3\n",
477 exit_code: 0,
478 kind: "monitor",
479 description: "Watch three tick echoes",
480 },
481 },
482 };
483 expect(xAiBackgroundTaskLifecycleMutation(completed, "completed")).toEqual({
484 sessionId: "01a0d660-c1bb-7842-91a3-02d91dc8b0d2",
485 taskId: "01a0d660-f98d-7ef3-a291-97af1c2b6455",
486 status: "completed",
487 output: "tick 1\ntick 2\ntick 3\n",
488 report: { kind: "monitor", label: "Watch three tick echoes" },
489 });
490 expect(
491 xAiBackgroundTaskLifecycleMutation(
492 {
493 ...completed,
494 update: { ...completed.update, task_snapshot: { task_id: "t", exit_code: 2 } },
495 },
496 "completed",
497 )?.status,
498 ).toBe("failed");
499 });
500
501 it("detects Monitor start ACKs from structured rawOutput when title is generic", () => {
502 const toolCall = {
503 toolCallId: "call-mon-generic-title",
504 title: "Tool",
505 status: "completed" as const,
506 data: {
507 rawInput: {
508 command: "echo mon_line",
509 description: "Demo monitor without variant",
510 },
511 rawOutput: {
512 type: "Monitor",
513 taskId: "019f44a5-87d1-7640-8e35-6a4667ffc873",
514 timeoutMs: 36000000,
515 persistent: false,
516 },
517 },
518 };
519 expect(isXAiMonitorTool(toolCall)).toBe(true);
520 expect(normalizeXAiAcpToolCallState(toolCall).status).toBe("inProgress");
521 expect(extractXAiMonitorTaskId(toolCall)).toBe("019f44a5-87d1-7640-8e35-6a4667ffc873");
522 });
523
524 it("completes status-less Bash results despite generic titles", () => {
525 // A Bash result that carries no status (empty rawInput, generic title) is
526 // terminalized from its exit_code. An explicit in_progress status wins:
527 // that is how Grok streams a still-running monitor.
528 const toolCall = {
529 toolCallId: "call-wake-1",
530 title: "Tool",
531 data: {
532 rawInput: {},
533 rawOutput: {
534 type: "Bash",
535 output: Array.from(new TextEncoder().encode("STREAM_1\nSTREAM_DONE\n")),
536 output_for_prompt: "STREAM_1\nSTREAM_DONE\n",
537 exit_code: 0,
538 command: "for i in 1 2; do echo STREAM_$i; done; echo STREAM_DONE",
539 },
540 },
541 };
542 expect(isXAiMonitorTool(toolCall)).toBe(false);
543 const normalized = normalizeXAiAcpToolCallState(toolCall);
544 expect(normalized.status).toBe("completed");
545 expect(normalized.title).toBe("for i in 1 2; do echo STREAM_$i; done; echo STREAM_DONE");
546 expect(
547 normalizeXAiAcpToolCallState({
548 ...toolCall,
549 data: {
550 ...toolCall.data,
551 rawOutput: { ...toolCall.data.rawOutput, exit_code: 2 },
552 },
553 }).status,
554 ).toBe("failed");
555 });
556
557 it("keeps empty successful Bash updates running until native output or turn settlement", () => {
558 const normalized = normalizeXAiAcpToolCallState({
559 toolCallId: "call-bash-permission-ack",
560 title: "Ran command",
561 status: "completed",
562 data: {
563 rawInput: { command: "sleep 40" },
564 rawOutput: { type: "Bash", exit_code: 0 },
565 },
566 });
567 expect(normalized.status).toBe("inProgress");
568 });
569
570 it("keeps structured Monitor start ACKs running and extracts taskId", () => {
571 const toolCall = {
572 toolCallId: "call-mon-3",
573 title: "Start monitor: Wait 30s then list directory",
574 status: "completed" as const,
575 data: {
576 rawInput: {
577 variant: "Monitor",
578 command: "sleep 30 && ls",
579 description: "Wait 30s then list directory",
580 },
581 rawOutput: {
582 type: "Monitor",
583 taskId: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3",
584 timeoutMs: 36000000,
585 persistent: false,
586 },
587 },
588 };
589 expect(normalizeXAiAcpToolCallState(toolCall).status).toBe("inProgress");
590 expect(extractXAiMonitorTaskId(toolCall)).toBe("019f44b8-8e98-7c80-a40e-df1e26a5f9e3");
591 });
592
593 it("replaces generic ACP titles with description / Monitor labels", () => {
594 expect(isGenericAcpToolTitle("Tool")).toBe(true);
595 expect(isGenericAcpToolTitle("Read package.json")).toBe(false);
596 expect(
597 resolveXAiAcpToolTitle({
598 toolCallId: "call-mon-title",
599 title: "Tool",
600 status: "completed",
601 data: {
602 rawInput: {
603 variant: "Monitor",
604 command: "sleep 30 && ls",
605 description: "Wait 30s then list directory",
606 },
607 },
608 }),
609 ).toBe("Monitor: Wait 30s then list directory");
610 const normalized = normalizeXAiAcpToolCallState({
611 toolCallId: "call-mon-title",
612 title: "Tool",
613 status: "completed",
614 data: {
615 rawInput: {
616 variant: "Monitor",
617 command: "sleep 30 && ls",
618 description: "Wait 30s then list directory",
619 },
620 rawOutput: {
621 type: "Monitor",
622 taskId: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3",
623 timeoutMs: 36000000,
624 persistent: false,
625 },
626 },
627 });
628 expect(normalized.title).toBe("Monitor: Wait 30s then list directory");
629 expect(normalized.status).toBe("inProgress");
630 // Non-generic titles from the CLI are preserved.
631 expect(
632 resolveXAiAcpToolTitle({
633 toolCallId: "call-read",
634 title: "Read package.json",
635 status: "inProgress",
636 data: { rawInput: { path: "package.json" } },
637 }),
638 ).toBe("Read package.json");
639 });
640
641 it("hydrates monitor completion from TaskOutput get_command envelopes", () => {
642 expect(
643 extractXAiBackgroundTaskCompletion({
644 toolCallId: "call-get-mon",
645 title: "Wait 30s then list directory",
646 status: "completed",
647 data: {
648 rawInput: {
649 variant: "TaskOutput",
650 task_ids: ["019f44b8-8e98-7c80-a40e-df1e26a5f9e3"],
651 },
652 rawOutput: {
653 type: "TaskOutput",
654 Result: {
655 task_id: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3",
656 command: "[monitor] Wait 30s then list directory",
657 status: "completed",
658 exit_code: 0,
659 output: "agents\nAGENTS.md\nnotes\n",
660 },
661 },
662 },
663 }),
664 ).toEqual([
665 {
666 taskId: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3",
667 status: "completed",
668 appendOutput: "agents\nAGENTS.md\nnotes",
669 },
670 ]);
671 });
672
673 it("hydrates monitor completion from a standalone TaskOutput frame without rawInput", () => {
674 // Post-settle shape observed live (2026-07-12): the final
675 // get_command_or_subagent_output update carries only rawOutput; the frame
676 // is parsed in isolation (no in-turn merge), and this completion is the
677 // ONLY end signal when the agent consumed the output itself (no "Monitor
678 // ended" reminder follows).
679 expect(
680 extractXAiBackgroundTaskCompletion({
681 toolCallId: "call-c29e64dd-ce5d-4eac-a6ca-c20513fefac3-1",
682 title: "[monitor] Stream numbered markers every 3s (019f54a0)",
683 status: "completed",
684 data: {
685 rawOutput: {
686 type: "TaskOutput",
687 Result: {
688 task_id: "019f54a0-06a8-77f2-8214-e24937cad564",
689 command: "[monitor] Stream numbered markers every 3s",
690 status: "completed",
691 exit_code: 0,
692 output: "STREAM_1\nSTREAM_DONE\n",
693 },
694 },
695 },
696 }),
697 ).toMatchObject([
698 {
699 taskId: "019f54a0-06a8-77f2-8214-e24937cad564",
700 status: "completed",
701 },
702 ]);
703 });
704
705 it("hydrates every requested monitor when TaskOutput has no result task_id", () => {
706 expect(
707 extractXAiBackgroundTaskCompletion({
708 toolCallId: "call-get-multi-mon",
709 title: "get_command_or_subagent_output",
710 status: "completed",
711 data: {
712 rawInput: {
713 variant: "TaskOutput",
714 task_ids: [
715 "019f44a6-4820-7402-925d-bc862ee711dd",
716 "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
717 ],
718 },
719 rawOutput: {
720 type: "TaskOutput",
721 Result: {
722 status: "completed",
723 exit_code: 0,
724 output: "BOTH_DONE\n",
725 },
726 },
727 },
728 }),
729 ).toEqual([
730 {
731 taskId: "019f44a6-4820-7402-925d-bc862ee711dd",
732 status: "completed",
733 appendOutput: "BOTH_DONE",
734 },
735 {
736 taskId: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
737 status: "completed",
738 appendOutput: "BOTH_DONE",
739 },
740 ]);
741 });
742
743 it("keeps every requested monitor running when a completed poll has no terminal task status", () => {
744 expect(
745 extractXAiBackgroundTaskCompletion({
746 toolCallId: "call-get-running-monitors",
747 title: "get_command_or_subagent_output",
748 status: "completed",
749 data: {
750 rawInput: {
751 variant: "TaskOutput",
752 task_ids: [
753 "019f44a6-4820-7402-925d-bc862ee711dd",
754 "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
755 ],
756 },
757 rawOutput: {
758 type: "TaskOutput",
759 Result: {
760 output: "No task has reached a terminal state.",
761 },
762 },
763 },
764 }),
765 ).toEqual([
766 {
767 taskId: "019f44a6-4820-7402-925d-bc862ee711dd",
768 status: "running",
769 appendOutput: "No task has reached a terminal state.",
770 },
771 {
772 taskId: "019f44b0-1b73-7f32-bb1b-1ff696f536e3",
773 status: "running",
774 appendOutput: "No task has reached a terminal state.",
775 },
776 ]);
777 });
778
779 it("treats Exit Code: -1 text envelopes as failed", () => {
780 expect(
781 extractXAiBackgroundTaskCompletion({
782 toolCallId: "call-get-neg",
783 title: "get_command_or_subagent_output",
784 status: "completed",
785 data: {
786 rawInput: {
787 variant: "TaskOutput",
788 task_ids: ["019f44b8-8e98-7c80-a40e-df1e26a5f9e3"],
789 },
790 rawOutput: {
791 type: "Text",
792 text: ["=== Task 019f44b8-8e98-7c80-a40e-df1e26a5f9e3 ===", "Exit Code: -1"].join("\n"),
793 },
794 },
795 }),
796 ).toMatchObject([
797 {
798 taskId: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3",
799 status: "failed",
800 },
801 ]);
802 });
803
804 it("tombstones confirmed kills and ignores failed kill calls", () => {
805 expect(
806 extractXAiKilledBackgroundTasks({
807 toolCallId: "call-kill-1",
808 title: "kill_command_or_subagent",
809 status: "completed",
810 data: {
811 rawInput: {
812 variant: "Kill",
813 task_ids: ["call-bg-1", "call-bg-2", "call-bg-1"],
814 },
815 },
816 }),
817 ).toEqual([
818 { taskId: "call-bg-1", status: "completed", appendOutput: "" },
819 { taskId: "call-bg-2", status: "completed", appendOutput: "" },
820 ]);
821 // Singular task_id shape, matched by variant when the title is generic.
822 expect(
823 extractXAiKilledBackgroundTasks({
824 toolCallId: "call-kill-2",
825 title: "Tool",
826 status: "failed",
827 data: { rawInput: { variant: "kill", task_id: "call-bg-3" } },
828 }),
829 ).toEqual([]);
830 // In-flight kill calls and unrelated tools contribute nothing.
831 expect(
832 extractXAiKilledBackgroundTasks({
833 toolCallId: "call-kill-3",
834 title: "kill_command_or_subagent",
835 status: "inProgress",
836 data: { rawInput: { task_ids: ["call-bg-4"] } },
837 }),
838 ).toEqual([]);
839 expect(
840 extractXAiKilledBackgroundTasks({
841 toolCallId: "call-get",
842 title: "get_command_or_subagent_output",
843 status: "completed",
844 data: { rawInput: { variant: "TaskOutput", task_ids: ["call-bg-5"] } },
845 }),
846 ).toEqual([]);
847 });
848
849 it("maps task lifecycle notifications to background mutations", () => {
850 // task_backgrounded carries the cancelled call's tool_call_id as task_id.
851 expect(
852 xAiBackgroundTaskLifecycleMutation(
853 {
854 sessionId: "session-1",
855 update: {
856 sessionUpdate: "task_backgrounded",
857 tool_call_id: "call-bg-1",
858 task_id: "call-bg-1",
859 },
860 },
861 "running",
862 ),
863 ).toEqual({ sessionId: "session-1", taskId: "call-bg-1", status: "running" });
864 // task_completed nests the id inside task_snapshot.
865 expect(
866 xAiBackgroundTaskLifecycleMutation(
867 {
868 sessionId: "session-1",
869 update: {
870 sessionUpdate: "task_completed",
871 task_snapshot: { task_id: "call-bg-1" },
872 },
873 },
874 "completed",
875 ),
876 ).toEqual({ sessionId: "session-1", taskId: "call-bg-1", status: "completed" });
877 expect(
878 xAiBackgroundTaskLifecycleMutation(
879 { sessionId: "session-1", update: { sessionUpdate: "task_completed" } },
880 "completed",
881 ),
882 ).toBeNull();
883 });
884
885 it.effect("tracks canonical Grok task lifecycle notifications", () =>
886 Effect.gen(function* () {
887 const handlers = new Map<string, (notification: unknown) => Effect.Effect<void>>();
888 const mutations: Array<{
889 readonly sessionId: string;
890 readonly taskId: string;
891 readonly status: "running" | "completed" | "failed";
892 }> = [];
893 const runtime = {
894 handleExtNotification: (
895 method: string,
896 _schema: unknown,
897 handler: (notification: unknown) => Effect.Effect<void>,
898 ) => {
899 handlers.set(method, handler);
900 return Effect.void;
901 },
902 } as unknown as Pick<AcpSessionRuntime.AcpSessionRuntime["Service"], "handleExtNotification">;
903
904 yield* registerXAiBackgroundTaskTracking(runtime, (mutation) =>
905 Effect.sync(() => mutations.push(mutation)),
906 );
907
908 const backgrounded = handlers.get("x.ai/task_backgrounded");
909 const completed = handlers.get("x.ai/task_completed");
910 expect(backgrounded).toBeDefined();
911 expect(completed).toBeDefined();
912 yield* backgrounded!({
913 sessionId: "session-1",
914 update: {
915 sessionUpdate: "task_backgrounded",
916 tool_call_id: "call-bg-1",
917 task_id: "task-bg-1",
918 },
919 });
920 yield* completed!({
921 sessionId: "session-1",
922 update: {
923 sessionUpdate: "task_completed",
924 task_snapshot: { task_id: "task-bg-1" },
925 },
926 });
927 expect(mutations).toEqual([
928 { sessionId: "session-1", taskId: "task-bg-1", status: "running" },
929 { sessionId: "session-1", taskId: "task-bg-1", status: "completed" },
930 ]);
931 expect(handlers.has("_x.ai/task_backgrounded")).toBe(true);
932 expect(handlers.has("_x.ai/task_completed")).toBe(true);
933 }),
934 );
935
936 it("detects persistent Monitor start ACKs", () => {
937 expect(
938 isXAiPersistentMonitor({
939 toolCallId: "call-mon-persist",
940 title: "Monitor",
941 status: "completed",
942 data: {
943 rawInput: { variant: "Monitor" },
944 rawOutput: {
945 type: "Monitor",
946 taskId: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3",
947 timeoutMs: 36000000,
948 persistent: true,
949 },
950 },
951 }),
952 ).toBe(true);
953 expect(
954 isXAiPersistentMonitor({
955 toolCallId: "call-mon-ephemeral",
956 title: "Monitor",
957 status: "completed",
958 data: {
959 rawInput: { variant: "Monitor" },
960 rawOutput: {
961 type: "Monitor",
962 taskId: "019f44b8-8e98-7c80-a40e-df1e26a5f9e3",
963 timeoutMs: 36000000,
964 persistent: false,
965 },
966 },
967 }),
968 ).toBe(false);
969 });
970
971 it("keeps a standalone TaskOutput completion terminal through normalize", () => {
972 // Must remain offer evidence post-settle: normalize must not demote it to
973 // inProgress the way monitor start ACKs are.
974 expect(
975 normalizeXAiAcpToolCallState({
976 toolCallId: "call-c29e64dd-ce5d-4eac-a6ca-c20513fefac3-1",
977 title: "[monitor] Stream numbered markers every 3s (019f54a0)",
978 status: "completed",
979 data: {
980 rawOutput: {
981 type: "TaskOutput",
982 Result: {
983 task_id: "019f54a0-06a8-77f2-8214-e24937cad564",
984 status: "completed",
985 exit_code: 0,
986 output: "STREAM_DONE\n",
987 },
988 },
989 },
990 }).status,
991 ).toBe("completed");
992 });
993
994 it("parses monitor event lines and end reminders", () => {
995 expect(
996 extractXAiAcpBackgroundToolMutation(
997 '<monitor-event task_id="019f44a5-87d1-7640-8e35-6a4667ffc873">\n[Demo] mon_line_1\n</monitor-event>',
998 ),
999 ).toEqual([
1000 {
1001 taskId: "019f44a5-87d1-7640-8e35-6a4667ffc873",
1002 status: "running",
1003 appendOutput: "[Demo] mon_line_1\n",
1004 },
1005 ]);
1006 expect(
1007 extractXAiAcpBackgroundToolMutation(
1008 [
1009 "<system-reminder>",
1010 'Monitor "019f44a5-87d1-7640-8e35-6a4667ffc873" ended: [monitor ended: exited (code 0)].',
1011 "Description: Demo monitor",
1012 "</system-reminder>",
1013 ].join("\n"),
1014 ),
1015 ).toMatchObject([
1016 {
1017 taskId: "019f44a5-87d1-7640-8e35-6a4667ffc873",
1018 status: "completed",
1019 },
1020 ]);
1021 // Description/output may contain "error" without meaning the monitor failed.
1022 expect(
1023 extractXAiAcpBackgroundToolMutation(
1024 [
1025 "<system-reminder>",
1026 'Monitor "019f44a5-87d1-7640-8e35-6a4667ffc873" ended: [monitor ended: exited (code 0)].',
1027 "Description: watch error logs for failed deploys",
1028 "error: nothing found",
1029 "</system-reminder>",
1030 ].join("\n"),
1031 ),
1032 ).toMatchObject([
1033 {
1034 taskId: "019f44a5-87d1-7640-8e35-6a4667ffc873",
1035 status: "completed",
1036 },
1037 ]);
1038 });
1039
1040 it("parses every monitor-event and trailing end notice in one chunk", () => {
1041 const mutations = extractXAiAcpBackgroundToolMutation(
1042 [
1043 '<monitor-event task_id="019f44a5-87d1-7640-8e35-6a4667ffc873">',
1044 "[Demo] mon_line_1",
1045 "</monitor-event>",
1046 '<monitor-event task_id="019f44a5-87d1-7640-8e35-6a4667ffc873">',
1047 "[Demo] mon_line_2",
1048 "</monitor-event>",
1049 'Monitor "019f44a5-87d1-7640-8e35-6a4667ffc873" ended: [monitor ended: exited (code 0)].',
1050 ].join("\n"),
1051 );
1052 expect(mutations).toHaveLength(3);
1053 expect(mutations[0]).toEqual({
1054 taskId: "019f44a5-87d1-7640-8e35-6a4667ffc873",
1055 status: "running",
1056 appendOutput: "[Demo] mon_line_1\n",
1057 });
1058 expect(mutations[1]).toEqual({
1059 taskId: "019f44a5-87d1-7640-8e35-6a4667ffc873",
1060 status: "running",
1061 appendOutput: "[Demo] mon_line_2\n",
1062 });
1063 expect(mutations[2]).toMatchObject({
1064 taskId: "019f44a5-87d1-7640-8e35-6a4667ffc873",
1065 status: "completed",
1066 });
1067 });
1068
1069 it("parses batched monitor event blocks as running mutations", () => {
1070 expect(
1071 extractXAiAcpBackgroundToolMutation(
1072 [
1073 "3 monitor events from 1 monitor (use get_command_or_subagent_output to identify each monitor):",
1074 "",
1075 '<monitor description="Stream STREAM_i every 3s" task_id="019f545d-4d39-7001-b1c8-c7744c448ec1">',
1076 "[1] STREAM_7",
1077 "[2] STREAM_8",
1078 "[3] STREAM_9",
1079 "</monitor>",
1080 ].join("\n"),
1081 ),
1082 ).toEqual([
1083 {
1084 taskId: "019f545d-4d39-7001-b1c8-c7744c448ec1",
1085 status: "running",
1086 appendOutput: "[1] STREAM_7\n[2] STREAM_8\n[3] STREAM_9\n",
1087 },
1088 ]);
1089 });
1090
1091 it("parses the subagent completed reminder as an end notice", () => {
1092 expect(
1093 extractXAiAcpSubagentEndNotice(
1094 [
1095 "<system-reminder>",
1096 'Background subagent "019f5470-bf92-7a90-afb3-5a6cea5b34a3" (general-purpose: "Run sleep then echo token") completed successfully.',
1097 "Duration: 28.8s | Tool calls: 1 | Turns: 1",
1098 'Use get_task_output("019f5470-bf92-7a90-afb3-5a6cea5b34a3") to see the full output.',
1099 "</system-reminder>",
1100 ].join("\n"),
1101 ),
1102 ).toEqual({
1103 childSessionId: "019f5470-bf92-7a90-afb3-5a6cea5b34a3",
1104 status: "completed",
1105 });
1106 });
1107
1108 it("parses a failed subagent reminder as a failed end notice", () => {
1109 expect(
1110 extractXAiAcpSubagentEndNotice(
1111 'Background subagent "019f5470-bf92-7a90-afb3-5a6cea5b34a3" (general-purpose: "Run sleep then echo token") failed.',
1112 ),
1113 ).toEqual({
1114 childSessionId: "019f5470-bf92-7a90-afb3-5a6cea5b34a3",
1115 status: "failed",
1116 });
1117 });
1118
1119 it("ignores prompt text that merely mentions subagents", () => {
1120 expect(
1121 extractXAiAcpSubagentEndNotice("Live-test post-settle subagent completion."),
1122 ).toBeUndefined();
1123 expect(
1124 extractXAiAcpSubagentEndNotice(
1125 'Background subagent "019f5470-bf92-7a90-afb3-5a6cea5b34a3" (general-purpose: "Run sleep then echo token") is still running.',
1126 ),
1127 ).toBeUndefined();
1128 });
1129
1130 it("does not treat nested parentheses in the title as the outcome verb", () => {
1131 expect(
1132 extractXAiAcpSubagentEndNotice(
1133 'Background subagent "019f5470-bf92-7a90-afb3-5a6cea5b34a3" (general-purpose: "Check (backend) failed tests") is still running.',
1134 ),
1135 ).toBeUndefined();
1136 expect(
1137 extractXAiAcpSubagentEndNotice(
1138 'Background subagent "019f5470-bf92-7a90-afb3-5a6cea5b34a3" (general-purpose: "Check (backend) failed tests") completed successfully.',
1139 ),
1140 ).toEqual({
1141 childSessionId: "019f5470-bf92-7a90-afb3-5a6cea5b34a3",
1142 status: "completed",
1143 });
1144 });
1145
1146 it("extracts questions from the real xAI ask_user_question payload shape", () => {
1147 const questions = extractXAiAskUserQuestions({
1148 sessionId: "session-1",
1149 toolCallId: "tool-call-1",
1150 mode: "default",
1151 questions: [
1152 {
1153 id: "scope",
1154 question: "Which scope should Grok use?",
1155 options: [
1156 { label: "Workspace", description: "Use the current workspace" },
1157 { label: "Session", description: "Only use this session" },
1158 ],
1159 },
1160 ],
1161 });
1162
1163 expect(questions).toEqual([
1164 {
1165 id: "scope",
1166 header: "Question",
1167 question: "Which scope should Grok use?",
1168 multiSelect: false,
1169 options: [
1170 { label: "Workspace", description: "Use the current workspace" },
1171 { label: "Session", description: "Only use this session" },
1172 ],
1173 },
1174 ]);
1175 });
1176
1177 it("extracts questions from wrapped _x.ai extension payloads", () => {
1178 const payload = {
1179 method: "_x.ai/ask_user_question",
1180 params: {
1181 sessionId: "session-1",
1182 toolCallId: "tool-call-1",
1183 mode: "plan",
1184 questions: [
1185 {
1186 question: "Which changes should be included?",
1187 multiSelect: true,
1188 options: [{ label: "Tests" }, { label: "Docs" }],
1189 },
1190 ],
1191 },
1192 };
1193 const decoded = decodeXAiAskUserQuestionRequest(payload);
1194 const questions = extractXAiAskUserQuestions(decoded);
1195
1196 expect(questions).toEqual([
1197 {
1198 id: "Which changes should be included?",
1199 header: "Question",
1200 question: "Which changes should be included?",
1201 multiSelect: true,
1202 options: [
1203 { label: "Tests", description: "Tests" },
1204 { label: "Docs", description: "Docs" },
1205 ],
1206 },
1207 ]);
1208 });
1209
1210 it("treats nullable multiSelect from Grok as single-select", () => {
1211 const questions = extractXAiAskUserQuestions({
1212 sessionId: "session-1",
1213 toolCallId: "tool-call-1",
1214 mode: "default",
1215 questions: [
1216 {
1217 question: "Which label should Grok use?",
1218 multiSelect: null,
1219 options: [
1220 { label: "Alpha", description: "Use the Alpha label" },
1221 { label: "Beta", description: "Use the Beta label" },
1222 { label: "Other", description: "Use the Other label" },
1223 ],
1224 },
1225 ],
1226 });
1227
1228 expect(questions).toEqual([
1229 {
1230 id: "Which label should Grok use?",
1231 header: "Question",
1232 question: "Which label should Grok use?",
1233 multiSelect: false,
1234 options: [
1235 { label: "Alpha", description: "Use the Alpha label" },
1236 { label: "Beta", description: "Use the Beta label" },
1237 { label: "Other", description: "Use the Other label" },
1238 ],
1239 },
1240 ]);
1241 });
1242
1243 it("maps UI question ids back to xAI question text in accepted responses", () => {
1244 const response = makeXAiAskUserQuestionResponse(
1245 {
1246 sessionId: "session-1",
1247 toolCallId: "tool-call-1",
1248 mode: "default",
1249 questions: [
1250 {
1251 id: "scope",
1252 question: "Which scope should Grok use?",
1253 options: [
1254 { label: "workspace", description: "Use the current workspace" },
1255 { label: "session", description: "Only use this session" },
1256 ],
1257 },
1258 ],
1259 },
1260 { scope: "workspace" },
1261 );
1262
1263 expect(response).toEqual({
1264 outcome: "accepted",
1265 answers: {
1266 "Which scope should Grok use?": ["workspace"],
1267 },
1268 });
1269 });
1270
1271 it("orders accepted answers by the original xAI question order", () => {
1272 const response = makeXAiAskUserQuestionResponse(
1273 {
1274 sessionId: "session-1",
1275 toolCallId: "tool-call-1",
1276 mode: "default",
1277 questions: [
1278 {
1279 id: "first",
1280 question: "First question?",
1281 options: [{ label: "A", description: "A" }],
1282 },
1283 {
1284 id: "second",
1285 question: "Second question?",
1286 options: [{ label: "B", description: "B" }],
1287 },
1288 ],
1289 },
1290 {
1291 second: "B",
1292 first: "A",
1293 },
1294 );
1295
1296 expect(Object.keys(response.answers)).toEqual(["First question?", "Second question?"]);
1297 expect(response).toMatchObject({
1298 outcome: "accepted",
1299 answers: {
1300 "First question?": ["A"],
1301 "Second question?": ["B"],
1302 },
1303 });
1304 });
1305
1306 it("encodes typed custom answers as xAI Other annotations", () => {
1307 const response = makeXAiAskUserQuestionResponse(
1308 {
1309 method: "x.ai/ask_user_question",
1310 params: {
1311 sessionId: "session-1",
1312 toolCallId: "tool-call-1",
1313 mode: "default",
1314 questions: [
1315 {
1316 question: "Which ice cream flavor?",
1317 options: [
1318 { label: "vanilla", description: "Vanilla flavor" },
1319 { label: "chocolate", description: "Chocolate flavor" },
1320 ],
1321 },
1322 ],
1323 },
1324 },
1325 { "Which ice cream flavor?": "pistachio" },
1326 );
1327
1328 expect(response).toEqual({
1329 outcome: "accepted",
1330 answers: {
1331 "Which ice cream flavor?": ["Other"],
1332 },
1333 annotations: {
1334 "Which ice cream flavor?": {
1335 notes: "pistachio",
1336 },
1337 },
1338 });
1339 });
1340
1341 it("does not echo preview annotations for multi-select answers", () => {
1342 const response = makeXAiAskUserQuestionResponse(
1343 {
1344 sessionId: "session-1",
1345 toolCallId: "tool-call-1",
1346 mode: "default",
1347 questions: [
1348 {
1349 question: "Which files should Grok touch?",
1350 multiSelect: true,
1351 options: [
1352 {
1353 label: "Tests",
1354 description: "Update tests",
1355 preview: "test preview",
1356 },
1357 {
1358 label: "Docs",
1359 description: "Update docs",
1360 preview: "docs preview",
1361 },
1362 ],
1363 },
1364 ],
1365 },
1366 { "Which files should Grok touch?": ["Tests", "Docs"] },
1367 );
1368
1369 expect(response).toEqual({
1370 outcome: "accepted",
1371 answers: {
1372 "Which files should Grok touch?": ["Tests", "Docs"],
1373 },
1374 });
1375 });
1376
1377 it.effect("settles a hung prompt from a root-session prompt_complete notification", () =>
1378 Effect.gen(function* () {
1379 const handlers = new Map<string, (notification: unknown) => Effect.Effect<void>>();
1380 const hungPrompt = yield* Deferred.make<never>();
1381 const baseRuntime = {
1382 start: () =>
1383 Effect.succeed({
1384 sessionId: "root-session",
1385 initializeResult: {},
1386 sessionSetupResult: {},
1387 modelConfigId: undefined,
1388 }),
1389 prompt: () => Deferred.await(hungPrompt),
1390 cancel: Effect.void,
1391 handleExtNotification: (
1392 method: string,
1393 _schema: unknown,
1394 handler: (notification: unknown) => Effect.Effect<void>,
1395 ) => {
1396 handlers.set(method, handler);
1397 return Effect.void;
1398 },
1399 handleExtRequest: () => Effect.void,
1400 } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
1401
1402 const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
1403 const promptFiber = yield* runtime
1404 .prompt({ prompt: [{ type: "text", text: "hi" }] })
1405 .pipe(Effect.forkChild);
1406 yield* Effect.yieldNow;
1407 const promptCompleteHandler = handlers.get("x.ai/session/prompt_complete");
1408 expect(promptCompleteHandler).toBeDefined();
1409 yield* promptCompleteHandler!({
1410 sessionId: "root-session",
1411 stopReason: "end_turn",
1412 });
1413 const response = yield* Fiber.join(promptFiber);
1414 expect(response.stopReason).toBe("end_turn");
1415 expect(handlers.has("_x.ai/session/prompt_complete")).toBe(true);
1416 }),
1417 );
1418
1419 it.effect("fails a hung prompt from an xAI rate-limit completion (#8358)", () =>
1420 Effect.gen(function* () {
1421 const handlers = new Map<string, (notification: unknown) => Effect.Effect<void>>();
1422 const hungPrompt = yield* Deferred.make<never>();
1423 const baseRuntime = {
1424 start: () =>
1425 Effect.succeed({
1426 sessionId: "root-session",
1427 initializeResult: {},
1428 sessionSetupResult: {},
1429 modelConfigId: undefined,
1430 }),
1431 prompt: () => Deferred.await(hungPrompt),
1432 cancel: Effect.void,
1433 handleExtNotification: (
1434 method: string,
1435 _schema: unknown,
1436 handler: (notification: unknown) => Effect.Effect<void>,
1437 ) => {
1438 handlers.set(method, handler);
1439 return Effect.void;
1440 },
1441 handleExtRequest: () => Effect.void,
1442 } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
1443
1444 const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
1445 const promptFiber = yield* runtime
1446 .prompt({ prompt: [{ type: "text", text: "hi" }] })
1447 .pipe(Effect.forkChild);
1448 yield* Effect.yieldNow;
1449
1450 const promptCompleteHandler = handlers.get("x.ai/session/prompt_complete");
1451 expect(promptCompleteHandler).toBeDefined();
1452 yield* promptCompleteHandler!({
1453 sessionId: "root-session",
1454 stopReason: "rate_limit",
1455 });
1456 const error = yield* Fiber.join(promptFiber).pipe(Effect.flip);
1457 expect(error).toMatchObject({
1458 _tag: "AcpRequestError",
1459 code: -32003,
1460 errorMessage: "Grok usage limit reached. Try again later.",
1461 });
1462 }),
1463 );
1464
1465 it("maps subagent_finished to a notice only for statuses Grok defines", () => {
1466 // Recorded from Grok 1.0.41 (ids normalized); statuses per grok-build
1467 // xai-grok-tools/.../task/types.rs `status()`.
1468 const finished = (update: Record<string, unknown>) =>
1469 xAiSubagentFinishedNotice({
1470 sessionId: "root-session",
1471 update: {
1472 sessionUpdate: "subagent_finished",
1473 subagent_id: "child-session",
1474 child_session_id: "child-session",
1475 tool_calls: 2,
1476 turns: 1,
1477 duration_ms: 23734,
1478 ...update,
1479 } as never,
1480 });
1481 expect(finished({ status: "completed", output: "SUBAGENT_DONE", will_wake: true })).toEqual({
1482 sessionId: "root-session",
1483 childSessionId: "child-session",
1484 status: "completed",
1485 result: "SUBAGENT_DONE",
1486 });
1487 expect(finished({ status: "failed", error: "tool crashed" })).toMatchObject({
1488 status: "failed",
1489 result: "tool crashed",
1490 });
1491 expect(
1492 finished({ status: "cancelled", error: "interrupted by process restart" }),
1493 ).toMatchObject({ status: "cancelled", result: "interrupted by process restart" });
1494 // A failed subagent's error is its result; `output` only accompanies success.
1495 expect(finished({ status: "failed", output: "partial" })).toMatchObject({ result: null });
1496 expect(finished({ status: "timed_out" })).toBeNull();
1497 expect(finished({})).toBeNull();
1498 expect(finished({ status: "completed", child_session_id: undefined })).toBeNull();
1499 });
1500
1501 it.effect("settles prompts and finishes subagents from the same session notifications", () =>
1502 Effect.gen(function* () {
1503 const handlers = new Map<string, (notification: unknown) => Effect.Effect<void>>();
1504 let capturedMeta: Record<string, unknown> | null | undefined;
1505 const hungPrompt = yield* Deferred.make<never>();
1506 const baseRuntime = {
1507 start: () =>
1508 Effect.succeed({
1509 sessionId: "root-session",
1510 initializeResult: {},
1511 sessionSetupResult: {},
1512 modelConfigId: undefined,
1513 }),
1514 prompt: (payload: { readonly _meta?: Record<string, unknown> | null }) => {
1515 capturedMeta = payload._meta ?? null;
1516 return Deferred.await(hungPrompt);
1517 },
1518 cancel: Effect.void,
1519 handleExtNotification: (
1520 method: string,
1521 _schema: unknown,
1522 handler: (notification: unknown) => Effect.Effect<void>,
1523 ) => {
1524 handlers.set(method, handler);
1525 return Effect.void;
1526 },
1527 } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
1528 const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
1529 const notices: Array<unknown> = [];
1530 // Registered after the wrapper, as the adapter does: it must not replace
1531 // the wrapper's prompt completion on the shared method.
1532 yield* registerXAiSubagentFinished(runtime, (notice) =>
1533 Effect.sync(() => notices.push(notice)),
1534 );
1535 const sessionNotification = handlers.get("_x.ai/session_notification")!;
1536 yield* sessionNotification({
1537 sessionId: "root-session",
1538 update: {
1539 sessionUpdate: "subagent_finished",
1540 child_session_id: "child-session",
1541 status: "completed",
1542 output: "SUBAGENT_DONE",
1543 },
1544 });
1545 expect(notices).toEqual([
1546 {
1547 sessionId: "root-session",
1548 childSessionId: "child-session",
1549 status: "completed",
1550 result: "SUBAGENT_DONE",
1551 },
1552 ]);
1553
1554 const promptFiber = yield* runtime
1555 .prompt({ prompt: [{ type: "text", text: "hi" }] })
1556 .pipe(Effect.forkChild);
1557 yield* Effect.yieldNow;
1558 yield* sessionNotification({
1559 sessionId: "root-session",
1560 update: {
1561 sessionUpdate: "turn_completed",
1562 prompt_id: capturedMeta?.promptId,
1563 stop_reason: "end_turn",
1564 },
1565 });
1566 const response = yield* Fiber.join(promptFiber);
1567 expect(response.stopReason).toBe("end_turn");
1568 expect(notices).toHaveLength(1);
1569 }),
1570 );
1571
1572 it.effect("settles a hung prompt from _x.ai/session/update turn_completed", () =>
1573 Effect.gen(function* () {
1574 const handlers = new Map<string, (notification: unknown) => Effect.Effect<void>>();
1575 let capturedMeta: Record<string, unknown> | null | undefined;
1576 const hungPrompt = yield* Deferred.make<never>();
1577 const baseRuntime = {
1578 start: () =>
1579 Effect.succeed({
1580 sessionId: "root-session",
1581 initializeResult: {},
1582 sessionSetupResult: {},
1583 modelConfigId: undefined,
1584 }),
1585 prompt: (payload: { readonly _meta?: Record<string, unknown> | null }) => {
1586 capturedMeta = payload._meta ?? null;
1587 return Deferred.await(hungPrompt);
1588 },
1589 cancel: Effect.void,
1590 handleExtNotification: (
1591 method: string,
1592 _schema: unknown,
1593 handler: (notification: unknown) => Effect.Effect<void>,
1594 ) => {
1595 handlers.set(method, handler);
1596 return Effect.void;
1597 },
1598 handleExtRequest: () => Effect.void,
1599 } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
1600
1601 const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
1602 const promptFiber = yield* runtime
1603 .prompt({ prompt: [{ type: "text", text: "hi" }] })
1604 .pipe(Effect.forkChild);
1605 yield* Effect.yieldNow;
1606 const promptId = capturedMeta?.promptId;
1607 expect(typeof promptId).toBe("string");
1608 const sessionUpdateHandler = handlers.get("_x.ai/session/update");
1609 expect(sessionUpdateHandler).toBeDefined();
1610 yield* sessionUpdateHandler!({
1611 sessionId: "root-session",
1612 update: {
1613 sessionUpdate: "turn_completed",
1614 prompt_id: promptId,
1615 stop_reason: "end_turn",
1616 },
1617 });
1618 const response = yield* Fiber.join(promptFiber);
1619 expect(response.stopReason).toBe("end_turn");
1620 }),
1621 );
1622
1623 it.effect("settles a hung prompt from released _x.ai/session_notification turns", () =>
1624 Effect.gen(function* () {
1625 const handlers = new Map<string, (notification: unknown) => Effect.Effect<void>>();
1626 const hungPrompt = yield* Deferred.make<never>();
1627 const baseRuntime = {
1628 start: () =>
1629 Effect.succeed({
1630 sessionId: "root-session",
1631 initializeResult: {},
1632 sessionSetupResult: {},
1633 modelConfigId: undefined,
1634 }),
1635 prompt: () => Deferred.await(hungPrompt),
1636 cancel: Effect.void,
1637 handleExtNotification: (
1638 method: string,
1639 _schema: unknown,
1640 handler: (notification: unknown) => Effect.Effect<void>,
1641 ) => {
1642 handlers.set(method, handler);
1643 return Effect.void;
1644 },
1645 handleExtRequest: () => Effect.void,
1646 } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
1647
1648 const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
1649 const promptFiber = yield* runtime
1650 .prompt({ prompt: [{ type: "text", text: "hi" }] })
1651 .pipe(Effect.forkChild);
1652 yield* Effect.yieldNow;
1653 const sessionNotificationHandler = handlers.get("_x.ai/session_notification");
1654 expect(sessionNotificationHandler).toBeDefined();
1655 yield* sessionNotificationHandler!({
1656 sessionId: "root-session",
1657 update: {
1658 sessionUpdate: "turn_completed",
1659 prompt_id: "t3-xai-prompt-1",
1660 stop_reason: "end_turn",
1661 },
1662 });
1663 const response = yield* Fiber.join(promptFiber);
1664 expect(response.stopReason).toBe("end_turn");
1665 expect(handlers.has("_x.ai/session/update")).toBe(true);
1666 }),
1667 );
1668
1669 it.effect("ignores turn_completed for non-pending prompt ids and task completions", () =>
1670 Effect.gen(function* () {
1671 const handlers = new Map<string, (notification: unknown) => Effect.Effect<void>>();
1672 const hungPrompt = yield* Deferred.make<never>();
1673 const baseRuntime = {
1674 start: () =>
1675 Effect.succeed({
1676 sessionId: "root-session",
1677 initializeResult: {},
1678 sessionSetupResult: {},
1679 modelConfigId: undefined,
1680 }),
1681 prompt: () => Deferred.await(hungPrompt),
1682 cancel: Effect.void,
1683 handleExtNotification: (
1684 method: string,
1685 _schema: unknown,
1686 handler: (notification: unknown) => Effect.Effect<void>,
1687 ) => {
1688 handlers.set(method, handler);
1689 return Effect.void;
1690 },
1691 handleExtRequest: () => Effect.void,
1692 } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
1693
1694 const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
1695 const promptFiber = yield* runtime
1696 .prompt({ prompt: [{ type: "text", text: "hi" }] })
1697 .pipe(Effect.forkChild);
1698 yield* Effect.yieldNow;
1699 const sessionUpdateHandler = handlers.get("_x.ai/session/update");
1700 expect(sessionUpdateHandler).toBeDefined();
1701 yield* sessionUpdateHandler!({
1702 sessionId: "root-session",
1703 update: {
1704 sessionUpdate: "turn_completed",
1705 prompt_id: "task-completed-call-abc",
1706 stop_reason: "end_turn",
1707 },
1708 });
1709 yield* sessionUpdateHandler!({
1710 sessionId: "root-session",
1711 update: {
1712 sessionUpdate: "turn_completed",
1713 prompt_id: "some-other-cli-turn",
1714 stop_reason: "end_turn",
1715 },
1716 });
1717 yield* Effect.yieldNow;
1718 expect(promptFiber.pollUnsafe()).toBeUndefined();
1719 yield* Fiber.interrupt(promptFiber);
1720 }),
1721 );
1722
1723 it.effect("ignores prompt_complete notifications for foreign session ids", () =>
1724 Effect.gen(function* () {
1725 const handlers = new Map<string, (notification: unknown) => Effect.Effect<void>>();
1726 const hungPrompt = yield* Deferred.make<never>();
1727 const baseRuntime = {
1728 start: () =>
1729 Effect.succeed({
1730 sessionId: "root-session",
1731 initializeResult: {},
1732 sessionSetupResult: {},
1733 modelConfigId: undefined,
1734 }),
1735 prompt: () => Deferred.await(hungPrompt),
1736 cancel: Effect.void,
1737 handleExtNotification: (
1738 method: string,
1739 _schema: unknown,
1740 handler: (notification: unknown) => Effect.Effect<void>,
1741 ) => {
1742 handlers.set(method, handler);
1743 return Effect.void;
1744 },
1745 handleExtRequest: () => Effect.void,
1746 } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
1747
1748 const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
1749 const promptFiber = yield* runtime
1750 .prompt({ prompt: [{ type: "text", text: "hi" }] })
1751 .pipe(Effect.forkChild);
1752 yield* Effect.yieldNow;
1753 const promptCompleteHandler = handlers.get("_x.ai/session/prompt_complete");
1754 expect(promptCompleteHandler).toBeDefined();
1755 yield* promptCompleteHandler!({
1756 sessionId: "child-session",
1757 });
1758 yield* Effect.yieldNow;
1759 expect(promptFiber.pollUnsafe()).toBeUndefined();
1760 yield* Fiber.interrupt(promptFiber);
1761 }),
1762 );
1763
1764 it.effect(
1765 "ignores promptId-less prompt_complete when multiple prompts are pending on the same session",
1766 () =>
1767 Effect.gen(function* () {
1768 const handlers = new Map<string, (notification: unknown) => Effect.Effect<void>>();
1769 const hungPrompt = yield* Deferred.make<never>();
1770 const baseRuntime = {
1771 start: () =>
1772 Effect.succeed({
1773 sessionId: "root-session",
1774 initializeResult: {},
1775 sessionSetupResult: {},
1776 modelConfigId: undefined,
1777 }),
1778 prompt: () => Deferred.await(hungPrompt),
1779 cancel: Effect.void,
1780 handleExtNotification: (
1781 method: string,
1782 _schema: unknown,
1783 handler: (notification: unknown) => Effect.Effect<void>,
1784 ) => {
1785 handlers.set(method, handler);
1786 return Effect.void;
1787 },
1788 handleExtRequest: () => Effect.void,
1789 } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
1790
1791 const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
1792 const firstPromptFiber = yield* runtime
1793 .prompt({ prompt: [{ type: "text", text: "first" }] })
1794 .pipe(Effect.forkChild);
1795 yield* Effect.yieldNow;
1796 const secondPromptFiber = yield* runtime
1797 .prompt({ prompt: [{ type: "text", text: "second" }] })
1798 .pipe(Effect.forkChild);
1799 yield* Effect.yieldNow;
1800 const promptCompleteHandler = handlers.get("_x.ai/session/prompt_complete");
1801 expect(promptCompleteHandler).toBeDefined();
1802 yield* promptCompleteHandler!({
1803 sessionId: "root-session",
1804 stopReason: "end_turn",
1805 });
1806 yield* Effect.yieldNow;
1807 expect(firstPromptFiber.pollUnsafe()).toBeUndefined();
1808 expect(secondPromptFiber.pollUnsafe()).toBeUndefined();
1809 yield* Fiber.interrupt(firstPromptFiber);
1810 yield* Fiber.interrupt(secondPromptFiber);
1811 }),
1812 );
1813
1814 it.effect("injects promptId and requestId into prompt _meta", () =>
1815 Effect.gen(function* () {
1816 let capturedMeta: Record<string, unknown> | null | undefined;
1817 const baseRuntime = {
1818 start: () => Effect.succeed({ sessionId: "session-1" }),
1819 prompt: (payload: { readonly _meta?: Record<string, unknown> | null }) => {
1820 capturedMeta = payload._meta ?? null;
1821 return Effect.succeed({ stopReason: "end_turn" as const });
1822 },
1823 cancel: Effect.void,
1824 handleExtNotification: () => Effect.void,
1825 handleExtRequest: () => Effect.void,
1826 } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
1827
1828 const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
1829 yield* runtime.prompt({ prompt: [{ type: "text", text: "hi" }] });
1830
1831 expect(typeof capturedMeta?.promptId).toBe("string");
1832 expect(capturedMeta).toMatchObject({
1833 promptId: capturedMeta?.promptId,
1834 requestId: capturedMeta?.promptId,
1835 });
1836 }),
1837 );
1838
1839 it.effect("cancels pending completions without restarting a stalled runtime", () =>
1840 Effect.gen(function* () {
1841 const hungStart = yield* Deferred.make<never>();
1842 const hungPrompt = yield* Deferred.make<never>();
1843 let startCount = 0;
1844 let cancelCalled = false;
1845 const baseRuntime = {
1846 start: () => {
1847 startCount += 1;
1848 return startCount === 1
1849 ? Effect.succeed({
1850 sessionId: "root-session",
1851 initializeResult: {},
1852 sessionSetupResult: {},
1853 modelConfigId: undefined,
1854 })
1855 : Deferred.await(hungStart);
1856 },
1857 prompt: () => Deferred.await(hungPrompt),
1858 cancel: Effect.sync(() => {
1859 cancelCalled = true;
1860 }),
1861 handleExtNotification: () => Effect.void,
1862 handleExtRequest: () => Effect.void,
1863 } as unknown as AcpSessionRuntime.AcpSessionRuntime["Service"];
1864
1865 const runtime = yield* makeXAiPromptCompletionRuntime(baseRuntime);
1866 const promptFiber = yield* runtime
1867 .prompt({ prompt: [{ type: "text", text: "hi" }] })
1868 .pipe(Effect.forkChild);
1869 yield* Effect.yieldNow;
1870
1871 yield* runtime.cancel;
1872
1873 expect(cancelCalled).toBe(true);
1874 expect(startCount).toBe(1);
1875 expect((yield* Fiber.join(promptFiber)).stopReason).toBe("cancelled");
1876 }),
1877 );
1878});
1879
1880describe("Grok exit_plan_mode capture (#8358)", () => {
1881 it("extracts plan markdown from exit_plan_mode payloads", () => {
1882 const decode = Schema.decodeUnknownSync(XAiExitPlanModeRequest);
1883 const direct = decode({
1884 sessionId: "session-1",
1885 toolCallId: "exit-1",
1886 planContent: "# Plan\n\n- do the thing\n",
1887 });
1888 expect(extractXAiExitPlanMarkdown(direct)).toBe("# Plan\n\n- do the thing");
1889
1890 const wrapped = decode({
1891 method: "_x.ai/exit_plan_mode",
1892 params: {
1893 sessionId: "session-1",
1894 toolCallId: "exit-1",
1895 planContent: null,
1896 },
1897 });
1898 expect(extractXAiExitPlanMarkdown(wrapped, " # fallback plan ")).toBe("# fallback plan");
1899 expect(extractXAiExitPlanMarkdown(wrapped, "")).toBe(XAI_EMPTY_PLAN_MARKDOWN);
1900 expect(extractXAiExitPlanMarkdown(wrapped)).toBe(XAI_EMPTY_PLAN_MARKDOWN);
1901 });
1902 it("identifies Grok plan.md paths and extracts markdown from tool call data", () => {
1903 const home = "/home/ada";
1904 const linuxHost = { platform: "linux" as const, environment: {}, homeDirectory: home };
1905 const windowsHost = { platform: "win32" as const, environment: {}, homeDirectory: home };
1906 const grokHomeHost = {
1907 platform: "linux" as const,
1908 environment: { GROK_HOME: "/opt/grok-data" },
1909 homeDirectory: home,
1910 };
1911 const sessionPlan = `${home}/.grok/sessions/abc/plan.md`;
1912 const nestedSessionPlan = `${home}/.grok/sessions/%2Fhome%2Fproj/019fd20e-c563-70a0-b801-a6bc51815a9b/plan.md`;
1913 expect(isGrokPlanMarkdownPath(sessionPlan, linuxHost)).toBe(true);
1914 expect(isGrokPlanMarkdownPath(nestedSessionPlan, linuxHost)).toBe(true);
1915 expect(isGrokPlanMarkdownPath("~/.grok/sessions/abc/plan.md", linuxHost)).toBe(true);
1916 expect(isGrokPlanMarkdownPath("/tmp/mock-home/.grok/sessions/sess/plan.md", linuxHost)).toBe(
1917 true,
1918 );
1919 expect(isGrokPlanMarkdownPath("/home/other/.grok/sessions/sess/plan.md", linuxHost)).toBe(true);
1920 expect(isGrokPlanMarkdownPath("/HOME/other/.grok/sessions/sess/plan.md", linuxHost)).toBe(
1921 false,
1922 );
1923 expect(isGrokPlanMarkdownPath("C:/Users/other/.grok/sessions/id/plan.md", windowsHost)).toBe(
1924 true,
1925 );
1926 expect(isGrokPlanMarkdownPath("c:/users/OTHER/.GROK/SESSIONS/id/PLAN.MD", windowsHost)).toBe(
1927 true,
1928 );
1929 expect(
1930 isGrokPlanMarkdownPath("C:\\Users\\other\\.grok\\sessions\\id\\plan.md", windowsHost),
1931 ).toBe(true);
1932 expect(isGrokPlanMarkdownPath("/opt/grok-data/sessions/sess/plan.md", grokHomeHost)).toBe(true);
1933 expect(
1934 isGrokPlanMarkdownPath("/OPT/GROK-DATA/sessions/sess/plan.md", {
1935 platform: "win32",
1936 environment: { GROK_HOME: "/opt/grok-data" },
1937 homeDirectory: home,
1938 }),
1939 ).toBe(true);
1940 expect(isGrokPlanMarkdownPath("/OPT/GROK-DATA/sessions/sess/plan.md", grokHomeHost)).toBe(
1941 false,
1942 );
1943 // Workspace plan.md must not be treated as the session plan file.
1944 expect(isGrokPlanMarkdownPath("plan.md", linuxHost)).toBe(false);
1945 expect(isGrokPlanMarkdownPath("/repo/docs/plan.md", linuxHost)).toBe(false);
1946 expect(isGrokPlanMarkdownPath("/tmp/other.md", linuxHost)).toBe(false);
1947 expect(isGrokPlanMarkdownPath("/repo/.grok/sessions/example/plan.md", linuxHost)).toBe(false);
1948 expect(
1949 isGrokPlanMarkdownPath(`${home}/project/.grok/sessions/example/plan.md`, linuxHost),
1950 ).toBe(false);
1951 expect(
1952 isGrokPlanMarkdownPath("/home/other/.grok/sessions/../../project/plan.md", linuxHost),
1953 ).toBe(false);
1954 expect(
1955 isGrokPlanMarkdownPath("/home/other/.grok/sessions/foo/../../../project/plan.md", linuxHost),
1956 ).toBe(false);
1957
1958 expect(
1959 extractGrokPlanMarkdownFromToolCallData(
1960 {
1961 rawInput: {
1962 file_path: sessionPlan,
1963 content: "# From rawInput\n\n- a\n",
1964 },
1965 },
1966 linuxHost,
1967 ),
1968 ).toBe("# From rawInput\n\n- a");
1969
1970 expect(
1971 extractGrokPlanMarkdownFromToolCallData(
1972 {
1973 content: [
1974 {
1975 type: "diff",
1976 path: sessionPlan,
1977 oldText: "",
1978 newText: "# From diff\n\n- b\n",
1979 },
1980 ],
1981 },
1982 linuxHost,
1983 ),
1984 ).toBe("# From diff\n\n- b");
1985
1986 expect(
1987 extractGrokPlanMarkdownFromToolCallData(
1988 {
1989 rawInput: { file_path: sessionPlan, content: "" },
1990 content: [
1991 {
1992 type: "diff",
1993 path: sessionPlan,
1994 oldText: "",
1995 newText: "# From diff after empty rawInput\n",
1996 },
1997 ],
1998 },
1999 linuxHost,
2000 ),
2001 ).toBe("# From diff after empty rawInput");
2002
2003 expect(
2004 extractGrokPlanMarkdownFromToolCallData(
2005 {
2006 rawInput: { file_path: sessionPlan, content: "" },
2007 },
2008 linuxHost,
2009 ),
2010 ).toBe("");
2011
2012 expect(
2013 extractGrokPlanMarkdownFromToolCallData(
2014 {
2015 content: [{ type: "diff", path: sessionPlan, oldText: "# old", newText: "" }],
2016 },
2017 linuxHost,
2018 ),
2019 ).toBe("");
2020
2021 expect(
2022 extractGrokPlanMarkdownFromToolCallData(
2023 {
2024 rawInput: { file_path: "/tmp/readme.md", content: "nope" },
2025 },
2026 linuxHost,
2027 ),
2028 ).toBeUndefined();
2029
2030 expect(
2031 extractGrokPlanMarkdownFromToolCallData(
2032 {
2033 rawInput: { file_path: "/repo/docs/plan.md", content: "# Project plan\n" },
2034 },
2035 linuxHost,
2036 ),
2037 ).toBeUndefined();
2038 });
2039});