claude-code-rust 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -661
- package/README.md +3 -9
- package/agent-sdk/README.md +5 -0
- package/agent-sdk/dist/bridge/commands.js +183 -19
- package/agent-sdk/dist/bridge/events.js +14 -1
- package/agent-sdk/dist/bridge/history.js +4 -1
- package/agent-sdk/dist/bridge/mcp.js +289 -0
- package/agent-sdk/dist/bridge/message_handlers.js +14 -3
- package/agent-sdk/dist/bridge/session_lifecycle.js +139 -41
- package/agent-sdk/dist/bridge/tool_calls.js +12 -3
- package/agent-sdk/dist/bridge/tooling.js +272 -7
- package/agent-sdk/dist/bridge/user_interaction.js +76 -28
- package/agent-sdk/dist/bridge.js +158 -7
- package/agent-sdk/dist/bridge.test.js +846 -23
- package/package.json +3 -3
|
@@ -1,6 +1,96 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { AsyncQueue, CACHE_SPLIT_POLICY, buildRateLimitUpdate, buildQueryOptions, buildToolResultFields, createToolCall, mapAvailableAgents, mapSessionMessagesToUpdates, mapSdkSessions, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, parseFastModeState, parseRateLimitStatus, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, resolveInstalledAgentSdkVersion, unwrapToolUseResult, } from "./bridge.js";
|
|
3
|
+
import { AsyncQueue, CACHE_SPLIT_POLICY, buildRateLimitUpdate, buildQueryOptions, canGenerateSessionTitle, generatePersistedSessionTitle, buildSessionMutationOptions, buildSessionListOptions, buildToolResultFields, createToolCall, handleTaskSystemMessage, mapAvailableAgents, mapAvailableModels, mapSessionMessagesToUpdates, mapSdkSessions, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, parseFastModeState, parseRateLimitStatus, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, staleMcpAuthCandidates, resolveInstalledAgentSdkVersion, unwrapToolUseResult, } from "./bridge.js";
|
|
4
|
+
import { emitToolProgressUpdate } from "./bridge/tool_calls.js";
|
|
5
|
+
import { requestAskUserQuestionAnswers } from "./bridge/user_interaction.js";
|
|
6
|
+
function makeSessionState() {
|
|
7
|
+
const input = new AsyncQueue();
|
|
8
|
+
return {
|
|
9
|
+
sessionId: "session-1",
|
|
10
|
+
cwd: "C:/work",
|
|
11
|
+
model: "haiku",
|
|
12
|
+
availableModels: [],
|
|
13
|
+
mode: null,
|
|
14
|
+
fastModeState: "off",
|
|
15
|
+
query: {},
|
|
16
|
+
input,
|
|
17
|
+
connected: true,
|
|
18
|
+
connectEvent: "connected",
|
|
19
|
+
toolCalls: new Map(),
|
|
20
|
+
taskToolUseIds: new Map(),
|
|
21
|
+
pendingPermissions: new Map(),
|
|
22
|
+
pendingQuestions: new Map(),
|
|
23
|
+
pendingElicitations: new Map(),
|
|
24
|
+
mcpStatusRevalidatedAt: new Map(),
|
|
25
|
+
authHintSent: false,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function captureBridgeEvents(run) {
|
|
29
|
+
const writes = [];
|
|
30
|
+
const originalWrite = process.stdout.write;
|
|
31
|
+
process.stdout.write = (chunk) => {
|
|
32
|
+
if (typeof chunk === "string") {
|
|
33
|
+
writes.push(chunk);
|
|
34
|
+
}
|
|
35
|
+
else if (Buffer.isBuffer(chunk)) {
|
|
36
|
+
writes.push(chunk.toString("utf8"));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
writes.push(String(chunk));
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
};
|
|
43
|
+
try {
|
|
44
|
+
run();
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
process.stdout.write = originalWrite;
|
|
48
|
+
}
|
|
49
|
+
return writes
|
|
50
|
+
.map((line) => line.trim())
|
|
51
|
+
.filter((line) => line.startsWith("{"))
|
|
52
|
+
.flatMap((line) => {
|
|
53
|
+
try {
|
|
54
|
+
return [JSON.parse(line)];
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function captureBridgeEventsAsync(run) {
|
|
62
|
+
const writes = [];
|
|
63
|
+
const originalWrite = process.stdout.write;
|
|
64
|
+
process.stdout.write = (chunk) => {
|
|
65
|
+
if (typeof chunk === "string") {
|
|
66
|
+
writes.push(chunk);
|
|
67
|
+
}
|
|
68
|
+
else if (Buffer.isBuffer(chunk)) {
|
|
69
|
+
writes.push(chunk.toString("utf8"));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
writes.push(String(chunk));
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
await run();
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
process.stdout.write = originalWrite;
|
|
81
|
+
}
|
|
82
|
+
return writes
|
|
83
|
+
.map((line) => line.trim())
|
|
84
|
+
.filter((line) => line.startsWith("{"))
|
|
85
|
+
.flatMap((line) => {
|
|
86
|
+
try {
|
|
87
|
+
return [JSON.parse(line)];
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
4
94
|
test("parseCommandEnvelope validates initialize command", () => {
|
|
5
95
|
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
6
96
|
request_id: "req-1",
|
|
@@ -20,11 +110,18 @@ test("parseCommandEnvelope validates resume_session command without cwd", () =>
|
|
|
20
110
|
command: "resume_session",
|
|
21
111
|
session_id: "session-123",
|
|
22
112
|
launch_settings: {
|
|
23
|
-
model: "haiku",
|
|
24
113
|
language: "German",
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
114
|
+
settings: {
|
|
115
|
+
alwaysThinkingEnabled: true,
|
|
116
|
+
model: "haiku",
|
|
117
|
+
permissions: { defaultMode: "plan" },
|
|
118
|
+
fastMode: false,
|
|
119
|
+
effortLevel: "high",
|
|
120
|
+
outputStyle: "Default",
|
|
121
|
+
spinnerTipsEnabled: true,
|
|
122
|
+
terminalProgressBarEnabled: true,
|
|
123
|
+
},
|
|
124
|
+
agent_progress_summaries: true,
|
|
28
125
|
},
|
|
29
126
|
}));
|
|
30
127
|
assert.equal(parsed.requestId, "req-2");
|
|
@@ -33,22 +130,176 @@ test("parseCommandEnvelope validates resume_session command without cwd", () =>
|
|
|
33
130
|
throw new Error("unexpected command variant");
|
|
34
131
|
}
|
|
35
132
|
assert.equal(parsed.command.session_id, "session-123");
|
|
36
|
-
assert.equal(parsed.command.launch_settings.model, "haiku");
|
|
37
133
|
assert.equal(parsed.command.launch_settings.language, "German");
|
|
38
|
-
assert.
|
|
39
|
-
|
|
40
|
-
|
|
134
|
+
assert.deepEqual(parsed.command.launch_settings.settings, {
|
|
135
|
+
alwaysThinkingEnabled: true,
|
|
136
|
+
model: "haiku",
|
|
137
|
+
permissions: { defaultMode: "plan" },
|
|
138
|
+
fastMode: false,
|
|
139
|
+
effortLevel: "high",
|
|
140
|
+
outputStyle: "Default",
|
|
141
|
+
spinnerTipsEnabled: true,
|
|
142
|
+
terminalProgressBarEnabled: true,
|
|
143
|
+
});
|
|
144
|
+
assert.equal(parsed.command.launch_settings.agent_progress_summaries, true);
|
|
145
|
+
});
|
|
146
|
+
test("parseCommandEnvelope validates rename_session command", () => {
|
|
147
|
+
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
148
|
+
request_id: "req-rename",
|
|
149
|
+
command: "rename_session",
|
|
150
|
+
session_id: "session-123",
|
|
151
|
+
title: "Renamed session",
|
|
152
|
+
}));
|
|
153
|
+
assert.equal(parsed.requestId, "req-rename");
|
|
154
|
+
assert.equal(parsed.command.command, "rename_session");
|
|
155
|
+
if (parsed.command.command !== "rename_session") {
|
|
156
|
+
throw new Error("unexpected command variant");
|
|
157
|
+
}
|
|
158
|
+
assert.equal(parsed.command.session_id, "session-123");
|
|
159
|
+
assert.equal(parsed.command.title, "Renamed session");
|
|
160
|
+
});
|
|
161
|
+
test("parseCommandEnvelope validates generate_session_title command", () => {
|
|
162
|
+
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
163
|
+
request_id: "req-generate",
|
|
164
|
+
command: "generate_session_title",
|
|
165
|
+
session_id: "session-123",
|
|
166
|
+
description: "Current custom title",
|
|
167
|
+
}));
|
|
168
|
+
assert.equal(parsed.requestId, "req-generate");
|
|
169
|
+
assert.equal(parsed.command.command, "generate_session_title");
|
|
170
|
+
if (parsed.command.command !== "generate_session_title") {
|
|
171
|
+
throw new Error("unexpected command variant");
|
|
172
|
+
}
|
|
173
|
+
assert.equal(parsed.command.session_id, "session-123");
|
|
174
|
+
assert.equal(parsed.command.description, "Current custom title");
|
|
175
|
+
});
|
|
176
|
+
test("parseCommandEnvelope validates mcp_toggle command", () => {
|
|
177
|
+
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
178
|
+
request_id: "req-mcp-toggle",
|
|
179
|
+
command: "mcp_toggle",
|
|
180
|
+
session_id: "session-123",
|
|
181
|
+
server_name: "notion",
|
|
182
|
+
enabled: false,
|
|
183
|
+
}));
|
|
184
|
+
assert.equal(parsed.requestId, "req-mcp-toggle");
|
|
185
|
+
assert.equal(parsed.command.command, "mcp_toggle");
|
|
186
|
+
if (parsed.command.command !== "mcp_toggle") {
|
|
187
|
+
throw new Error("unexpected command variant");
|
|
188
|
+
}
|
|
189
|
+
assert.equal(parsed.command.session_id, "session-123");
|
|
190
|
+
assert.equal(parsed.command.server_name, "notion");
|
|
191
|
+
assert.equal(parsed.command.enabled, false);
|
|
192
|
+
});
|
|
193
|
+
test("parseCommandEnvelope validates mcp_set_servers command", () => {
|
|
194
|
+
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
195
|
+
request_id: "req-mcp-set",
|
|
196
|
+
command: "mcp_set_servers",
|
|
197
|
+
session_id: "session-123",
|
|
198
|
+
servers: {
|
|
199
|
+
notion: {
|
|
200
|
+
type: "http",
|
|
201
|
+
url: "https://mcp.notion.com/mcp",
|
|
202
|
+
headers: {
|
|
203
|
+
"X-Test": "1",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
}));
|
|
208
|
+
assert.equal(parsed.requestId, "req-mcp-set");
|
|
209
|
+
assert.equal(parsed.command.command, "mcp_set_servers");
|
|
210
|
+
if (parsed.command.command !== "mcp_set_servers") {
|
|
211
|
+
throw new Error("unexpected command variant");
|
|
212
|
+
}
|
|
213
|
+
assert.equal(parsed.command.session_id, "session-123");
|
|
214
|
+
assert.deepEqual(parsed.command.servers, {
|
|
215
|
+
notion: {
|
|
216
|
+
type: "http",
|
|
217
|
+
url: "https://mcp.notion.com/mcp",
|
|
218
|
+
headers: {
|
|
219
|
+
"X-Test": "1",
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
test("staleMcpAuthCandidates selects previously connected servers that regressed to needs-auth", () => {
|
|
225
|
+
const candidates = staleMcpAuthCandidates([
|
|
226
|
+
{
|
|
227
|
+
name: "supabase",
|
|
228
|
+
status: "needs-auth",
|
|
229
|
+
server_info: undefined,
|
|
230
|
+
error: undefined,
|
|
231
|
+
config: undefined,
|
|
232
|
+
scope: undefined,
|
|
233
|
+
tools: [],
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "notion",
|
|
237
|
+
status: "needs-auth",
|
|
238
|
+
server_info: undefined,
|
|
239
|
+
error: undefined,
|
|
240
|
+
config: undefined,
|
|
241
|
+
scope: undefined,
|
|
242
|
+
tools: [],
|
|
243
|
+
},
|
|
244
|
+
], new Set(["supabase"]), new Map(), 10_000, 1_000);
|
|
245
|
+
assert.deepEqual(candidates, ["supabase"]);
|
|
246
|
+
});
|
|
247
|
+
test("staleMcpAuthCandidates respects the revalidation cooldown", () => {
|
|
248
|
+
const candidates = staleMcpAuthCandidates([
|
|
249
|
+
{
|
|
250
|
+
name: "supabase",
|
|
251
|
+
status: "needs-auth",
|
|
252
|
+
server_info: undefined,
|
|
253
|
+
error: undefined,
|
|
254
|
+
config: undefined,
|
|
255
|
+
scope: undefined,
|
|
256
|
+
tools: [],
|
|
257
|
+
},
|
|
258
|
+
], new Set(["supabase"]), new Map([["supabase", 9_500]]), 10_000, 1_000);
|
|
259
|
+
assert.deepEqual(candidates, []);
|
|
260
|
+
});
|
|
261
|
+
test("buildSessionMutationOptions scopes rename requests to the session cwd", () => {
|
|
262
|
+
assert.deepEqual(buildSessionMutationOptions("C:/worktree"), { dir: "C:/worktree" });
|
|
263
|
+
assert.equal(buildSessionMutationOptions(undefined), undefined);
|
|
264
|
+
});
|
|
265
|
+
test("canGenerateSessionTitle detects supported query objects", () => {
|
|
266
|
+
const query = {
|
|
267
|
+
async generateSessionTitle() {
|
|
268
|
+
return "Generated";
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
assert.equal(canGenerateSessionTitle(query), true);
|
|
272
|
+
assert.equal(canGenerateSessionTitle({}), false);
|
|
273
|
+
});
|
|
274
|
+
test("generatePersistedSessionTitle calls sdk query with persist true", async () => {
|
|
275
|
+
const calls = [];
|
|
276
|
+
const query = {
|
|
277
|
+
async generateSessionTitle(description, options) {
|
|
278
|
+
calls.push({ description, persist: options?.persist });
|
|
279
|
+
return "Generated title";
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
const title = await generatePersistedSessionTitle(query, "Current summary");
|
|
283
|
+
assert.equal(title, "Generated title");
|
|
284
|
+
assert.deepEqual(calls, [{ description: "Current summary", persist: true }]);
|
|
41
285
|
});
|
|
42
286
|
test("buildQueryOptions maps launch settings into sdk query options", () => {
|
|
43
287
|
const input = new AsyncQueue();
|
|
44
288
|
const options = buildQueryOptions({
|
|
45
289
|
cwd: "C:/work",
|
|
46
290
|
launchSettings: {
|
|
47
|
-
model: "haiku",
|
|
48
291
|
language: "German",
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
292
|
+
settings: {
|
|
293
|
+
alwaysThinkingEnabled: true,
|
|
294
|
+
model: "haiku",
|
|
295
|
+
permissions: { defaultMode: "plan" },
|
|
296
|
+
fastMode: false,
|
|
297
|
+
effortLevel: "medium",
|
|
298
|
+
outputStyle: "Default",
|
|
299
|
+
spinnerTipsEnabled: true,
|
|
300
|
+
terminalProgressBarEnabled: true,
|
|
301
|
+
},
|
|
302
|
+
agent_progress_summaries: true,
|
|
52
303
|
},
|
|
53
304
|
provisionalSessionId: "session-1",
|
|
54
305
|
input,
|
|
@@ -57,26 +308,48 @@ test("buildQueryOptions maps launch settings into sdk query options", () => {
|
|
|
57
308
|
enableSpawnDebug: false,
|
|
58
309
|
sessionIdForLogs: () => "session-1",
|
|
59
310
|
});
|
|
60
|
-
assert.
|
|
311
|
+
assert.deepEqual(options.settings, {
|
|
312
|
+
alwaysThinkingEnabled: true,
|
|
313
|
+
model: "haiku",
|
|
314
|
+
permissions: { defaultMode: "plan" },
|
|
315
|
+
fastMode: false,
|
|
316
|
+
effortLevel: "medium",
|
|
317
|
+
outputStyle: "Default",
|
|
318
|
+
spinnerTipsEnabled: true,
|
|
319
|
+
terminalProgressBarEnabled: true,
|
|
320
|
+
});
|
|
61
321
|
assert.deepEqual(options.systemPrompt, {
|
|
62
322
|
type: "preset",
|
|
63
323
|
preset: "claude_code",
|
|
64
324
|
append: "Always respond to the user in German unless the user explicitly asks for a different language. " +
|
|
65
325
|
"Keep code, shell commands, file paths, API names, tool names, and raw error text unchanged unless the user explicitly asks for translation.",
|
|
66
326
|
});
|
|
327
|
+
assert.equal(options.model, "haiku");
|
|
67
328
|
assert.equal(options.permissionMode, "plan");
|
|
68
|
-
assert.
|
|
69
|
-
assert.equal(options
|
|
329
|
+
assert.equal("allowDangerouslySkipPermissions" in options, false);
|
|
330
|
+
assert.equal("thinking" in options, false);
|
|
331
|
+
assert.equal("effort" in options, false);
|
|
332
|
+
assert.equal(options.agentProgressSummaries, true);
|
|
70
333
|
assert.equal(options.sessionId, "session-1");
|
|
71
334
|
assert.deepEqual(options.settingSources, ["user", "project", "local"]);
|
|
335
|
+
assert.deepEqual(options.toolConfig, {
|
|
336
|
+
askUserQuestion: { previewFormat: "markdown" },
|
|
337
|
+
});
|
|
72
338
|
});
|
|
73
|
-
test("buildQueryOptions
|
|
339
|
+
test("buildQueryOptions forwards settings and maps startup model and permission mode", () => {
|
|
74
340
|
const input = new AsyncQueue();
|
|
75
341
|
const options = buildQueryOptions({
|
|
76
342
|
cwd: "C:/work",
|
|
77
343
|
launchSettings: {
|
|
78
|
-
|
|
79
|
-
|
|
344
|
+
settings: {
|
|
345
|
+
alwaysThinkingEnabled: false,
|
|
346
|
+
permissions: { defaultMode: "default" },
|
|
347
|
+
fastMode: true,
|
|
348
|
+
effortLevel: "high",
|
|
349
|
+
outputStyle: "Learning",
|
|
350
|
+
spinnerTipsEnabled: false,
|
|
351
|
+
terminalProgressBarEnabled: false,
|
|
352
|
+
},
|
|
80
353
|
},
|
|
81
354
|
provisionalSessionId: "session-3",
|
|
82
355
|
input,
|
|
@@ -85,9 +358,60 @@ test("buildQueryOptions maps disabled thinking mode into sdk query options", ()
|
|
|
85
358
|
enableSpawnDebug: false,
|
|
86
359
|
sessionIdForLogs: () => "session-3",
|
|
87
360
|
});
|
|
88
|
-
assert.deepEqual(options.
|
|
361
|
+
assert.deepEqual(options.settings, {
|
|
362
|
+
alwaysThinkingEnabled: false,
|
|
363
|
+
permissions: { defaultMode: "default" },
|
|
364
|
+
fastMode: true,
|
|
365
|
+
effortLevel: "high",
|
|
366
|
+
outputStyle: "Learning",
|
|
367
|
+
spinnerTipsEnabled: false,
|
|
368
|
+
terminalProgressBarEnabled: false,
|
|
369
|
+
});
|
|
370
|
+
assert.equal("model" in options, false);
|
|
371
|
+
assert.equal(options.permissionMode, "default");
|
|
372
|
+
assert.equal("allowDangerouslySkipPermissions" in options, false);
|
|
373
|
+
assert.equal("thinking" in options, false);
|
|
89
374
|
assert.equal("effort" in options, false);
|
|
90
375
|
});
|
|
376
|
+
test("buildQueryOptions trims startup model before passing sdk option", () => {
|
|
377
|
+
const input = new AsyncQueue();
|
|
378
|
+
const options = buildQueryOptions({
|
|
379
|
+
cwd: "C:/work",
|
|
380
|
+
launchSettings: {
|
|
381
|
+
settings: {
|
|
382
|
+
model: " claude-opus-4-6 ",
|
|
383
|
+
permissions: { defaultMode: "plan" },
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
provisionalSessionId: "session-model",
|
|
387
|
+
input,
|
|
388
|
+
canUseTool: async () => ({ behavior: "deny", message: "not used" }),
|
|
389
|
+
enableSdkDebug: false,
|
|
390
|
+
enableSpawnDebug: false,
|
|
391
|
+
sessionIdForLogs: () => "session-model",
|
|
392
|
+
});
|
|
393
|
+
assert.equal(options.model, "claude-opus-4-6");
|
|
394
|
+
assert.equal(options.permissionMode, "plan");
|
|
395
|
+
});
|
|
396
|
+
test("buildQueryOptions enables dangerous skip flag for bypass permissions startup mode", () => {
|
|
397
|
+
const input = new AsyncQueue();
|
|
398
|
+
const options = buildQueryOptions({
|
|
399
|
+
cwd: "C:/work",
|
|
400
|
+
launchSettings: {
|
|
401
|
+
settings: {
|
|
402
|
+
permissions: { defaultMode: "bypassPermissions" },
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
provisionalSessionId: "session-4",
|
|
406
|
+
input,
|
|
407
|
+
canUseTool: async () => ({ behavior: "deny", message: "not used" }),
|
|
408
|
+
enableSdkDebug: false,
|
|
409
|
+
enableSpawnDebug: false,
|
|
410
|
+
sessionIdForLogs: () => "session-4",
|
|
411
|
+
});
|
|
412
|
+
assert.equal(options.permissionMode, "bypassPermissions");
|
|
413
|
+
assert.equal(options.allowDangerouslySkipPermissions, true);
|
|
414
|
+
});
|
|
91
415
|
test("buildQueryOptions omits startup overrides for default logout path", () => {
|
|
92
416
|
const input = new AsyncQueue();
|
|
93
417
|
const options = buildQueryOptions({
|
|
@@ -102,7 +426,135 @@ test("buildQueryOptions omits startup overrides for default logout path", () =>
|
|
|
102
426
|
});
|
|
103
427
|
assert.equal("model" in options, false);
|
|
104
428
|
assert.equal("permissionMode" in options, false);
|
|
429
|
+
assert.equal("allowDangerouslySkipPermissions" in options, false);
|
|
105
430
|
assert.equal("systemPrompt" in options, false);
|
|
431
|
+
assert.equal("agentProgressSummaries" in options, false);
|
|
432
|
+
});
|
|
433
|
+
test("handleTaskSystemMessage prefers task_progress summary over fallback text", () => {
|
|
434
|
+
const session = makeSessionState();
|
|
435
|
+
const events = captureBridgeEvents(() => {
|
|
436
|
+
handleTaskSystemMessage(session, "task_started", {
|
|
437
|
+
task_id: "task-1",
|
|
438
|
+
tool_use_id: "tool-1",
|
|
439
|
+
description: "Initial task description",
|
|
440
|
+
});
|
|
441
|
+
handleTaskSystemMessage(session, "task_progress", {
|
|
442
|
+
task_id: "task-1",
|
|
443
|
+
summary: "Analyzing authentication flow",
|
|
444
|
+
description: "Should not be shown",
|
|
445
|
+
last_tool_name: "Read",
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
const lastEvent = events.at(-1);
|
|
449
|
+
assert.ok(lastEvent);
|
|
450
|
+
assert.equal(lastEvent.event, "session_update");
|
|
451
|
+
assert.deepEqual(lastEvent.update, {
|
|
452
|
+
type: "tool_call_update",
|
|
453
|
+
tool_call_update: {
|
|
454
|
+
tool_call_id: "tool-1",
|
|
455
|
+
fields: {
|
|
456
|
+
status: "in_progress",
|
|
457
|
+
raw_output: "Analyzing authentication flow",
|
|
458
|
+
content: [
|
|
459
|
+
{
|
|
460
|
+
type: "content",
|
|
461
|
+
content: { type: "text", text: "Analyzing authentication flow" },
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
test("handleTaskSystemMessage falls back to description and last tool when progress summary is absent", () => {
|
|
469
|
+
const session = makeSessionState();
|
|
470
|
+
const events = captureBridgeEvents(() => {
|
|
471
|
+
handleTaskSystemMessage(session, "task_started", {
|
|
472
|
+
task_id: "task-1",
|
|
473
|
+
tool_use_id: "tool-1",
|
|
474
|
+
description: "Initial task description",
|
|
475
|
+
});
|
|
476
|
+
handleTaskSystemMessage(session, "task_progress", {
|
|
477
|
+
task_id: "task-1",
|
|
478
|
+
description: "Inspecting auth code",
|
|
479
|
+
last_tool_name: "Read",
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
const lastEvent = events.at(-1);
|
|
483
|
+
assert.ok(lastEvent);
|
|
484
|
+
assert.equal(lastEvent.event, "session_update");
|
|
485
|
+
assert.deepEqual(lastEvent.update, {
|
|
486
|
+
type: "tool_call_update",
|
|
487
|
+
tool_call_update: {
|
|
488
|
+
tool_call_id: "tool-1",
|
|
489
|
+
fields: {
|
|
490
|
+
status: "in_progress",
|
|
491
|
+
raw_output: "Inspecting auth code (last tool: Read)",
|
|
492
|
+
content: [
|
|
493
|
+
{
|
|
494
|
+
type: "content",
|
|
495
|
+
content: { type: "text", text: "Inspecting auth code (last tool: Read)" },
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
test("handleTaskSystemMessage final summary replaces prior task content and finalizes status", () => {
|
|
503
|
+
const session = makeSessionState();
|
|
504
|
+
const events = captureBridgeEvents(() => {
|
|
505
|
+
handleTaskSystemMessage(session, "task_started", {
|
|
506
|
+
task_id: "task-1",
|
|
507
|
+
tool_use_id: "tool-1",
|
|
508
|
+
description: "Initial task description",
|
|
509
|
+
});
|
|
510
|
+
handleTaskSystemMessage(session, "task_progress", {
|
|
511
|
+
task_id: "task-1",
|
|
512
|
+
summary: "Analyzing authentication flow",
|
|
513
|
+
description: "Should not be shown",
|
|
514
|
+
});
|
|
515
|
+
handleTaskSystemMessage(session, "task_notification", {
|
|
516
|
+
task_id: "task-1",
|
|
517
|
+
status: "completed",
|
|
518
|
+
summary: "Found the auth bug and prepared the fix",
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
const lastEvent = events.at(-1);
|
|
522
|
+
assert.ok(lastEvent);
|
|
523
|
+
assert.equal(lastEvent.event, "session_update");
|
|
524
|
+
assert.deepEqual(lastEvent.update, {
|
|
525
|
+
type: "tool_call_update",
|
|
526
|
+
tool_call_update: {
|
|
527
|
+
tool_call_id: "tool-1",
|
|
528
|
+
fields: {
|
|
529
|
+
status: "completed",
|
|
530
|
+
raw_output: "Found the auth bug and prepared the fix",
|
|
531
|
+
content: [
|
|
532
|
+
{
|
|
533
|
+
type: "content",
|
|
534
|
+
content: { type: "text", text: "Found the auth bug and prepared the fix" },
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
assert.equal(session.taskToolUseIds.has("task-1"), false);
|
|
541
|
+
});
|
|
542
|
+
test("emitToolProgressUpdate does not reopen completed tools", () => {
|
|
543
|
+
const session = makeSessionState();
|
|
544
|
+
session.toolCalls.set("tool-1", {
|
|
545
|
+
tool_call_id: "tool-1",
|
|
546
|
+
title: "Bash",
|
|
547
|
+
kind: "execute",
|
|
548
|
+
status: "completed",
|
|
549
|
+
content: [],
|
|
550
|
+
locations: [],
|
|
551
|
+
meta: { claudeCode: { toolName: "Bash" } },
|
|
552
|
+
});
|
|
553
|
+
const events = captureBridgeEvents(() => {
|
|
554
|
+
emitToolProgressUpdate(session, "tool-1", "Bash");
|
|
555
|
+
});
|
|
556
|
+
assert.equal(events.length, 0);
|
|
557
|
+
assert.equal(session.toolCalls.get("tool-1")?.status, "completed");
|
|
106
558
|
});
|
|
107
559
|
test("buildQueryOptions trims language before appending system prompt", () => {
|
|
108
560
|
const input = new AsyncQueue();
|
|
@@ -128,6 +580,172 @@ test("buildQueryOptions trims language before appending system prompt", () => {
|
|
|
128
580
|
test("parseCommandEnvelope rejects missing required fields", () => {
|
|
129
581
|
assert.throws(() => parseCommandEnvelope(JSON.stringify({ command: "set_model", session_id: "s1" })), /set_model\.model must be a string/);
|
|
130
582
|
});
|
|
583
|
+
test("parseCommandEnvelope validates question_response command", () => {
|
|
584
|
+
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
585
|
+
request_id: "req-question",
|
|
586
|
+
command: "question_response",
|
|
587
|
+
session_id: "session-1",
|
|
588
|
+
tool_call_id: "tool-1",
|
|
589
|
+
outcome: {
|
|
590
|
+
outcome: "answered",
|
|
591
|
+
selected_option_ids: ["question_0", "question_2"],
|
|
592
|
+
annotation: {
|
|
593
|
+
preview: "Rendered preview",
|
|
594
|
+
notes: "User note",
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
}));
|
|
598
|
+
assert.equal(parsed.requestId, "req-question");
|
|
599
|
+
assert.equal(parsed.command.command, "question_response");
|
|
600
|
+
if (parsed.command.command !== "question_response") {
|
|
601
|
+
throw new Error("unexpected command variant");
|
|
602
|
+
}
|
|
603
|
+
assert.deepEqual(parsed.command.outcome, {
|
|
604
|
+
outcome: "answered",
|
|
605
|
+
selected_option_ids: ["question_0", "question_2"],
|
|
606
|
+
annotation: {
|
|
607
|
+
preview: "Rendered preview",
|
|
608
|
+
notes: "User note",
|
|
609
|
+
},
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
test("requestAskUserQuestionAnswers preserves previews and annotations in updated input", async () => {
|
|
613
|
+
const session = makeSessionState();
|
|
614
|
+
const baseToolCall = {
|
|
615
|
+
tool_call_id: "tool-question",
|
|
616
|
+
title: "AskUserQuestion",
|
|
617
|
+
kind: "other",
|
|
618
|
+
status: "in_progress",
|
|
619
|
+
content: [],
|
|
620
|
+
locations: [],
|
|
621
|
+
meta: { claudeCode: { toolName: "AskUserQuestion" } },
|
|
622
|
+
};
|
|
623
|
+
const events = await captureBridgeEventsAsync(async () => {
|
|
624
|
+
const resultPromise = requestAskUserQuestionAnswers(session, "tool-question", {
|
|
625
|
+
questions: [
|
|
626
|
+
{
|
|
627
|
+
question: "Pick deployment target",
|
|
628
|
+
header: "Target",
|
|
629
|
+
multiSelect: true,
|
|
630
|
+
options: [
|
|
631
|
+
{
|
|
632
|
+
label: "Staging",
|
|
633
|
+
description: "Low-risk validation",
|
|
634
|
+
preview: "Deploy to staging first.",
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
label: "Production",
|
|
638
|
+
description: "Customer-facing rollout",
|
|
639
|
+
preview: "Deploy to production after approval.",
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
},
|
|
643
|
+
],
|
|
644
|
+
}, baseToolCall);
|
|
645
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
646
|
+
const pending = session.pendingQuestions.get("tool-question");
|
|
647
|
+
assert.ok(pending, "expected pending question");
|
|
648
|
+
pending.onOutcome({
|
|
649
|
+
outcome: "answered",
|
|
650
|
+
selected_option_ids: ["question_0", "question_1"],
|
|
651
|
+
annotation: {
|
|
652
|
+
notes: "Roll out in both environments",
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
const result = await resultPromise;
|
|
656
|
+
assert.equal(result.behavior, "allow");
|
|
657
|
+
if (result.behavior !== "allow") {
|
|
658
|
+
throw new Error("expected allow result");
|
|
659
|
+
}
|
|
660
|
+
assert.deepEqual(result.updatedInput, {
|
|
661
|
+
questions: [
|
|
662
|
+
{
|
|
663
|
+
question: "Pick deployment target",
|
|
664
|
+
header: "Target",
|
|
665
|
+
multiSelect: true,
|
|
666
|
+
options: [
|
|
667
|
+
{
|
|
668
|
+
label: "Staging",
|
|
669
|
+
description: "Low-risk validation",
|
|
670
|
+
preview: "Deploy to staging first.",
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
label: "Production",
|
|
674
|
+
description: "Customer-facing rollout",
|
|
675
|
+
preview: "Deploy to production after approval.",
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
},
|
|
679
|
+
],
|
|
680
|
+
answers: {
|
|
681
|
+
"Pick deployment target": "Staging, Production",
|
|
682
|
+
},
|
|
683
|
+
annotations: {
|
|
684
|
+
"Pick deployment target": {
|
|
685
|
+
preview: "Deploy to staging first.\n\nDeploy to production after approval.",
|
|
686
|
+
notes: "Roll out in both environments",
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
const questionEvent = events.find((event) => event.event === "question_request");
|
|
692
|
+
assert.ok(questionEvent, "expected question request event");
|
|
693
|
+
assert.deepEqual(questionEvent.request, {
|
|
694
|
+
tool_call: {
|
|
695
|
+
tool_call_id: "tool-question",
|
|
696
|
+
title: "Pick deployment target",
|
|
697
|
+
kind: "other",
|
|
698
|
+
status: "in_progress",
|
|
699
|
+
content: [],
|
|
700
|
+
locations: [],
|
|
701
|
+
meta: { claudeCode: { toolName: "AskUserQuestion" } },
|
|
702
|
+
raw_input: {
|
|
703
|
+
prompt: {
|
|
704
|
+
question: "Pick deployment target",
|
|
705
|
+
header: "Target",
|
|
706
|
+
multi_select: true,
|
|
707
|
+
options: [
|
|
708
|
+
{
|
|
709
|
+
option_id: "question_0",
|
|
710
|
+
label: "Staging",
|
|
711
|
+
description: "Low-risk validation",
|
|
712
|
+
preview: "Deploy to staging first.",
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
option_id: "question_1",
|
|
716
|
+
label: "Production",
|
|
717
|
+
description: "Customer-facing rollout",
|
|
718
|
+
preview: "Deploy to production after approval.",
|
|
719
|
+
},
|
|
720
|
+
],
|
|
721
|
+
},
|
|
722
|
+
question_index: 0,
|
|
723
|
+
total_questions: 1,
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
prompt: {
|
|
727
|
+
question: "Pick deployment target",
|
|
728
|
+
header: "Target",
|
|
729
|
+
multi_select: true,
|
|
730
|
+
options: [
|
|
731
|
+
{
|
|
732
|
+
option_id: "question_0",
|
|
733
|
+
label: "Staging",
|
|
734
|
+
description: "Low-risk validation",
|
|
735
|
+
preview: "Deploy to staging first.",
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
option_id: "question_1",
|
|
739
|
+
label: "Production",
|
|
740
|
+
description: "Customer-facing rollout",
|
|
741
|
+
preview: "Deploy to production after approval.",
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
},
|
|
745
|
+
question_index: 0,
|
|
746
|
+
total_questions: 1,
|
|
747
|
+
});
|
|
748
|
+
});
|
|
131
749
|
test("normalizeToolKind maps known tool names", () => {
|
|
132
750
|
assert.equal(normalizeToolKind("Bash"), "execute");
|
|
133
751
|
assert.equal(normalizeToolKind("Delete"), "delete");
|
|
@@ -324,6 +942,9 @@ test("buildToolResultFields maps structured Write output to diff content", () =>
|
|
|
324
942
|
content: "new",
|
|
325
943
|
originalFile: "old",
|
|
326
944
|
structuredPatch: [],
|
|
945
|
+
gitDiff: {
|
|
946
|
+
repository: "acme/project",
|
|
947
|
+
},
|
|
327
948
|
}, base);
|
|
328
949
|
assert.equal(fields.status, "completed");
|
|
329
950
|
assert.deepEqual(fields.content, [
|
|
@@ -333,16 +954,24 @@ test("buildToolResultFields maps structured Write output to diff content", () =>
|
|
|
333
954
|
new_path: "src/main.ts",
|
|
334
955
|
old: "old",
|
|
335
956
|
new: "new",
|
|
957
|
+
repository: "acme/project",
|
|
336
958
|
},
|
|
337
959
|
]);
|
|
338
960
|
});
|
|
339
|
-
test("buildToolResultFields preserves Edit diff content from input", () => {
|
|
961
|
+
test("buildToolResultFields preserves Edit diff content from input and structured repository", () => {
|
|
340
962
|
const base = createToolCall("tc-e", "Edit", {
|
|
341
963
|
file_path: "src/main.ts",
|
|
342
964
|
old_string: "old",
|
|
343
965
|
new_string: "new",
|
|
344
966
|
});
|
|
345
|
-
const fields = buildToolResultFields(false, [{ text: "Updated successfully" }], base
|
|
967
|
+
const fields = buildToolResultFields(false, [{ text: "Updated successfully" }], base, {
|
|
968
|
+
result: {
|
|
969
|
+
filePath: "src/main.ts",
|
|
970
|
+
gitDiff: {
|
|
971
|
+
repository: "acme/project",
|
|
972
|
+
},
|
|
973
|
+
},
|
|
974
|
+
});
|
|
346
975
|
assert.equal(fields.status, "completed");
|
|
347
976
|
assert.deepEqual(fields.content, [
|
|
348
977
|
{
|
|
@@ -351,6 +980,120 @@ test("buildToolResultFields preserves Edit diff content from input", () => {
|
|
|
351
980
|
new_path: "src/main.ts",
|
|
352
981
|
old: "old",
|
|
353
982
|
new: "new",
|
|
983
|
+
repository: "acme/project",
|
|
984
|
+
},
|
|
985
|
+
]);
|
|
986
|
+
});
|
|
987
|
+
test("buildToolResultFields prefers structured Bash stdout over token-saver output", () => {
|
|
988
|
+
const base = createToolCall("tc-bash", "Bash", { command: "npm test" });
|
|
989
|
+
const fields = buildToolResultFields(false, {
|
|
990
|
+
stdout: "real stdout",
|
|
991
|
+
stderr: "",
|
|
992
|
+
interrupted: false,
|
|
993
|
+
tokenSaverOutput: "compressed output for model",
|
|
994
|
+
}, base, {
|
|
995
|
+
result: {
|
|
996
|
+
stdout: "real stdout",
|
|
997
|
+
stderr: "",
|
|
998
|
+
interrupted: false,
|
|
999
|
+
tokenSaverOutput: "compressed output for model",
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
1002
|
+
assert.equal(fields.raw_output, "real stdout");
|
|
1003
|
+
assert.deepEqual(fields.output_metadata, {
|
|
1004
|
+
bash: {
|
|
1005
|
+
token_saver_active: true,
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
1008
|
+
});
|
|
1009
|
+
test("buildToolResultFields adds Bash auto-backgrounded metadata and message", () => {
|
|
1010
|
+
const base = createToolCall("tc-bash-bg", "Bash", { command: "npm run watch" });
|
|
1011
|
+
const fields = buildToolResultFields(false, {
|
|
1012
|
+
stdout: "",
|
|
1013
|
+
stderr: "",
|
|
1014
|
+
interrupted: false,
|
|
1015
|
+
backgroundTaskId: "task-42",
|
|
1016
|
+
assistantAutoBackgrounded: true,
|
|
1017
|
+
}, base, {
|
|
1018
|
+
result: {
|
|
1019
|
+
stdout: "",
|
|
1020
|
+
stderr: "",
|
|
1021
|
+
interrupted: false,
|
|
1022
|
+
backgroundTaskId: "task-42",
|
|
1023
|
+
assistantAutoBackgrounded: true,
|
|
1024
|
+
},
|
|
1025
|
+
});
|
|
1026
|
+
assert.equal(fields.raw_output, "Command was auto-backgrounded by assistant mode with ID: task-42.");
|
|
1027
|
+
assert.deepEqual(fields.output_metadata, {
|
|
1028
|
+
bash: {
|
|
1029
|
+
assistant_auto_backgrounded: true,
|
|
1030
|
+
},
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
test("buildToolResultFields maps structured ReadMcpResource output to typed resource content", () => {
|
|
1034
|
+
const base = createToolCall("tc-mcp", "ReadMcpResource", {
|
|
1035
|
+
server: "docs",
|
|
1036
|
+
uri: "file://manual.pdf",
|
|
1037
|
+
});
|
|
1038
|
+
const fields = buildToolResultFields(false, {
|
|
1039
|
+
contents: [
|
|
1040
|
+
{
|
|
1041
|
+
uri: "file://manual.pdf",
|
|
1042
|
+
mimeType: "application/pdf",
|
|
1043
|
+
text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
|
|
1044
|
+
blobSavedTo: "C:\\tmp\\manual.pdf",
|
|
1045
|
+
},
|
|
1046
|
+
],
|
|
1047
|
+
}, base, {
|
|
1048
|
+
result: {
|
|
1049
|
+
contents: [
|
|
1050
|
+
{
|
|
1051
|
+
uri: "file://manual.pdf",
|
|
1052
|
+
mimeType: "application/pdf",
|
|
1053
|
+
text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
|
|
1054
|
+
blobSavedTo: "C:\\tmp\\manual.pdf",
|
|
1055
|
+
},
|
|
1056
|
+
],
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
1059
|
+
assert.equal(fields.status, "completed");
|
|
1060
|
+
assert.deepEqual(fields.content, [
|
|
1061
|
+
{
|
|
1062
|
+
type: "mcp_resource",
|
|
1063
|
+
uri: "file://manual.pdf",
|
|
1064
|
+
mime_type: "application/pdf",
|
|
1065
|
+
text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
|
|
1066
|
+
blob_saved_to: "C:\\tmp\\manual.pdf",
|
|
1067
|
+
},
|
|
1068
|
+
]);
|
|
1069
|
+
});
|
|
1070
|
+
test("buildToolResultFields restores ReadMcpResource blob paths from transcript JSON text", () => {
|
|
1071
|
+
const base = createToolCall("tc-mcp-history", "ReadMcpResource", {
|
|
1072
|
+
server: "docs",
|
|
1073
|
+
uri: "file://manual.pdf",
|
|
1074
|
+
});
|
|
1075
|
+
const transcriptJson = JSON.stringify({
|
|
1076
|
+
contents: [
|
|
1077
|
+
{
|
|
1078
|
+
uri: "file://manual.pdf",
|
|
1079
|
+
mimeType: "application/pdf",
|
|
1080
|
+
text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
|
|
1081
|
+
blobSavedTo: "C:\\tmp\\manual.pdf",
|
|
1082
|
+
},
|
|
1083
|
+
],
|
|
1084
|
+
});
|
|
1085
|
+
const fields = buildToolResultFields(false, transcriptJson, base, {
|
|
1086
|
+
type: "tool_result",
|
|
1087
|
+
tool_use_id: "tc-mcp-history",
|
|
1088
|
+
content: transcriptJson,
|
|
1089
|
+
});
|
|
1090
|
+
assert.deepEqual(fields.content, [
|
|
1091
|
+
{
|
|
1092
|
+
type: "mcp_resource",
|
|
1093
|
+
uri: "file://manual.pdf",
|
|
1094
|
+
mime_type: "application/pdf",
|
|
1095
|
+
text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
|
|
1096
|
+
blob_saved_to: "C:\\tmp\\manual.pdf",
|
|
354
1097
|
},
|
|
355
1098
|
]);
|
|
356
1099
|
});
|
|
@@ -492,7 +1235,7 @@ test("looksLikeAuthRequired detects login hints", () => {
|
|
|
492
1235
|
assert.equal(looksLikeAuthRequired("normal tool output"), false);
|
|
493
1236
|
});
|
|
494
1237
|
test("agent sdk version compatibility check matches pinned version", () => {
|
|
495
|
-
assert.equal(resolveInstalledAgentSdkVersion(), "0.2.
|
|
1238
|
+
assert.equal(resolveInstalledAgentSdkVersion(), "0.2.74");
|
|
496
1239
|
assert.equal(agentSdkVersionCompatibilityError(), undefined);
|
|
497
1240
|
});
|
|
498
1241
|
test("mapSessionMessagesToUpdates maps message content blocks", () => {
|
|
@@ -607,3 +1350,83 @@ test("mapSdkSessions normalizes and sorts sessions", () => {
|
|
|
607
1350
|
},
|
|
608
1351
|
]);
|
|
609
1352
|
});
|
|
1353
|
+
test("buildSessionListOptions scopes repo-local listings to worktrees", () => {
|
|
1354
|
+
assert.deepEqual(buildSessionListOptions("C:/repo"), {
|
|
1355
|
+
dir: "C:/repo",
|
|
1356
|
+
includeWorktrees: true,
|
|
1357
|
+
limit: 50,
|
|
1358
|
+
});
|
|
1359
|
+
assert.deepEqual(buildSessionListOptions(undefined), {
|
|
1360
|
+
limit: 50,
|
|
1361
|
+
});
|
|
1362
|
+
});
|
|
1363
|
+
test("buildToolResultFields extracts ExitPlanMode ultraplan metadata from structured results", () => {
|
|
1364
|
+
const base = createToolCall("tc-plan", "ExitPlanMode", {});
|
|
1365
|
+
const fields = buildToolResultFields(false, [{ text: "Plan ready for approval" }], base, {
|
|
1366
|
+
result: {
|
|
1367
|
+
plan: "Plan contents",
|
|
1368
|
+
isUltraplan: true,
|
|
1369
|
+
},
|
|
1370
|
+
});
|
|
1371
|
+
assert.deepEqual(fields.output_metadata, {
|
|
1372
|
+
exit_plan_mode: {
|
|
1373
|
+
is_ultraplan: true,
|
|
1374
|
+
},
|
|
1375
|
+
});
|
|
1376
|
+
});
|
|
1377
|
+
test("buildToolResultFields extracts TodoWrite verification metadata from structured results", () => {
|
|
1378
|
+
const base = createToolCall("tc-todo", "TodoWrite", {
|
|
1379
|
+
todos: [{ content: "Verify changes", status: "pending", activeForm: "Verifying changes" }],
|
|
1380
|
+
});
|
|
1381
|
+
const fields = buildToolResultFields(false, [{ text: "Todos have been modified successfully." }], base, {
|
|
1382
|
+
data: {
|
|
1383
|
+
oldTodos: [],
|
|
1384
|
+
newTodos: [],
|
|
1385
|
+
verificationNudgeNeeded: true,
|
|
1386
|
+
},
|
|
1387
|
+
});
|
|
1388
|
+
assert.deepEqual(fields.output_metadata, {
|
|
1389
|
+
todo_write: {
|
|
1390
|
+
verification_nudge_needed: true,
|
|
1391
|
+
},
|
|
1392
|
+
});
|
|
1393
|
+
});
|
|
1394
|
+
test("mapAvailableModels preserves optional fast and auto mode metadata", () => {
|
|
1395
|
+
const mapped = mapAvailableModels([
|
|
1396
|
+
{
|
|
1397
|
+
value: "sonnet",
|
|
1398
|
+
displayName: "Claude Sonnet",
|
|
1399
|
+
description: "Balanced model",
|
|
1400
|
+
supportsEffort: true,
|
|
1401
|
+
supportedEffortLevels: ["low", "medium", "high", "max"],
|
|
1402
|
+
supportsAdaptiveThinking: true,
|
|
1403
|
+
supportsFastMode: true,
|
|
1404
|
+
supportsAutoMode: false,
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
value: "haiku",
|
|
1408
|
+
displayName: "Claude Haiku",
|
|
1409
|
+
description: "Fast model",
|
|
1410
|
+
supportsEffort: false,
|
|
1411
|
+
},
|
|
1412
|
+
]);
|
|
1413
|
+
assert.deepEqual(mapped, [
|
|
1414
|
+
{
|
|
1415
|
+
id: "sonnet",
|
|
1416
|
+
display_name: "Claude Sonnet",
|
|
1417
|
+
description: "Balanced model",
|
|
1418
|
+
supports_effort: true,
|
|
1419
|
+
supported_effort_levels: ["low", "medium", "high"],
|
|
1420
|
+
supports_adaptive_thinking: true,
|
|
1421
|
+
supports_fast_mode: true,
|
|
1422
|
+
supports_auto_mode: false,
|
|
1423
|
+
},
|
|
1424
|
+
{
|
|
1425
|
+
id: "haiku",
|
|
1426
|
+
display_name: "Claude Haiku",
|
|
1427
|
+
description: "Fast model",
|
|
1428
|
+
supports_effort: false,
|
|
1429
|
+
supported_effort_levels: [],
|
|
1430
|
+
},
|
|
1431
|
+
]);
|
|
1432
|
+
});
|