1import { describe, expect, it } from "vite-plus/test";
2
3import type * as EffectAcpSchema from "effect-acp/compat";
4
5import {
6 decideToolCallUpdateEmission,
7 applyAcpAgentTerminalUpdate,
8 acpContentBlockDisplayText,
9 embeddedTerminalIdsFromSessionUpdate,
10 extractMcpToolCallIdentity,
11 extractModelConfigId,
12 mergeToolCallState,
13 parsePermissionRequest,
14 parseSessionModeState,
15 parseSessionUpdateEvent,
16 sessionModelStateFromInitialize,
17 sessionUpdateCountsAsLoadReplayActivity,
18 sessionUpdateIsReplay,
19 syntheticLoadSessionResponseFromInitialize,
20 toolCallProgressLength,
21 type AcpToolCallState,
22} from "./runtimeModel.ts";
23
24describe("AcpRuntimeModel", () => {
25 it("parses session mode state from typed ACP session setup responses", () => {
26 const modeState = parseSessionModeState({
27 sessionId: "session-1",
28 modes: {
29 currentModeId: " code ",
30 availableModes: [
31 { id: " ask ", name: " Ask ", description: " Request approval " },
32 { id: " code ", name: " Code " },
33 ],
34 },
35 configOptions: [],
36 } satisfies EffectAcpSchema.NewSessionResponse);
37
38 expect(modeState).toEqual({
39 currentModeId: "code",
40 availableModes: [
41 { id: "ask", name: "Ask", description: "Request approval" },
42 { id: "code", name: "Code" },
43 ],
44 });
45 });
46
47 it("extracts the model config id from typed ACP config options", () => {
48 const modelConfigId = extractModelConfigId({
49 sessionId: "session-1",
50 configOptions: [
51 {
52 id: "approval",
53 name: "Approval Mode",
54 category: "permission",
55 type: "select",
56 currentValue: "ask",
57 options: [{ value: "ask", name: "Ask" }],
58 },
59 {
60 id: "model",
61 name: "Model",
62 category: "model",
63 type: "select",
64 currentValue: "default",
65 options: [{ value: "default", name: "Auto" }],
66 },
67 ],
68 } satisfies EffectAcpSchema.NewSessionResponse);
69
70 expect(modelConfigId).toBe("model");
71 });
72
73 it("detects Grok session replay updates from _meta.isReplay", () => {
74 expect(
75 sessionUpdateIsReplay({
76 _meta: { isReplay: true },
77 sessionId: "session-1",
78 update: {
79 sessionUpdate: "agent_message_chunk",
80 content: { type: "text", text: "replayed" },
81 },
82 } satisfies EffectAcpSchema.SessionNotification),
83 ).toBe(true);
84 expect(
85 sessionUpdateIsReplay({
86 sessionId: "session-1",
87 update: {
88 sessionUpdate: "agent_message_chunk",
89 content: { type: "text", text: "live" },
90 },
91 } satisfies EffectAcpSchema.SessionNotification),
92 ).toBe(false);
93 });
94
95 it("ignores Grok keepalive chunks when tracking session/load replay activity", () => {
96 expect(
97 sessionUpdateCountsAsLoadReplayActivity({
98 sessionId: "session-1",
99 update: {
100 sessionUpdate: "agent_message_chunk",
101 content: { type: "text", text: "" },
102 },
103 } satisfies EffectAcpSchema.SessionNotification),
104 ).toBe(false);
105 expect(
106 sessionUpdateCountsAsLoadReplayActivity({
107 _meta: { isReplay: true },
108 sessionId: "session-1",
109 update: {
110 sessionUpdate: "tool_call",
111 toolCallId: "replay-tool",
112 title: "Replay",
113 kind: "search",
114 status: "completed",
115 },
116 } satisfies EffectAcpSchema.SessionNotification),
117 ).toBe(true);
118 });
119
120 it("ignores load-replay activity from other sessions while a load gate is active", () => {
121 expect(
122 sessionUpdateCountsAsLoadReplayActivity(
123 {
124 sessionId: "session-other",
125 update: {
126 sessionUpdate: "agent_message_chunk",
127 content: { type: "text", text: "unrelated" },
128 },
129 } satisfies EffectAcpSchema.SessionNotification,
130 "session-loading",
131 ),
132 ).toBe(false);
133 expect(
134 sessionUpdateCountsAsLoadReplayActivity(
135 {
136 sessionId: "session-loading",
137 update: {
138 sessionUpdate: "agent_message_chunk",
139 content: { type: "text", text: "replay" },
140 },
141 } satisfies EffectAcpSchema.SessionNotification,
142 "session-loading",
143 ),
144 ).toBe(true);
145 });
146
147 it("counts mode/config/session/usage updates as load-replay activity", () => {
148 expect(
149 sessionUpdateCountsAsLoadReplayActivity({
150 sessionId: "session-1",
151 update: {
152 sessionUpdate: "current_mode_update",
153 currentModeId: "code",
154 },
155 } satisfies EffectAcpSchema.SessionNotification),
156 ).toBe(true);
157 expect(
158 sessionUpdateCountsAsLoadReplayActivity({
159 sessionId: "session-1",
160 update: {
161 sessionUpdate: "config_option_update",
162 configOptions: [],
163 },
164 } satisfies EffectAcpSchema.SessionNotification),
165 ).toBe(true);
166 expect(
167 sessionUpdateCountsAsLoadReplayActivity({
168 sessionId: "session-1",
169 update: {
170 sessionUpdate: "session_info_update",
171 title: "Restored",
172 },
173 } satisfies EffectAcpSchema.SessionNotification),
174 ).toBe(true);
175 expect(
176 sessionUpdateCountsAsLoadReplayActivity({
177 sessionId: "session-1",
178 update: {
179 sessionUpdate: "usage_update",
180 used: 1,
181 size: 10,
182 },
183 } satisfies EffectAcpSchema.SessionNotification),
184 ).toBe(true);
185 });
186
187 it("ignores malformed initialize mode state in synthetic load responses", () => {
188 const response = syntheticLoadSessionResponseFromInitialize({
189 protocolVersion: 1,
190 _meta: {
191 modeState: {
192 currentModeId: "code",
193 availableModes: [{ id: "code", name: 12 }],
194 },
195 },
196 } as EffectAcpSchema.InitializeResponse);
197
198 expect(response.modes).toBeUndefined();
199 expect(response._meta).toMatchObject({ t3SessionLoadReady: "replay_idle" });
200 });
201
202 it("builds a synthetic load response with initialize mode state", () => {
203 const response = syntheticLoadSessionResponseFromInitialize({
204 protocolVersion: 1,
205 _meta: {
206 modeState: {
207 currentModeId: "code",
208 availableModes: [
209 { id: "ask", name: "Ask" },
210 { id: "code", name: "Code" },
211 ],
212 },
213 },
214 } satisfies EffectAcpSchema.InitializeResponse);
215
216 expect(response.modes?.currentModeId).toBe("code");
217 expect(response.modes?.availableModes).toHaveLength(2);
218 });
219
220 it("keeps initialize mode state whose modes have null descriptions", () => {
221 const response = syntheticLoadSessionResponseFromInitialize({
222 protocolVersion: 1,
223 _meta: {
224 modeState: {
225 currentModeId: "code",
226 availableModes: [
227 { id: "ask", name: "Ask", description: null },
228 { id: "code", name: "Code", description: "Edit files", _meta: null },
229 ],
230 _meta: null,
231 },
232 },
233 } satisfies EffectAcpSchema.InitializeResponse);
234
235 expect(parseSessionModeState(response)).toEqual({
236 currentModeId: "code",
237 availableModes: [
238 { id: "ask", name: "Ask" },
239 { id: "code", name: "Code", description: "Edit files" },
240 ],
241 });
242 });
243
244 it("reads initialize model state and ignores malformed model state", () => {
245 const modelState = {
246 currentModelId: "grok-4.7",
247 availableModels: [
248 {
249 modelId: "grok-4.7",
250 name: "Grok 4.7",
251 description: null,
252 _meta: { reasoningEfforts: [{ id: "high", default: true }] },
253 },
254 { modelId: "grok-4.6", name: "Grok 4.6" },
255 ],
256 };
257
258 expect(sessionModelStateFromInitialize({ protocolVersion: 1, _meta: { modelState } })).toEqual(
259 modelState,
260 );
261 expect(
262 sessionModelStateFromInitialize({
263 protocolVersion: 1,
264 _meta: {
265 modelState: { currentModelId: "grok-4.7", availableModels: [{ modelId: "grok-4.7" }] },
266 },
267 }),
268 ).toBeUndefined();
269 expect(
270 sessionModelStateFromInitialize({
271 protocolVersion: 1,
272 _meta: { modelState: { ...modelState, _meta: "not an object" } },
273 }),
274 ).toBeUndefined();
275 });
276
277 it("projects typed ACP tool call updates into runtime events", () => {
278 const created = parseSessionUpdateEvent({
279 sessionId: "session-1",
280 update: {
281 sessionUpdate: "tool_call",
282 toolCallId: "tool-1",
283 title: "Terminal",
284 kind: "execute",
285 status: "pending",
286 rawInput: {
287 executable: "bun",
288 args: ["run", "typecheck"],
289 },
290 content: [
291 {
292 type: "content",
293 content: {
294 type: "text",
295 text: "Running checks",
296 },
297 },
298 ],
299 },
300 } satisfies EffectAcpSchema.SessionNotification);
301
302 expect(created.events).toEqual([
303 {
304 _tag: "ToolCallUpdated",
305 toolCall: {
306 toolCallId: "tool-1",
307 kind: "execute",
308 title: "Ran command",
309 status: "pending",
310 command: "bun run typecheck",
311 detail: "bun run typecheck",
312 data: {
313 toolCallId: "tool-1",
314 kind: "execute",
315 title: "Terminal",
316 command: "bun run typecheck",
317 rawInput: {
318 executable: "bun",
319 args: ["run", "typecheck"],
320 },
321 content: [
322 {
323 type: "content",
324 content: {
325 type: "text",
326 text: "Running checks",
327 },
328 },
329 ],
330 },
331 },
332 rawPayload: {
333 sessionId: "session-1",
334 update: {
335 sessionUpdate: "tool_call",
336 toolCallId: "tool-1",
337 title: "Terminal",
338 kind: "execute",
339 status: "pending",
340 rawInput: {
341 executable: "bun",
342 args: ["run", "typecheck"],
343 },
344 content: [
345 {
346 type: "content",
347 content: {
348 type: "text",
349 text: "Running checks",
350 },
351 },
352 ],
353 },
354 },
355 },
356 ]);
357
358 const updated = parseSessionUpdateEvent({
359 sessionId: "session-1",
360 update: {
361 sessionUpdate: "tool_call_update",
362 toolCallId: "tool-1",
363 status: "completed",
364 rawOutput: { exitCode: 0 },
365 },
366 } satisfies EffectAcpSchema.SessionNotification);
367
368 expect(updated.events).toHaveLength(1);
369 expect(updated.events[0]?._tag).toBe("ToolCallUpdated");
370 const createdEvent = created.events[0];
371 const updatedEvent = updated.events[0];
372 if (createdEvent?._tag === "ToolCallUpdated" && updatedEvent?._tag === "ToolCallUpdated") {
373 expect(mergeToolCallState(createdEvent.toolCall, updatedEvent.toolCall)).toMatchObject({
374 toolCallId: "tool-1",
375 status: "completed",
376 title: "Ran command",
377 detail: "bun run typecheck",
378 command: "bun run typecheck",
379 });
380 }
381 });
382
383 it("clears prior ACP locations when an update explicitly replaces them", () => {
384 const toolCall = (locations?: EffectAcpSchema.ToolCallLocation[] | null) => {
385 const parsed = parseSessionUpdateEvent({
386 sessionId: "session-1",
387 update: {
388 sessionUpdate: "tool_call_update",
389 toolCallId: "read-1",
390 kind: "read",
391 ...(locations === undefined ? {} : { locations }),
392 },
393 } satisfies EffectAcpSchema.SessionNotification);
394 const event = parsed.events.find((candidate) => candidate._tag === "ToolCallUpdated");
395 if (!event || event._tag !== "ToolCallUpdated") {
396 throw new Error("expected a tool call update");
397 }
398 return event.toolCall;
399 };
400
401 const created = toolCall([{ path: "README" }]);
402 expect(mergeToolCallState(created, toolCall()).data.locations).toEqual([{ path: "README" }]);
403 const cleared = mergeToolCallState(created, toolCall([]));
404 expect(cleared.data.locations).toEqual([]);
405 expect(mergeToolCallState(created, toolCall(null)).data.locations).toEqual([]);
406 expect(
407 decideToolCallUpdateEmission({
408 previous: created,
409 next: cleared,
410 lastEmittedDetailLength: 0,
411 skippedSinceEmit: 0,
412 }).emit,
413 ).toBe(true);
414 });
415
416 it("trims padded current mode updates before emitting a mode change", () => {
417 const result = parseSessionUpdateEvent({
418 sessionId: "session-1",
419 update: {
420 sessionUpdate: "current_mode_update",
421 currentModeId: " code ",
422 },
423 } satisfies EffectAcpSchema.SessionNotification);
424
425 expect(result.modeId).toBe("code");
426 expect(result.events).toEqual([
427 {
428 _tag: "ModeChanged",
429 modeId: "code",
430 },
431 ]);
432 });
433
434 it("projects typed ACP plan and content updates", () => {
435 const planResult = parseSessionUpdateEvent({
436 sessionId: "session-1",
437 update: {
438 sessionUpdate: "plan",
439 entries: [
440 { content: " Inspect state ", priority: "high", status: "completed" },
441 { content: "", priority: "medium", status: "in_progress" },
442 ],
443 },
444 } satisfies EffectAcpSchema.SessionNotification);
445
446 expect(planResult.events).toEqual([
447 {
448 _tag: "PlanUpdated",
449 payload: {
450 nativePlanId: "legacy",
451 kind: "items",
452 plan: [
453 { step: "Inspect state", status: "completed" },
454 { step: "Step 2", status: "inProgress" },
455 ],
456 },
457 rawPayload: {
458 sessionId: "session-1",
459 update: {
460 sessionUpdate: "plan",
461 entries: [
462 { content: " Inspect state ", priority: "high", status: "completed" },
463 { content: "", priority: "medium", status: "in_progress" },
464 ],
465 },
466 },
467 },
468 ]);
469
470 const contentResult = parseSessionUpdateEvent({
471 sessionId: "session-1",
472 update: {
473 sessionUpdate: "agent_message_chunk",
474 content: {
475 type: "text",
476 text: "hello from acp",
477 },
478 },
479 } satisfies EffectAcpSchema.SessionNotification);
480
481 expect(contentResult.events).toEqual([
482 {
483 _tag: "ContentDelta",
484 text: "hello from acp",
485 rawPayload: {
486 sessionId: "session-1",
487 update: {
488 sessionUpdate: "agent_message_chunk",
489 content: {
490 type: "text",
491 text: "hello from acp",
492 },
493 },
494 },
495 },
496 ]);
497 });
498
499 it("keeps thought chunks separate from assistant text", () => {
500 const notification = {
501 sessionId: "session-1",
502 update: {
503 sessionUpdate: "agent_thought_chunk",
504 content: { type: "text", text: "Inspect the current implementation first." },
505 },
506 } satisfies EffectAcpSchema.SessionNotification;
507
508 expect(parseSessionUpdateEvent(notification).events).toEqual([
509 {
510 _tag: "ThoughtDelta",
511 text: "Inspect the current implementation first.",
512 rawPayload: notification,
513 },
514 ]);
515 });
516
517 it("preserves native command inputs and empty command lists", () => {
518 const availableCommands = [
519 { name: "plan", description: "Plan a task", input: { type: "text", hint: "task" } },
520 { name: "logout", description: "Sign out" },
521 ] satisfies ReadonlyArray<EffectAcpSchema.AvailableCommand>;
522
523 for (const commands of [availableCommands, []]) {
524 const notification = {
525 sessionId: "session-1",
526 update: { sessionUpdate: "available_commands_update", availableCommands: commands },
527 } satisfies EffectAcpSchema.SessionNotification;
528 expect(parseSessionUpdateEvent(notification).events).toEqual([
529 {
530 _tag: "AvailableCommandsUpdated",
531 availableCommands: commands,
532 rawPayload: notification,
533 },
534 ]);
535 }
536 });
537
538 it("keeps permission request parsing compatible with loose extension payloads", () => {
539 const request = parsePermissionRequest({
540 sessionId: "session-1",
541 options: [
542 {
543 optionId: "allow-once",
544 name: "Allow once",
545 kind: "allow_once",
546 },
547 ],
548 toolCall: {
549 toolCallId: "tool-1",
550 title: "`cat package.json`",
551 kind: "execute",
552 status: "pending",
553 content: [
554 {
555 type: "content",
556 content: {
557 type: "text",
558 text: "Not in allowlist",
559 },
560 },
561 ],
562 },
563 });
564
565 expect(request).toMatchObject({
566 kind: "execute",
567 detail: "cat package.json",
568 toolCall: {
569 toolCallId: "tool-1",
570 kind: "execute",
571 status: "pending",
572 command: "cat package.json",
573 },
574 });
575 });
576
577 it("bounds an oversized cumulative tool_call_update content buffer to a tail window", () => {
578 // Mirrors Grok's ACP CLI resending the ENTIRE accumulated terminal output on every
579 // tool_call_update notification instead of a delta (see upstream #6556).
580 const hugeText = Array.from({ length: 2_000 }, (_, i) => `line ${i}: ${"x".repeat(50)}`).join(
581 "\n",
582 );
583 expect(hugeText.length).toBeGreaterThan(60_000);
584
585 const result = parseSessionUpdateEvent({
586 sessionId: "session-1",
587 update: {
588 // Real ACP `tool_call_update` deltas typically omit `title` (already established by
589 // the initial `tool_call`); that is also the shape that surfaces raw content as detail.
590 sessionUpdate: "tool_call_update",
591 toolCallId: "tool-1",
592 kind: "other",
593 status: "in_progress",
594 content: [{ type: "content", content: { type: "text", text: hugeText } }],
595 },
596 } satisfies EffectAcpSchema.SessionNotification);
597
598 expect(result.events).toHaveLength(1);
599 const event = result.events[0];
600 if (event?._tag !== "ToolCallUpdated") {
601 throw new Error("expected a ToolCallUpdated event");
602 }
603
604 expect(event.toolCall.detail).toBeDefined();
605 const detail = event.toolCall.detail!;
606 // 8000 chars of tail plus the truncation marker, regardless of input size.
607 expect(detail.length).toBe(8_028);
608 expect(detail.startsWith("[Earlier output truncated]")).toBe(true);
609 expect(detail.endsWith(hugeText.slice(-100))).toBe(true);
610
611 // The raw payload threaded through for logging/persistence must not smuggle the full
612 // cumulative buffer back in either.
613 const rawUpdate = (
614 event.rawPayload as {
615 readonly update: {
616 readonly content: ReadonlyArray<{ readonly content: { text: string } }>;
617 };
618 }
619 ).update;
620 expect(rawUpdate.content[0]?.content.text.length).toBeLessThan(8_100);
621 expect(JSON.stringify(event).length).toBeLessThan(hugeText.length);
622 });
623
624 it("coalesces 1000 rapid cumulative tool_call_update notifications for a redrawing progress bar", () => {
625 let previous: AcpToolCallState | undefined;
626 let lastEmittedDetailLength: number | undefined;
627 let skippedSinceEmit = 0;
628 let emittedCount = 0;
629 let emittedBytes = 0;
630 let notificationBytes = 0;
631 let largestEmittedEventBytes = 0;
632 let finalDetail: string | undefined;
633 let cumulativeBuffer = "";
634
635 for (let i = 0; i < 1_000; i += 1) {
636 // Grok resends the FULL accumulated buffer, not a delta, on every redraw.
637 cumulativeBuffer += `frame ${i}: ${"#".repeat(50)}\n`;
638 const isLast = i === 999;
639
640 const notification = {
641 sessionId: "session-1",
642 update: {
643 sessionUpdate: "tool_call_update",
644 toolCallId: "tool-1",
645 kind: "other",
646 status: isLast ? "completed" : "in_progress",
647 content: [{ type: "content", content: { type: "text", text: cumulativeBuffer } }],
648 },
649 } satisfies EffectAcpSchema.SessionNotification;
650 notificationBytes += JSON.stringify(notification).length;
651
652 const { events } = parseSessionUpdateEvent(notification);
653
654 const event = events[0];
655 if (event?._tag !== "ToolCallUpdated") {
656 continue;
657 }
658
659 const merged = mergeToolCallState(previous, event.toolCall);
660 const decision = decideToolCallUpdateEmission({
661 previous,
662 next: merged,
663 lastEmittedDetailLength,
664 skippedSinceEmit,
665 });
666 previous = merged;
667 skippedSinceEmit = decision.skippedSinceEmit;
668 if (decision.emit) {
669 emittedCount += 1;
670 const eventBytes = JSON.stringify({
671 toolCall: merged,
672 rawPayload: event.rawPayload,
673 }).length;
674 emittedBytes += eventBytes;
675 largestEmittedEventBytes = Math.max(largestEmittedEventBytes, eventBytes);
676 lastEmittedDetailLength = merged.detail?.length;
677 finalDetail = merged.detail;
678 }
679 }
680
681 // The flood as the CLI sends it: 1000 cumulative redraws, ~31.6 MB of JSON.
682 expect(notificationBytes).toBeGreaterThan(31_000_000);
683
684 // 1000 cumulative redraws collapse into a fixed, small number of runtime events...
685 expect(emittedCount).toBe(114);
686 // ...each individually bounded, no matter how long the tool call runs...
687 expect(largestEmittedEventBytes).toBeLessThan(25_000);
688 // ...so the whole flooding tool call costs ~2.5 MB of runtime events instead of ~31.6 MB.
689 expect(emittedBytes).toBeLessThan(2_600_000);
690 // ...while the FINAL state (forced by the completed status) still reflects the real,
691 // latest output rather than a stale coalesced value.
692 expect(finalDetail).toBeDefined();
693 expect(finalDetail?.endsWith(`frame 999: ${"#".repeat(50)}`)).toBe(true);
694 });
695
696 it("keeps non-text tool call content entries in order when bounding oversized text", () => {
697 const hugePrefix = "x".repeat(25_000);
698 const hugeTail = "y".repeat(25_000);
699 const { events } = parseSessionUpdateEvent({
700 sessionId: "session-1",
701 update: {
702 sessionUpdate: "tool_call_update",
703 toolCallId: "tool-1",
704 kind: "edit",
705 status: "in_progress",
706 content: [
707 { type: "content", content: { type: "text", text: hugePrefix } },
708 { type: "diff", path: "/repo/file.ts", oldText: "before", newText: "after" },
709 { type: "content", content: { type: "text", text: hugeTail } },
710 { type: "diff", path: "/repo/other.ts", oldText: "old", newText: "new" },
711 { type: "content", content: { type: "text", text: " " } },
712 ],
713 },
714 } satisfies EffectAcpSchema.SessionNotification);
715
716 const event = events[0];
717 if (event?._tag !== "ToolCallUpdated") {
718 throw new Error("expected a ToolCallUpdated event");
719 }
720 const content = event.toolCall.data.content as ReadonlyArray<EffectAcpSchema.ToolCallContent>;
721 expect(content).toHaveLength(3);
722 expect(content[0]).toEqual({
723 type: "diff",
724 path: "/repo/file.ts",
725 oldText: "before",
726 newText: "after",
727 });
728 const lastEntry = content[1];
729 if (lastEntry?.type !== "content" || lastEntry.content.type !== "text") {
730 throw new Error("expected a bounded text entry");
731 }
732 expect(lastEntry.content.text.length).toBeLessThan(8_100);
733 expect(lastEntry.content.text.endsWith(hugeTail.slice(-100))).toBe(true);
734 expect(content[2]).toEqual({
735 type: "diff",
736 path: "/repo/other.ts",
737 oldText: "old",
738 newText: "new",
739 });
740 });
741
742 it("keeps a retained tail on the original text entries around non-text content", () => {
743 const prefix = "a".repeat(4_000);
744 const suffix = "b".repeat(5_000);
745 const { events } = parseSessionUpdateEvent({
746 sessionId: "session-1",
747 update: {
748 sessionUpdate: "tool_call_update",
749 toolCallId: "tool-1",
750 kind: "edit",
751 status: "in_progress",
752 content: [
753 { type: "content", content: { type: "text", text: prefix } },
754 { type: "diff", path: "/repo/file.ts", oldText: "before", newText: "after" },
755 { type: "content", content: { type: "text", text: suffix } },
756 ],
757 },
758 } satisfies EffectAcpSchema.SessionNotification);
759
760 const event = events[0];
761 if (event?._tag !== "ToolCallUpdated") {
762 throw new Error("expected a ToolCallUpdated event");
763 }
764 const content = event.toolCall.data.content as ReadonlyArray<EffectAcpSchema.ToolCallContent>;
765 expect(content).toHaveLength(3);
766 const firstText = content[0];
767 if (firstText?.type !== "content" || firstText.content.type !== "text") {
768 throw new Error("expected a bounded prefix text entry");
769 }
770 expect(firstText.content.text.startsWith("[Earlier output truncated]")).toBe(true);
771 expect(firstText.content.text.endsWith("a".repeat(100))).toBe(true);
772 expect(content[1]).toEqual({
773 type: "diff",
774 path: "/repo/file.ts",
775 oldText: "before",
776 newText: "after",
777 });
778 expect(content[2]).toEqual({
779 type: "content",
780 content: { type: "text", text: suffix },
781 });
782 });
783
784 it("bounds oversized whitespace-only tool call content that has no trimmed text", () => {
785 // Whitespace-only entries are skipped when extracting display text (`chunks.length === 0`)
786 // and used to be returned unchanged, which let a redrawing terminal persist unbounded
787 // buffers on `toolCall.data.content` and `rawPayload`.
788 const hugeWhitespace = " \n\t".repeat(30_000);
789 expect(hugeWhitespace.length).toBeGreaterThan(60_000);
790
791 const { events } = parseSessionUpdateEvent({
792 sessionId: "session-1",
793 update: {
794 sessionUpdate: "tool_call_update",
795 toolCallId: "tool-1",
796 kind: "other",
797 status: "in_progress",
798 content: [{ type: "content", content: { type: "text", text: hugeWhitespace } }],
799 },
800 } satisfies EffectAcpSchema.SessionNotification);
801
802 const event = events[0];
803 if (event?._tag !== "ToolCallUpdated") {
804 throw new Error("expected a ToolCallUpdated event");
805 }
806
807 expect(event.toolCall.detail).toBeUndefined();
808 const content = event.toolCall.data.content as ReadonlyArray<EffectAcpSchema.ToolCallContent>;
809 const textEntry = content[0];
810 if (textEntry?.type !== "content" || textEntry.content.type !== "text") {
811 throw new Error("expected a bounded text entry");
812 }
813 expect(textEntry.content.text.length).toBeLessThan(8_100);
814 expect(textEntry.content.text.startsWith("[Earlier output truncated]")).toBe(true);
815
816 const rawUpdate = (
817 event.rawPayload as {
818 readonly update: {
819 readonly content: ReadonlyArray<{ readonly content: { text: string } }>;
820 };
821 }
822 ).update;
823 expect(rawUpdate.content[0]?.content.text.length).toBeLessThan(8_100);
824 expect(JSON.stringify(event).length).toBeLessThan(hugeWhitespace.length);
825 });
826
827 it("bounds oversized whitespace-padded text entries even when trimmed content fits", () => {
828 const padded = `${" ".repeat(40_000)}ok${" ".repeat(40_000)}`;
829 expect(padded.length).toBeGreaterThan(60_000);
830
831 const { events } = parseSessionUpdateEvent({
832 sessionId: "session-1",
833 update: {
834 sessionUpdate: "tool_call_update",
835 toolCallId: "tool-1",
836 kind: "other",
837 status: "in_progress",
838 content: [{ type: "content", content: { type: "text", text: padded } }],
839 },
840 } satisfies EffectAcpSchema.SessionNotification);
841
842 const event = events[0];
843 if (event?._tag !== "ToolCallUpdated") {
844 throw new Error("expected a ToolCallUpdated event");
845 }
846
847 expect(event.toolCall.detail).toBe("ok");
848 const content = event.toolCall.data.content as ReadonlyArray<EffectAcpSchema.ToolCallContent>;
849 const textEntry = content[0];
850 if (textEntry?.type !== "content" || textEntry.content.type !== "text") {
851 throw new Error("expected a bounded text entry");
852 }
853 expect(textEntry.content.text).toBe("ok");
854
855 const rawUpdate = (
856 event.rawPayload as {
857 readonly update: {
858 readonly content: ReadonlyArray<{ readonly content: { text: string } }>;
859 };
860 }
861 ).update;
862 expect(rawUpdate.content[0]?.content.text).toBe("ok");
863 expect(JSON.stringify(event).length).toBeLessThan(padded.length);
864 });
865
866 describe("decideToolCallUpdateEmission", () => {
867 const toolCall = (detail: string | undefined, status?: AcpToolCallState["status"]) =>
868 ({
869 toolCallId: "tool-1",
870 title: "Grok Tool",
871 ...(status ? { status } : {}),
872 ...(detail ? { detail } : {}),
873 data: {},
874 }) satisfies AcpToolCallState;
875
876 it("emits the first in-progress tool_call even when it has no detail", () => {
877 expect(
878 decideToolCallUpdateEmission({
879 previous: undefined,
880 next: { toolCallId: "tool-1", title: "Grok Tool", status: "pending", data: {} },
881 lastEmittedDetailLength: undefined,
882 skippedSinceEmit: 0,
883 }),
884 ).toEqual({ emit: true, skippedSinceEmit: 0 });
885 });
886
887 it("always emits terminal (completed/failed) status updates regardless of growth", () => {
888 expect(
889 decideToolCallUpdateEmission({
890 previous: toolCall("same", "inProgress"),
891 next: toolCall("same", "completed"),
892 lastEmittedDetailLength: 4,
893 skippedSinceEmit: 0,
894 }),
895 ).toEqual({ emit: true, skippedSinceEmit: 0 });
896
897 expect(
898 decideToolCallUpdateEmission({
899 previous: toolCall("same", "inProgress"),
900 next: toolCall("same", "failed"),
901 lastEmittedDetailLength: 4,
902 skippedSinceEmit: 3,
903 }),
904 ).toEqual({ emit: true, skippedSinceEmit: 0 });
905 });
906
907 it("skips updates whose bounded detail did not change", () => {
908 const previous = toolCall("frame 1", "inProgress");
909 expect(
910 decideToolCallUpdateEmission({
911 previous,
912 next: previous,
913 lastEmittedDetailLength: 7,
914 skippedSinceEmit: 0,
915 }),
916 ).toEqual({ emit: false, skippedSinceEmit: 0 });
917 });
918
919 it("coalesces command-tool updates whose content grew while detail stayed the command", () => {
920 const commandCall = (stdout: string): AcpToolCallState => ({
921 toolCallId: "tool-1",
922 title: "Ran command",
923 status: "inProgress",
924 command: "ls",
925 detail: "ls",
926 data: {
927 command: "ls",
928 content: [{ type: "content", content: { type: "text", text: stdout } }],
929 },
930 });
931
932 let previous: AcpToolCallState | undefined;
933 let lastEmittedDetailLength: number | undefined;
934 let skippedSinceEmit = 0;
935 const emissions: Array<boolean> = [];
936
937 for (let i = 1; i <= 12; i += 1) {
938 const next = commandCall("x".repeat(i));
939 const decision = decideToolCallUpdateEmission({
940 previous,
941 next,
942 lastEmittedDetailLength,
943 skippedSinceEmit,
944 });
945 emissions.push(decision.emit);
946 skippedSinceEmit = decision.skippedSinceEmit;
947 if (decision.emit) {
948 lastEmittedDetailLength = toolCallProgressLength(next);
949 }
950 previous = next;
951 }
952
953 const emittedIndices = emissions.flatMap((emitted, index) => (emitted ? [index + 1] : []));
954 expect(emittedIndices).toEqual([1, 11]);
955 });
956
957 it("emits pending to inProgress status changes even when detail and output are unchanged", () => {
958 expect(
959 decideToolCallUpdateEmission({
960 previous: toolCall("same", "pending"),
961 next: toolCall("same", "inProgress"),
962 lastEmittedDetailLength: 4,
963 skippedSinceEmit: 0,
964 }),
965 ).toEqual({ emit: true, skippedSinceEmit: 0 });
966 });
967
968 it("emits immediately when the title changes, even with no growth", () => {
969 const decision = decideToolCallUpdateEmission({
970 previous: { toolCallId: "tool-1", title: "Reading file", detail: "x", data: {} },
971 next: { toolCallId: "tool-1", title: "Ran command", detail: "x", data: {} },
972 lastEmittedDetailLength: 1,
973 skippedSinceEmit: 0,
974 });
975 expect(decision).toEqual({ emit: true, skippedSinceEmit: 0 });
976 });
977
978 it("coalesces small deltas but forces an emission after the coalesce limit", () => {
979 let lastEmittedDetailLength: number | undefined = 0;
980 let skippedSinceEmit = 0;
981 const emissions: Array<boolean> = [];
982 let previous: AcpToolCallState | undefined;
983
984 for (let i = 1; i <= 12; i += 1) {
985 // Grows by 1 char per update — well under the 256-char growth threshold, so this
986 // exercises the coalesce-count fallback rather than the growth-based trigger.
987 const next = toolCall("x".repeat(i), "inProgress");
988 const decision = decideToolCallUpdateEmission({
989 previous,
990 next,
991 lastEmittedDetailLength,
992 skippedSinceEmit,
993 });
994 emissions.push(decision.emit);
995 skippedSinceEmit = decision.skippedSinceEmit;
996 if (decision.emit) {
997 lastEmittedDetailLength = next.detail?.length;
998 }
999 previous = next;
1000 }
1001
1002 // First update always emits (no previous state yet); after that, small per-update
1003 // growth should be coalesced until the coalesce limit forces a periodic emission.
1004 const emittedIndices = emissions.flatMap((emitted, index) => (emitted ? [index + 1] : []));
1005 expect(emittedIndices).toEqual([1, 11]);
1006 });
1007
1008 it("retains the latest replacement snapshot when equal-length updates are coalesced", () => {
1009 let previous: AcpToolCallState = toolCall("frame-a", "inProgress");
1010 const lastEmittedDetailLength = previous.detail?.length;
1011 let skippedSinceEmit = 0;
1012
1013 for (const detail of ["frame-b", "frame-c"]) {
1014 const next = mergeToolCallState(previous, toolCall(detail, "inProgress"));
1015 const decision = decideToolCallUpdateEmission({
1016 previous,
1017 next,
1018 lastEmittedDetailLength,
1019 skippedSinceEmit,
1020 });
1021 expect(decision.emit).toBe(false);
1022 skippedSinceEmit = decision.skippedSinceEmit;
1023 previous = next;
1024 }
1025
1026 const completed = mergeToolCallState(previous, toolCall(undefined, "completed"));
1027 expect(completed.detail).toBe("frame-c");
1028 expect(
1029 decideToolCallUpdateEmission({
1030 previous,
1031 next: completed,
1032 lastEmittedDetailLength,
1033 skippedSinceEmit,
1034 }),
1035 ).toEqual({ emit: true, skippedSinceEmit: 0 });
1036 });
1037 });
1038
1039 it("preserves message identity, usage, metadata, and non-text output", () => {
1040 const message = parseSessionUpdateEvent({
1041 sessionId: "session-1",
1042 update: {
1043 sessionUpdate: "agent_message_chunk",
1044 messageId: "message-1",
1045 content: {
1046 type: "resource_link",
1047 name: "report.txt",
1048 title: "Report",
1049 description: "Generated artifact",
1050 uri: "file:///workspace/report.txt",
1051 },
1052 },
1053 } satisfies EffectAcpSchema.SessionNotification);
1054 const usage = parseSessionUpdateEvent({
1055 sessionId: "session-1",
1056 update: {
1057 sessionUpdate: "usage_update",
1058 used: 2_500,
1059 size: 10_000,
1060 cost: { amount: 0.42, currency: "USD" },
1061 },
1062 } satisfies EffectAcpSchema.SessionNotification);
1063 const metadata = parseSessionUpdateEvent({
1064 sessionId: "session-1",
1065 update: {
1066 sessionUpdate: "session_info_update",
1067 title: " Native session ",
1068 updatedAt: " 2026-08-23T00:00:00Z ",
1069 },
1070 } satisfies EffectAcpSchema.SessionNotification);
1071
1072 expect(message.events[0]).toMatchObject({
1073 _tag: "ContentDelta",
1074 messageId: "message-1",
1075 text: "Report: Generated artifact\nfile:///workspace/report.txt",
1076 });
1077 expect(usage.events[0]).toMatchObject({
1078 _tag: "UsageUpdated",
1079 usage: {
1080 usedTokens: 2_500,
1081 maxTokens: 10_000,
1082 cost: { amount: 0.42, currency: "USD" },
1083 },
1084 });
1085 expect(metadata.events[0]).toMatchObject({
1086 _tag: "SessionInfoUpdated",
1087 metadata: {
1088 title: "Native session",
1089 updatedAt: "2026-08-23T00:00:00Z",
1090 },
1091 });
1092 });
1093
1094 it("projects ACP v2 plan replacement and removal updates", () => {
1095 const replaced = parseSessionUpdateEvent({
1096 sessionId: "session-1",
1097 update: {
1098 sessionUpdate: "plan_update",
1099 plan: {
1100 type: "items",
1101 planId: "plan-1",
1102 entries: [
1103 { content: " Inspect schema ", priority: "high", status: "completed" },
1104 { content: "Ship integration", priority: "high", status: "in_progress" },
1105 ],
1106 },
1107 },
1108 } satisfies EffectAcpSchema.SessionNotification);
1109 const removed = parseSessionUpdateEvent({
1110 sessionId: "session-1",
1111 update: { sessionUpdate: "plan_removed", planId: "plan-1" },
1112 } satisfies EffectAcpSchema.SessionNotification);
1113
1114 expect(replaced.events[0]).toMatchObject({
1115 _tag: "PlanUpdated",
1116 payload: {
1117 nativePlanId: "plan-1",
1118 kind: "items",
1119 plan: [
1120 { step: "Inspect schema", status: "completed" },
1121 { step: "Ship integration", status: "inProgress" },
1122 ],
1123 },
1124 });
1125 expect(removed.events[0]).toMatchObject({
1126 _tag: "PlanUpdated",
1127 payload: { nativePlanId: "plan-1", kind: "removed" },
1128 });
1129 });
1130
1131 it("preserves markdown, file, and future ACP plan variants by plan ID", () => {
1132 const updates = [
1133 {
1134 sessionId: "session-1",
1135 update: {
1136 sessionUpdate: "plan_update",
1137 plan: { type: "markdown", planId: "plan-markdown", content: "# Ship it" },
1138 },
1139 },
1140 {
1141 sessionId: "session-1",
1142 update: {
1143 sessionUpdate: "plan_update",
1144 plan: { type: "file", planId: "plan-file", uri: "file:///workspace/PLAN.md" },
1145 },
1146 },
1147 {
1148 sessionId: "session-1",
1149 update: {
1150 sessionUpdate: "plan_update",
1151 plan: { type: "diagram", planId: "plan-future", nodes: [] },
1152 },
1153 },
1154 ] as const;
1155
1156 expect(updates.flatMap((update) => parseSessionUpdateEvent(update).events)).toMatchObject([
1157 {
1158 _tag: "PlanUpdated",
1159 payload: { nativePlanId: "plan-markdown", kind: "markdown", markdown: "# Ship it" },
1160 },
1161 {
1162 _tag: "PlanUpdated",
1163 payload: {
1164 nativePlanId: "plan-file",
1165 kind: "file",
1166 uri: "file:///workspace/PLAN.md",
1167 },
1168 },
1169 {
1170 _tag: "PlanUpdated",
1171 payload: { nativePlanId: "plan-future", kind: "unknown", contentType: "diagram" },
1172 },
1173 ]);
1174 });
1175
1176 it("turns future ACP content and session updates into explicit placeholders", () => {
1177 expect(
1178 acpContentBlockDisplayText({
1179 type: "_t3_unknown",
1180 originalType: "chart",
1181 raw: { type: "chart", points: [] },
1182 }),
1183 ).toBe("[Unsupported ACP content: chart]");
1184
1185 expect(
1186 parseSessionUpdateEvent({
1187 sessionId: "session-1",
1188 update: {
1189 sessionUpdate: "_t3_unknown",
1190 originalSessionUpdate: "timeline_update",
1191 raw: { sessionUpdate: "timeline_update", entries: [] },
1192 },
1193 }).events,
1194 ).toMatchObject([{ _tag: "UnknownUpdate", updateType: "timeline_update" }]);
1195 });
1196
1197 it("projects ACP v2 compaction lifecycle and prompt usage", () => {
1198 const compacting = parseSessionUpdateEvent({
1199 sessionId: "session-1",
1200 update: {
1201 sessionUpdate: "compaction_update",
1202 compactionId: "compact-1",
1203 status: "in_progress",
1204 },
1205 } satisfies EffectAcpSchema.SessionNotification);
1206 const compacted = parseSessionUpdateEvent({
1207 sessionId: "session-1",
1208 update: {
1209 sessionUpdate: "compaction_update",
1210 compactionId: "compact-1",
1211 status: "completed",
1212 summary: [{ type: "text", text: "Retained the implementation decisions." }],
1213 },
1214 } satisfies EffectAcpSchema.SessionNotification);
1215 const idle = parseSessionUpdateEvent({
1216 sessionId: "session-1",
1217 update: {
1218 sessionUpdate: "state_update",
1219 state: "idle",
1220 stopReason: "end_turn",
1221 usage: { totalTokens: 420, inputTokens: 300, outputTokens: 100, thoughtTokens: 20 },
1222 },
1223 } satisfies EffectAcpSchema.SessionNotification);
1224
1225 expect(compacting.events[0]).toMatchObject({
1226 _tag: "ToolCallUpdated",
1227 toolCall: {
1228 toolCallId: "acp-compaction:compact-1",
1229 kind: "think",
1230 title: "Compact context",
1231 status: "inProgress",
1232 },
1233 });
1234 expect(compacted.events[0]).toMatchObject({
1235 _tag: "ToolCallUpdated",
1236 toolCall: {
1237 toolCallId: "acp-compaction:compact-1",
1238 status: "completed",
1239 data: { rawOutput: "Retained the implementation decisions." },
1240 },
1241 });
1242 expect(idle.events[0]).toMatchObject({
1243 _tag: "UsageUpdated",
1244 usage: { usedTokens: 420 },
1245 });
1246 });
1247
1248 it("applies ACP v2 agent-owned terminal snapshots and output chunks", () => {
1249 const snapshot = applyAcpAgentTerminalUpdate(undefined, {
1250 sessionUpdate: "terminal_update",
1251 terminalId: "terminal-1",
1252 command: "pnpm test",
1253 cwd: "/workspace",
1254 output: { data: Buffer.from("first line\n").toString("base64") },
1255 });
1256 const chunked = applyAcpAgentTerminalUpdate(snapshot, {
1257 sessionUpdate: "terminal_output_chunk",
1258 terminalId: "terminal-1",
1259 data: Buffer.from("second line\n").toString("base64"),
1260 });
1261 const completed = applyAcpAgentTerminalUpdate(chunked, {
1262 sessionUpdate: "terminal_update",
1263 terminalId: "terminal-1",
1264 exitStatus: { exitCode: 0 },
1265 });
1266
1267 expect(completed).toEqual({
1268 command: "pnpm test",
1269 cwd: "/workspace",
1270 output: "first line\nsecond line\n",
1271 exitStatus: { exitCode: 0 },
1272 });
1273 });
1274
1275 it("preserves UTF-8 characters split across ACP terminal output chunks", () => {
1276 const bytes = Buffer.from("A🙂B", "utf8");
1277 const first = applyAcpAgentTerminalUpdate(undefined, {
1278 sessionUpdate: "terminal_output_chunk",
1279 terminalId: "terminal-utf8",
1280 data: bytes.subarray(0, 3).toString("base64"),
1281 });
1282 const second = applyAcpAgentTerminalUpdate(first, {
1283 sessionUpdate: "terminal_output_chunk",
1284 terminalId: "terminal-utf8",
1285 data: bytes.subarray(3).toString("base64"),
1286 });
1287
1288 expect(first.output).toBe("A");
1289 expect(first.pendingOutputBytes).toBeDefined();
1290 expect(second.output).toBe("A🙂B");
1291 expect(second.pendingOutputBytes).toBeUndefined();
1292 });
1293
1294 it("drops malformed and oversized ACP terminal frames without decoding them", () => {
1295 const seeded = applyAcpAgentTerminalUpdate(undefined, {
1296 sessionUpdate: "terminal_output_chunk",
1297 terminalId: "terminal-invalid",
1298 data: Buffer.from("kept").toString("base64"),
1299 });
1300
1301 expect(
1302 applyAcpAgentTerminalUpdate(seeded, {
1303 sessionUpdate: "terminal_output_chunk",
1304 terminalId: "terminal-invalid",
1305 data: "not base64!",
1306 }),
1307 ).toBe(seeded);
1308
1309 expect(
1310 applyAcpAgentTerminalUpdate(seeded, {
1311 sessionUpdate: "terminal_output_chunk",
1312 terminalId: "terminal-oversized",
1313 data: Buffer.alloc(16 * 1024 * 1024 + 1).toString("base64"),
1314 }),
1315 ).toBe(seeded);
1316 });
1317
1318 it("projects binary ACP content without retaining encoded payloads", () => {
1319 const encodedPayload = "sensitive-base64-payload";
1320 const binary = acpContentBlockDisplayText({
1321 type: "resource",
1322 resource: {
1323 blob: encodedPayload,
1324 mimeType: "application/octet-stream",
1325 uri: "file:///workspace/archive.bin",
1326 },
1327 });
1328 const image = acpContentBlockDisplayText({
1329 type: "image",
1330 data: encodedPayload,
1331 mimeType: "image/png",
1332 uri: "file:///workspace/image.png",
1333 });
1334 const audio = acpContentBlockDisplayText({
1335 type: "audio",
1336 data: encodedPayload,
1337 mimeType: "audio/wav",
1338 });
1339
1340 expect(binary).toBe(
1341 "[ACP binary resource (application/octet-stream): file:///workspace/archive.bin]",
1342 );
1343 expect(image).toBe("[ACP image (image/png): file:///workspace/image.png]");
1344 expect(audio).toBe("[ACP audio (audio/wav)]");
1345 expect(`${binary}${image}${audio}`).not.toContain(encodedPayload);
1346
1347 const toolUpdate = parseSessionUpdateEvent({
1348 sessionId: "session-1",
1349 update: {
1350 sessionUpdate: "tool_call",
1351 toolCallId: "tool-with-image",
1352 title: "Generated image",
1353 content: [
1354 {
1355 type: "content",
1356 content: {
1357 type: "image",
1358 data: encodedPayload,
1359 mimeType: "image/png",
1360 uri: `data:image/png;base64,${encodedPayload}`,
1361 },
1362 },
1363 ],
1364 },
1365 } satisfies EffectAcpSchema.SessionNotification);
1366 const toolEvent = toolUpdate.events.find((event) => event._tag === "ToolCallUpdated");
1367 expect(JSON.stringify(toolEvent?.toolCall)).not.toContain(encodedPayload);
1368 expect(toolUpdate.events[0]).toMatchObject({
1369 _tag: "ToolCallUpdated",
1370 toolCall: {
1371 data: {
1372 content: [
1373 { type: "content", content: { type: "text", text: "[ACP image (image/png)]" } },
1374 ],
1375 },
1376 },
1377 });
1378 });
1379
1380 it("bounds text-bearing ACP content without changing streamed whitespace", () => {
1381 const text = ` ${"x".repeat(70_000)} `;
1382 const projectedText = acpContentBlockDisplayText({ type: "text", text });
1383 const projectedResource = acpContentBlockDisplayText({
1384 type: "resource",
1385 resource: { uri: "file:///workspace/large.txt", text },
1386 });
1387
1388 expect(projectedText).toHaveLength(65_536);
1389 expect(projectedText?.startsWith(" ")).toBe(true);
1390 expect(projectedResource).toEqual(projectedText);
1391 });
1392});
1393
1394describe("extractMcpToolCallIdentity", () => {
1395 function toolCallFromUpdate(
1396 update: EffectAcpSchema.SessionNotification["update"],
1397 ): NonNullable<ReturnType<typeof parsePermissionRequest>["toolCall"]> {
1398 const parsed = parseSessionUpdateEvent({ sessionId: "session-1", update });
1399 const event = parsed.events.find(
1400 (
1401 candidate,
1402 ): candidate is Extract<(typeof parsed.events)[number], { _tag: "ToolCallUpdated" }> =>
1403 candidate._tag === "ToolCallUpdated",
1404 );
1405 if (event === undefined) throw new Error("expected a tool call event");
1406 return event.toolCall;
1407 }
1408
1409 it("recovers server and tool from codex-acp tagged execute calls", () => {
1410 // Captured verbatim from codex-acp 2026-08-14: MCP calls arrive as kind
1411 // "execute" with the identity only in rawInput.
1412 const toolCall = toolCallFromUpdate({
1413 sessionUpdate: "tool_call",
1414 toolCallId: "exec-f4591587-0754-4bb4-990b-f2767894ba93",
1415 kind: "execute",
1416 title: "mcp.t3-code.orchestrator_capabilities",
1417 status: "in_progress",
1418 rawInput: { server: "t3-code", tool: "orchestrator_capabilities", arguments: {} },
1419 _meta: { is_mcp_tool_call: true },
1420 });
1421
1422 expect(extractMcpToolCallIdentity(toolCall)).toEqual({
1423 server: "t3-code",
1424 tool: "orchestrator_capabilities",
1425 });
1426 });
1427
1428 it("recovers T3 identity from acp-mcp-call fallback commands", () => {
1429 const toolCall = toolCallFromUpdate({
1430 sessionUpdate: "tool_call",
1431 toolCallId: "exec-1",
1432 kind: "execute",
1433 title: "Ran command",
1434 status: "in_progress",
1435 });
1436
1437 expect(
1438 extractMcpToolCallIdentity(toolCall, {
1439 embeddedTerminalCommands: [
1440 '/usr/bin/node /srv/t3/bin.ts acp-mcp-call delegate_task {"task":"x"}',
1441 ],
1442 }),
1443 ).toEqual({ server: "t3-code", tool: "delegate_task", input: { task: "x" } });
1444 });
1445
1446 it("recovers T3 identity from pi-acp title-only fallback execs", () => {
1447 // Captured verbatim from pi-acp 0.0.33 2026-08-14: rawInput is null and
1448 // the command line only appears as the verbatim title, which the
1449 // presentation layer summarizes into "Ran command".
1450 const toolCall = toolCallFromUpdate({
1451 sessionUpdate: "tool_call",
1452 toolCallId: "call_JdxnvzjHHrbvyASTLVekLYWV|fc_08f5a805a7159aa6016a7ec4afad548191",
1453 kind: "execute",
1454 title:
1455 '"$T3_ACP_MCP_NODE" "$T3_ACP_MCP_ENTRYPOINT" acp-mcp-call orchestrator_capabilities \'{}\'',
1456 status: "in_progress",
1457 rawInput: null,
1458 content: [
1459 {
1460 type: "terminal",
1461 terminalId: "call_JdxnvzjHHrbvyASTLVekLYWV|fc_08f5a805a7159aa6016a7ec4afad548191",
1462 },
1463 ],
1464 });
1465
1466 expect(toolCall.title).toBe("Ran command");
1467 expect(extractMcpToolCallIdentity(toolCall)).toEqual({
1468 server: "t3-code",
1469 tool: "orchestrator_capabilities",
1470 input: {},
1471 });
1472 });
1473
1474 it("recovers T3 identity from server-namespaced titles across titleless updates", () => {
1475 // Captured verbatim from Kilo 7.4.22 2026-08-15: the initial tool_call
1476 // titles the MCP function "<server>_<tool>" with kind "other", and the
1477 // completed update carries no title at all, so the merged presentation
1478 // title regresses to "Tool" while data.title keeps the wire value.
1479 const created = toolCallFromUpdate({
1480 sessionUpdate: "tool_call",
1481 toolCallId: "chatcmpl-tool-b2a6142ee1a510a5",
1482 kind: "other",
1483 title: "t3-code_orchestrator_capabilities",
1484 status: "pending",
1485 locations: [],
1486 rawInput: {},
1487 });
1488 const completed = toolCallFromUpdate({
1489 sessionUpdate: "tool_call_update",
1490 toolCallId: "chatcmpl-tool-b2a6142ee1a510a5",
1491 status: "completed",
1492 content: [{ type: "content", content: { type: "text", text: '{"ok":true}' } }],
1493 });
1494 const merged = mergeToolCallState(created, completed);
1495
1496 expect(extractMcpToolCallIdentity(merged)).toEqual({
1497 server: "t3-code",
1498 tool: "orchestrator_capabilities",
1499 });
1500 });
1501
1502 it("recovers T3 identity from Gemini and qwen MCP-server title templates", () => {
1503 // Gemini CLI 0.55.1: "<tool> (<server> MCP Server)"; qwen-code 0.21.12
1504 // appends ": <args json>" to the same template.
1505 const gemini = toolCallFromUpdate({
1506 sessionUpdate: "tool_call",
1507 toolCallId: "gemini-1",
1508 kind: "other",
1509 title: "delegate_task (t3-code MCP Server)",
1510 status: "in_progress",
1511 });
1512 const qwen = toolCallFromUpdate({
1513 sessionUpdate: "tool_call",
1514 toolCallId: "qwen-1",
1515 kind: "other",
1516 title: 'task_status (t3-code MCP Server): {"taskId":"node:delegated-task:1"}',
1517 status: "pending",
1518 rawInput: { taskId: "node:delegated-task:1" },
1519 });
1520
1521 expect(extractMcpToolCallIdentity(gemini)).toEqual({
1522 server: "t3-code",
1523 tool: "delegate_task",
1524 });
1525 expect(extractMcpToolCallIdentity(qwen)).toEqual({ server: "t3-code", tool: "task_status" });
1526 });
1527
1528 it("recovers T3 identity across the registry agents' naming conventions", () => {
1529 // One representative per surveyed convention (2026-08 registry builds):
1530 // droid triple underscore, Copilot hyphen, Amp mangled server + detail
1531 // tail, cline args tail, Auggie tool-first suffix.
1532 // One representative per surveyed convention (2026-08 registry builds):
1533 // droid triple underscore, Copilot hyphen, Amp mangled server + detail
1534 // tail, cline args tail, Auggie tool-first suffix, fast-agent slash,
1535 // Kimi bare name with args tail.
1536 for (const title of [
1537 "t3-code___delegate_task",
1538 "t3-code-delegate_task",
1539 'mcp__t3_code__delegate_task: {"mode":"async"}',
1540 't3-code__delegate_task: {"mode":"async"}',
1541 "delegate_task_t3-code",
1542 "t3-code/delegate_task",
1543 'delegate_task: {"mode":"async"}',
1544 ]) {
1545 const toolCall = toolCallFromUpdate({
1546 sessionUpdate: "tool_call",
1547 toolCallId: "convention-1",
1548 kind: "other",
1549 title,
1550 status: "pending",
1551 });
1552 expect(extractMcpToolCallIdentity(toolCall), title).toEqual({
1553 server: "t3-code",
1554 tool: "delegate_task",
1555 });
1556 }
1557 });
1558
1559 it("recovers T3 identity from goose _meta despite LLM-rewritten titles", () => {
1560 // goose enriches titles asynchronously, so only _meta.goose.toolCall is
1561 // stable; shape from crates/goose/src/acp/server/tool_calls/conversion.rs.
1562 const toolCall = toolCallFromUpdate({
1563 sessionUpdate: "tool_call",
1564 toolCallId: "goose-1",
1565 title: "Checking on the delegated task",
1566 status: "in_progress",
1567 _meta: {
1568 goose: {
1569 toolCall: { toolName: "t3-code__task_status", extensionName: "t3-code" },
1570 messageId: "message-1",
1571 },
1572 },
1573 });
1574
1575 expect(extractMcpToolCallIdentity(toolCall)).toEqual({
1576 server: "t3-code",
1577 tool: "task_status",
1578 });
1579 });
1580
1581 it("recovers T3 identity from qwen serverId meta regardless of prefix format", () => {
1582 // qwen-code 0.21.12 emits _meta.serverId + _meta.toolName; serverId is an
1583 // explicit origin assertion, so a known tool suffix suffices even if the
1584 // prefix format changes.
1585 const toolCall = toolCallFromUpdate({
1586 sessionUpdate: "tool_call",
1587 toolCallId: "qwen-meta-1",
1588 kind: "other",
1589 title: "unrelated display title",
1590 status: "pending",
1591 _meta: { toolName: "mcp::t3-code::t3_thread_send", serverId: "t3-code", provenance: "mcp" },
1592 });
1593
1594 expect(extractMcpToolCallIdentity(toolCall)).toEqual({
1595 server: "t3-code",
1596 tool: "t3_thread_send",
1597 });
1598 });
1599
1600 it.each([
1601 { _meta: { claudeCode: { toolName: "mcp__weather__get_weather" } } },
1602 { _meta: { toolName: "mcp::weather::get_weather", serverId: "weather" } },
1603 ])("recovers external MCP identity from provider metadata", (metadata) => {
1604 const toolCall = toolCallFromUpdate({
1605 sessionUpdate: "tool_call",
1606 toolCallId: "weather-call",
1607 title: "Checking the forecast",
1608 status: "in_progress",
1609 ...metadata,
1610 });
1611 expect(extractMcpToolCallIdentity(toolCall)).toEqual({
1612 server: "weather",
1613 tool: "get_weather",
1614 });
1615 });
1616
1617 it.each([
1618 { kind: "execute", toolName: "developer__shell", extensionName: "developer" },
1619 { kind: "edit", toolName: "developer__edit", extensionName: "developer" },
1620 { kind: "other", toolName: "weather__get_weather", extensionName: "weather" },
1621 ] as const)(
1622 "leaves goose $toolName unclassified so built-ins keep their projection",
1623 ({ kind, toolName, extensionName }) => {
1624 // goose tags built-in extensions exactly like user MCP servers.
1625 const toolCall = toolCallFromUpdate({
1626 sessionUpdate: "tool_call",
1627 toolCallId: "goose-builtin",
1628 kind,
1629 title: "developer: shell",
1630 status: "in_progress",
1631 _meta: { goose: { toolCall: { toolName, extensionName } } },
1632 });
1633 expect(extractMcpToolCallIdentity(toolCall)).toBeUndefined();
1634 },
1635 );
1636
1637 it("uses the asserted server instead of a misleading T3 title", () => {
1638 const toolCall = toolCallFromUpdate({
1639 sessionUpdate: "tool_call",
1640 toolCallId: "foreign-1",
1641 kind: "other",
1642 title: "t3-code_delegate_task",
1643 status: "pending",
1644 _meta: { toolName: "delegate_task", serverId: "other-orchestrator" },
1645 });
1646
1647 expect(extractMcpToolCallIdentity(toolCall)).toEqual({
1648 server: "other-orchestrator",
1649 tool: "delegate_task",
1650 });
1651 });
1652
1653 it("does not brand path-like or unknown-tool titles", () => {
1654 for (const title of ["t3-code/README.md", "t3-code_not_a_real_tool"]) {
1655 const toolCall = toolCallFromUpdate({
1656 sessionUpdate: "tool_call",
1657 toolCallId: "path-1",
1658 kind: "other",
1659 title,
1660 status: "pending",
1661 });
1662 expect(extractMcpToolCallIdentity(toolCall), title).toBeUndefined();
1663 }
1664 });
1665
1666 it("leaves ordinary execute calls unidentified", () => {
1667 const toolCall = toolCallFromUpdate({
1668 sessionUpdate: "tool_call",
1669 toolCallId: "exec-2",
1670 kind: "execute",
1671 title: "cat package.json",
1672 status: "in_progress",
1673 rawInput: { command: "cat package.json" },
1674 });
1675
1676 expect(extractMcpToolCallIdentity(toolCall)).toBeUndefined();
1677 expect(
1678 extractMcpToolCallIdentity(toolCall, { embeddedTerminalCommands: ["bash -lc ls"] }),
1679 ).toBeUndefined();
1680 });
1681
1682 it("does not treat generic raw server and tool fields as MCP provenance", () => {
1683 const toolCall = toolCallFromUpdate({
1684 sessionUpdate: "tool_call",
1685 toolCallId: "generic-1",
1686 kind: "other",
1687 title: "Deploy",
1688 status: "pending",
1689 rawInput: { server: "production", tool: "deploy" },
1690 });
1691
1692 expect(extractMcpToolCallIdentity(toolCall)).toBeUndefined();
1693 });
1694});
1695
1696describe("embeddedTerminalIdsFromSessionUpdate", () => {
1697 it("collects terminal ids from tool_call content before the text rewrite", () => {
1698 expect(
1699 embeddedTerminalIdsFromSessionUpdate({
1700 sessionId: "session-1",
1701 update: {
1702 sessionUpdate: "tool_call_update",
1703 toolCallId: "call_1",
1704 status: "in_progress",
1705 content: [
1706 { type: "terminal", terminalId: "t3-term-9" },
1707 { type: "content", content: { type: "text", text: "noise" } },
1708 ],
1709 },
1710 }),
1711 ).toEqual({ toolCallId: "call_1", terminalIds: ["t3-term-9"] });
1712 expect(
1713 embeddedTerminalIdsFromSessionUpdate({
1714 sessionId: "session-1",
1715 update: {
1716 sessionUpdate: "tool_call",
1717 toolCallId: "call_2",
1718 title: "Tool",
1719 status: "pending",
1720 },
1721 }),
1722 ).toBeUndefined();
1723 });
1724});