1import { describe, expect, it } from "vite-plus/test";
2
3import { summarizeT3ToolCalls, type T3ToolSummaryCall } from "./t3ToolSummary.ts";
4
5function completed(input: unknown, output?: unknown): T3ToolSummaryCall {
6 return { input, output, outcome: "completed" };
7}
8
9describe("summarizeT3ToolCalls", () => {
10 it("counts registered projects, repository destinations, and accepted thread launches", () => {
11 expect(
12 summarizeT3ToolCalls("project-create", [
13 completed({}, { id: "project-1" }),
14 completed({}, { id: "project-1" }),
15 completed({}, { id: "project-2" }),
16 ]).label,
17 ).toBe("Registered 2 projects");
18 expect(
19 summarizeT3ToolCalls("project-clone", [
20 completed({}, { cwd: "/tmp/first" }),
21 completed({}, { cwd: "/tmp/second" }),
22 ]).label,
23 ).toBe("Cloned 2 repositories");
24 expect(
25 summarizeT3ToolCalls("thread-create", [
26 completed({}, { threadId: "launched-thread", status: "preparing" }),
27 ]).label,
28 ).toBe("Created 1 thread");
29 });
30
31 it("reports review guides by outcome, and a call where none started as a failure", () => {
32 const guides = (...outcomes: ReadonlyArray<string>) => ({
33 guides: outcomes.map((outcome, index) => ({ number: index + 1, outcome })),
34 });
35 expect(
36 summarizeT3ToolCalls("write-pr-guides", [
37 completed({}, guides("started", "running", "failed")),
38 ]),
39 ).toEqual({ label: "Started 2 review guides, 1 failed", failedCount: 1 });
40 const allFailed = summarizeT3ToolCalls("write-pr-guides", [
41 completed({}, guides("failed", "failed")),
42 ]);
43 expect(allFailed.label).toBe("Could not start 2 review guides");
44 expect(allFailed.failedCount).toBe(2);
45 });
46
47 it.each([
48 ["queue-read", "Read 1 queued message"],
49 ["queue-edit", "Edited 1 queued message"],
50 ["queue-cancel", "Requested cancellation of 1 queued run"],
51 ["queue-reorder", "Reordered 1 queued run"],
52 ["queue-steer", "Requested steering with 1 queued message"],
53 ] as const)("deduplicates the queued run target for %s", (action, label) => {
54 expect(
55 summarizeT3ToolCalls(action, [
56 completed({ queuedRunId: "queued-1" }),
57 completed({ queuedRunId: "queued-1" }),
58 { input: { queuedRunId: "queued-2" }, output: undefined, outcome: "unfinished" },
59 ]),
60 ).toEqual({ label, failedCount: 0 });
61 });
62
63 it("counts answered requests rather than pretending every request contains one question", () => {
64 expect(
65 summarizeT3ToolCalls("question-respond", [
66 completed({ requestId: "request-1", answers: { one: ["Yes"], two: ["No"] } }),
67 completed({ requestId: "request-1" }),
68 completed({ requestId: "request-2" }),
69 ]).label,
70 ).toBe("Answered 2 pending question requests");
71 });
72
73 it("counts attachments in distinct messages and falls back when attachment counts are missing", () => {
74 const first = completed(
75 { threadId: "thread-1", attachments: [{ id: "one" }, { id: "two" }] },
76 { messageId: "message-1", threadId: "thread-1" },
77 );
78 const second = completed(
79 { threadId: "thread-2", attachments: [{ id: "one" }] },
80 { messageId: "message-2", threadId: "thread-2" },
81 );
82 expect(summarizeT3ToolCalls("attachment-send", [first, first, second])).toEqual({
83 label: "Sent 3 attachments to 2 threads",
84 failedCount: 0,
85 });
86 expect(
87 summarizeT3ToolCalls("attachment-send", [first, completed({ threadId: "thread-1" })]).label,
88 ).toBe("Sent attachments to 1 thread 2 times");
89 });
90
91 it("keeps repeated manual runs separate and describes asynchronous controls as requests", () => {
92 expect(
93 summarizeT3ToolCalls("schedule-run", [
94 completed({ taskId: "schedule-1" }, { lastRunStatus: "running" }),
95 completed({ taskId: "schedule-1" }, { lastRunStatus: "skipped" }),
96 ]).label,
97 ).toBe("Requested 2 scheduled task runs");
98 expect(
99 summarizeT3ToolCalls("thread-fork", [completed({}, { targetThreadId: "fork", sequence: 3 })])
100 .label,
101 ).toBe("Requested 1 thread fork");
102 expect(
103 summarizeT3ToolCalls("thread-merge", [
104 completed({ targetThreadId: "parent" }, { sequence: 4 }),
105 ]).label,
106 ).toBe("Requested 1 context merge");
107 });
108
109 it.each([
110 "WorktreeMcpFailure",
111 "DeviceOperationError",
112 "PreviewAutomationExecutionError",
113 "PullRequestOperationError",
114 ])("treats a returned %s as a failed call even if the provider says completed", (_tag) => {
115 const output = [
116 {
117 type: "content",
118 content: { type: "text", text: JSON.stringify({ _tag, message: "Unavailable" }) },
119 },
120 ];
121 expect(summarizeT3ToolCalls("browser", [completed({}, output)])).toEqual({
122 label: "Tried to use browser 1 time",
123 failedCount: 1,
124 });
125 });
126 it("counts messages and distinct destinations across delivery modes, deduplicating retries", () => {
127 const calls = Array.from({ length: 5 }, (_, i) =>
128 completed(
129 { threadId: `thread-${i % 2}`, mode: ["auto", "queue", "steer", "restart"][i % 4] },
130 { messageId: `message-${i}`, threadId: `thread-${i % 2}` },
131 ),
132 );
133 expect(summarizeT3ToolCalls("thread-send", [...calls, calls[0]!])).toEqual({
134 label: "Sent 5 messages to 2 threads",
135 failedCount: 0,
136 });
137 });
138
139 it("reads provider result envelopes without treating JSON in the message as result data", () => {
140 const result = { messageId: "message-1", threadId: "actual-thread" };
141 const json = JSON.stringify(result);
142 const outputs = [
143 result,
144 { structuredContent: result },
145 [{ type: "text", text: json }],
146 { content: [{ text: { text: json } }], isError: false },
147 json,
148 ];
149 const calls = outputs.map((output) =>
150 completed(
151 {
152 toolName: "t3_thread_send",
153 args: { threadId: "input-thread", message: '{"threadId":"fake"}' },
154 },
155 output,
156 ),
157 );
158 expect(summarizeT3ToolCalls("thread-send", calls).label).toBe("Sent 1 message to 1 thread");
159 expect(
160 summarizeT3ToolCalls("thread-send", [
161 completed({ toolName: "t3_thread_send", args: { threadId: "input-thread" } }),
162 completed({ threadId: "input-thread" }),
163 ]).label,
164 ).toBe("Sent 2 messages to 1 thread");
165 });
166
167 it("falls back to message counts when a destination is missing or a result is malformed", () => {
168 expect(
169 summarizeT3ToolCalls("thread-send", [
170 completed({ threadId: "known" }),
171 completed({ message: '{"threadId":"not-a-destination"}' }, "{truncated"),
172 completed(undefined, "Message sent"),
173 ]).label,
174 ).toBe("Sent 3 messages");
175 });
176
177 it("counts batch-created threads, excludes rollbacks, and deduplicates returned thread IDs", () => {
178 const threads = Array.from({ length: 4 }, (_, i) => ({
179 threadId: `thread-${i}`,
180 status: "running",
181 }));
182 expect(
183 summarizeT3ToolCalls("thread-create", [
184 completed(
185 {},
186 { threads: [...threads, { threadId: "rolled-back", status: "rolled_back" }] },
187 ),
188 completed({}, { threadId: "thread-0", status: "running" }),
189 ]).label,
190 ).toBe("Created 4 threads");
191 expect(
192 summarizeT3ToolCalls("thread-create", [
193 completed({ threads: [{ title: "Requested, not confirmed" }] }),
194 ]).label,
195 ).toBe("Requested thread creation 1 time");
196 });
197
198 it("excludes failed and unfinished sends even if the provider reports completed", () => {
199 const calls: T3ToolSummaryCall[] = [
200 completed({ threadId: "success" }, { messageId: "ok", threadId: "success" }),
201 completed(
202 { threadId: "failed" },
203 { isError: true, structuredContent: { threadId: "failed" } },
204 ),
205 completed({ threadId: "failed" }, [
206 { type: "text", text: JSON.stringify({ _tag: "OrchestratorMcpFailure" }) },
207 ]),
208 { input: { threadId: "cancelled" }, output: undefined, outcome: "unfinished" },
209 ];
210 expect(summarizeT3ToolCalls("thread-send", calls)).toEqual({
211 label: "Sent 1 message to 1 thread",
212 failedCount: 2,
213 });
214 expect(summarizeT3ToolCalls("thread-send", [calls[1]!])).toEqual({
215 label: "Tried to send 1 message to 1 thread",
216 failedCount: 1,
217 });
218 });
219
220 it("does not confuse a child's failure or wait timeout with failure of the orchestration call", () => {
221 const failedChild = { taskId: "task-1", status: "failed", summary: "command not found" };
222 expect(
223 summarizeT3ToolCalls("delegate", [completed({}, failedChild), completed({}, failedChild)]),
224 ).toEqual({
225 label: "Delegated 1 task",
226 failedCount: 0,
227 });
228 expect(
229 summarizeT3ToolCalls(
230 "task-status",
231 Array.from({ length: 4 }, () => completed({ taskId: "task-1" }, failedChild)),
232 ).label,
233 ).toBe("Checked task status 4 times");
234 expect(
235 summarizeT3ToolCalls("thread-wait", [
236 completed({ threadId: "thread-1" }, { threadId: "thread-1", timedOut: true }),
237 ]),
238 ).toEqual({
239 label: "Waited on 1 thread",
240 failedCount: 0,
241 });
242 });
243
244 it("describes control requests without claiming that a thread stopped or a task was deleted", () => {
245 expect(
246 summarizeT3ToolCalls("thread-interrupt", [
247 completed(
248 { threadId: "thread-1" },
249 { threadId: "thread-1", status: "interrupt_requested" },
250 ),
251 ]).label,
252 ).toBe("Requested interrupts for 1 thread");
253 expect(
254 summarizeT3ToolCalls("task-cancel", [
255 completed({ taskId: "task-1" }, { taskId: "task-1", status: "cancel_requested" }),
256 ]).label,
257 ).toBe("Requested cancellation of 1 task");
258 expect(
259 summarizeT3ToolCalls("schedule-delete", [
260 completed(
261 { scheduledTaskId: "schedule-1" },
262 { scheduledTaskId: "schedule-1", deleted: false },
263 ),
264 ]).label,
265 ).toBe("Requested deletion of 1 scheduled task");
266 });
267
268 it("counts workflow stop requests by coordinator and excludes failed requests", () => {
269 const first = completed({ threadId: "parent", subagentId: "workflow-1" }, {});
270 const failed = completed({ threadId: "parent", subagentId: "workflow-3" }, { isError: true });
271 expect(
272 summarizeT3ToolCalls("workflow-stop", [
273 first,
274 first,
275 completed({ threadId: "parent", subagentId: "workflow-2" }, {}),
276 failed,
277 ]),
278 ).toEqual({ label: "Requested stops for 2 workflows", failedCount: 1 });
279 expect(summarizeT3ToolCalls("workflow-stop", [failed])).toEqual({
280 label: "Tried to stop 1 workflow",
281 failedCount: 1,
282 });
283 });
284});