claude-code-rust 0.7.0 → 0.8.0
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 +10 -13
- package/agent-sdk/README.md +17 -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 +116 -43
- 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 +809 -28
- package/bin/claude-rs.js +1 -1
- package/package.json +8 -7
- package/scripts/postinstall.js +1 -1
|
@@ -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,47 @@ 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
|
});
|
|
67
|
-
assert.equal(options
|
|
68
|
-
assert.
|
|
69
|
-
assert.equal(options
|
|
327
|
+
assert.equal("model" in options, false);
|
|
328
|
+
assert.equal("permissionMode" in options, false);
|
|
329
|
+
assert.equal("thinking" in options, false);
|
|
330
|
+
assert.equal("effort" in options, false);
|
|
331
|
+
assert.equal(options.agentProgressSummaries, true);
|
|
70
332
|
assert.equal(options.sessionId, "session-1");
|
|
71
333
|
assert.deepEqual(options.settingSources, ["user", "project", "local"]);
|
|
334
|
+
assert.deepEqual(options.toolConfig, {
|
|
335
|
+
askUserQuestion: { previewFormat: "markdown" },
|
|
336
|
+
});
|
|
72
337
|
});
|
|
73
|
-
test("buildQueryOptions
|
|
338
|
+
test("buildQueryOptions forwards settings without direct model and permission flags", () => {
|
|
74
339
|
const input = new AsyncQueue();
|
|
75
340
|
const options = buildQueryOptions({
|
|
76
341
|
cwd: "C:/work",
|
|
77
342
|
launchSettings: {
|
|
78
|
-
|
|
79
|
-
|
|
343
|
+
settings: {
|
|
344
|
+
alwaysThinkingEnabled: false,
|
|
345
|
+
permissions: { defaultMode: "default" },
|
|
346
|
+
fastMode: true,
|
|
347
|
+
effortLevel: "high",
|
|
348
|
+
outputStyle: "Learning",
|
|
349
|
+
spinnerTipsEnabled: false,
|
|
350
|
+
terminalProgressBarEnabled: false,
|
|
351
|
+
},
|
|
80
352
|
},
|
|
81
353
|
provisionalSessionId: "session-3",
|
|
82
354
|
input,
|
|
@@ -85,7 +357,18 @@ test("buildQueryOptions maps disabled thinking mode into sdk query options", ()
|
|
|
85
357
|
enableSpawnDebug: false,
|
|
86
358
|
sessionIdForLogs: () => "session-3",
|
|
87
359
|
});
|
|
88
|
-
assert.deepEqual(options.
|
|
360
|
+
assert.deepEqual(options.settings, {
|
|
361
|
+
alwaysThinkingEnabled: false,
|
|
362
|
+
permissions: { defaultMode: "default" },
|
|
363
|
+
fastMode: true,
|
|
364
|
+
effortLevel: "high",
|
|
365
|
+
outputStyle: "Learning",
|
|
366
|
+
spinnerTipsEnabled: false,
|
|
367
|
+
terminalProgressBarEnabled: false,
|
|
368
|
+
});
|
|
369
|
+
assert.equal("model" in options, false);
|
|
370
|
+
assert.equal("permissionMode" in options, false);
|
|
371
|
+
assert.equal("thinking" in options, false);
|
|
89
372
|
assert.equal("effort" in options, false);
|
|
90
373
|
});
|
|
91
374
|
test("buildQueryOptions omits startup overrides for default logout path", () => {
|
|
@@ -103,6 +386,133 @@ test("buildQueryOptions omits startup overrides for default logout path", () =>
|
|
|
103
386
|
assert.equal("model" in options, false);
|
|
104
387
|
assert.equal("permissionMode" in options, false);
|
|
105
388
|
assert.equal("systemPrompt" in options, false);
|
|
389
|
+
assert.equal("agentProgressSummaries" in options, false);
|
|
390
|
+
});
|
|
391
|
+
test("handleTaskSystemMessage prefers task_progress summary over fallback text", () => {
|
|
392
|
+
const session = makeSessionState();
|
|
393
|
+
const events = captureBridgeEvents(() => {
|
|
394
|
+
handleTaskSystemMessage(session, "task_started", {
|
|
395
|
+
task_id: "task-1",
|
|
396
|
+
tool_use_id: "tool-1",
|
|
397
|
+
description: "Initial task description",
|
|
398
|
+
});
|
|
399
|
+
handleTaskSystemMessage(session, "task_progress", {
|
|
400
|
+
task_id: "task-1",
|
|
401
|
+
summary: "Analyzing authentication flow",
|
|
402
|
+
description: "Should not be shown",
|
|
403
|
+
last_tool_name: "Read",
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
const lastEvent = events.at(-1);
|
|
407
|
+
assert.ok(lastEvent);
|
|
408
|
+
assert.equal(lastEvent.event, "session_update");
|
|
409
|
+
assert.deepEqual(lastEvent.update, {
|
|
410
|
+
type: "tool_call_update",
|
|
411
|
+
tool_call_update: {
|
|
412
|
+
tool_call_id: "tool-1",
|
|
413
|
+
fields: {
|
|
414
|
+
status: "in_progress",
|
|
415
|
+
raw_output: "Analyzing authentication flow",
|
|
416
|
+
content: [
|
|
417
|
+
{
|
|
418
|
+
type: "content",
|
|
419
|
+
content: { type: "text", text: "Analyzing authentication flow" },
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
test("handleTaskSystemMessage falls back to description and last tool when progress summary is absent", () => {
|
|
427
|
+
const session = makeSessionState();
|
|
428
|
+
const events = captureBridgeEvents(() => {
|
|
429
|
+
handleTaskSystemMessage(session, "task_started", {
|
|
430
|
+
task_id: "task-1",
|
|
431
|
+
tool_use_id: "tool-1",
|
|
432
|
+
description: "Initial task description",
|
|
433
|
+
});
|
|
434
|
+
handleTaskSystemMessage(session, "task_progress", {
|
|
435
|
+
task_id: "task-1",
|
|
436
|
+
description: "Inspecting auth code",
|
|
437
|
+
last_tool_name: "Read",
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
const lastEvent = events.at(-1);
|
|
441
|
+
assert.ok(lastEvent);
|
|
442
|
+
assert.equal(lastEvent.event, "session_update");
|
|
443
|
+
assert.deepEqual(lastEvent.update, {
|
|
444
|
+
type: "tool_call_update",
|
|
445
|
+
tool_call_update: {
|
|
446
|
+
tool_call_id: "tool-1",
|
|
447
|
+
fields: {
|
|
448
|
+
status: "in_progress",
|
|
449
|
+
raw_output: "Inspecting auth code (last tool: Read)",
|
|
450
|
+
content: [
|
|
451
|
+
{
|
|
452
|
+
type: "content",
|
|
453
|
+
content: { type: "text", text: "Inspecting auth code (last tool: Read)" },
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
test("handleTaskSystemMessage final summary replaces prior task content and finalizes status", () => {
|
|
461
|
+
const session = makeSessionState();
|
|
462
|
+
const events = captureBridgeEvents(() => {
|
|
463
|
+
handleTaskSystemMessage(session, "task_started", {
|
|
464
|
+
task_id: "task-1",
|
|
465
|
+
tool_use_id: "tool-1",
|
|
466
|
+
description: "Initial task description",
|
|
467
|
+
});
|
|
468
|
+
handleTaskSystemMessage(session, "task_progress", {
|
|
469
|
+
task_id: "task-1",
|
|
470
|
+
summary: "Analyzing authentication flow",
|
|
471
|
+
description: "Should not be shown",
|
|
472
|
+
});
|
|
473
|
+
handleTaskSystemMessage(session, "task_notification", {
|
|
474
|
+
task_id: "task-1",
|
|
475
|
+
status: "completed",
|
|
476
|
+
summary: "Found the auth bug and prepared the fix",
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
const lastEvent = events.at(-1);
|
|
480
|
+
assert.ok(lastEvent);
|
|
481
|
+
assert.equal(lastEvent.event, "session_update");
|
|
482
|
+
assert.deepEqual(lastEvent.update, {
|
|
483
|
+
type: "tool_call_update",
|
|
484
|
+
tool_call_update: {
|
|
485
|
+
tool_call_id: "tool-1",
|
|
486
|
+
fields: {
|
|
487
|
+
status: "completed",
|
|
488
|
+
raw_output: "Found the auth bug and prepared the fix",
|
|
489
|
+
content: [
|
|
490
|
+
{
|
|
491
|
+
type: "content",
|
|
492
|
+
content: { type: "text", text: "Found the auth bug and prepared the fix" },
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
assert.equal(session.taskToolUseIds.has("task-1"), false);
|
|
499
|
+
});
|
|
500
|
+
test("emitToolProgressUpdate does not reopen completed tools", () => {
|
|
501
|
+
const session = makeSessionState();
|
|
502
|
+
session.toolCalls.set("tool-1", {
|
|
503
|
+
tool_call_id: "tool-1",
|
|
504
|
+
title: "Bash",
|
|
505
|
+
kind: "execute",
|
|
506
|
+
status: "completed",
|
|
507
|
+
content: [],
|
|
508
|
+
locations: [],
|
|
509
|
+
meta: { claudeCode: { toolName: "Bash" } },
|
|
510
|
+
});
|
|
511
|
+
const events = captureBridgeEvents(() => {
|
|
512
|
+
emitToolProgressUpdate(session, "tool-1", "Bash");
|
|
513
|
+
});
|
|
514
|
+
assert.equal(events.length, 0);
|
|
515
|
+
assert.equal(session.toolCalls.get("tool-1")?.status, "completed");
|
|
106
516
|
});
|
|
107
517
|
test("buildQueryOptions trims language before appending system prompt", () => {
|
|
108
518
|
const input = new AsyncQueue();
|
|
@@ -128,6 +538,172 @@ test("buildQueryOptions trims language before appending system prompt", () => {
|
|
|
128
538
|
test("parseCommandEnvelope rejects missing required fields", () => {
|
|
129
539
|
assert.throws(() => parseCommandEnvelope(JSON.stringify({ command: "set_model", session_id: "s1" })), /set_model\.model must be a string/);
|
|
130
540
|
});
|
|
541
|
+
test("parseCommandEnvelope validates question_response command", () => {
|
|
542
|
+
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
543
|
+
request_id: "req-question",
|
|
544
|
+
command: "question_response",
|
|
545
|
+
session_id: "session-1",
|
|
546
|
+
tool_call_id: "tool-1",
|
|
547
|
+
outcome: {
|
|
548
|
+
outcome: "answered",
|
|
549
|
+
selected_option_ids: ["question_0", "question_2"],
|
|
550
|
+
annotation: {
|
|
551
|
+
preview: "Rendered preview",
|
|
552
|
+
notes: "User note",
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
}));
|
|
556
|
+
assert.equal(parsed.requestId, "req-question");
|
|
557
|
+
assert.equal(parsed.command.command, "question_response");
|
|
558
|
+
if (parsed.command.command !== "question_response") {
|
|
559
|
+
throw new Error("unexpected command variant");
|
|
560
|
+
}
|
|
561
|
+
assert.deepEqual(parsed.command.outcome, {
|
|
562
|
+
outcome: "answered",
|
|
563
|
+
selected_option_ids: ["question_0", "question_2"],
|
|
564
|
+
annotation: {
|
|
565
|
+
preview: "Rendered preview",
|
|
566
|
+
notes: "User note",
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
test("requestAskUserQuestionAnswers preserves previews and annotations in updated input", async () => {
|
|
571
|
+
const session = makeSessionState();
|
|
572
|
+
const baseToolCall = {
|
|
573
|
+
tool_call_id: "tool-question",
|
|
574
|
+
title: "AskUserQuestion",
|
|
575
|
+
kind: "other",
|
|
576
|
+
status: "in_progress",
|
|
577
|
+
content: [],
|
|
578
|
+
locations: [],
|
|
579
|
+
meta: { claudeCode: { toolName: "AskUserQuestion" } },
|
|
580
|
+
};
|
|
581
|
+
const events = await captureBridgeEventsAsync(async () => {
|
|
582
|
+
const resultPromise = requestAskUserQuestionAnswers(session, "tool-question", {
|
|
583
|
+
questions: [
|
|
584
|
+
{
|
|
585
|
+
question: "Pick deployment target",
|
|
586
|
+
header: "Target",
|
|
587
|
+
multiSelect: true,
|
|
588
|
+
options: [
|
|
589
|
+
{
|
|
590
|
+
label: "Staging",
|
|
591
|
+
description: "Low-risk validation",
|
|
592
|
+
preview: "Deploy to staging first.",
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
label: "Production",
|
|
596
|
+
description: "Customer-facing rollout",
|
|
597
|
+
preview: "Deploy to production after approval.",
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
}, baseToolCall);
|
|
603
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
604
|
+
const pending = session.pendingQuestions.get("tool-question");
|
|
605
|
+
assert.ok(pending, "expected pending question");
|
|
606
|
+
pending.onOutcome({
|
|
607
|
+
outcome: "answered",
|
|
608
|
+
selected_option_ids: ["question_0", "question_1"],
|
|
609
|
+
annotation: {
|
|
610
|
+
notes: "Roll out in both environments",
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
const result = await resultPromise;
|
|
614
|
+
assert.equal(result.behavior, "allow");
|
|
615
|
+
if (result.behavior !== "allow") {
|
|
616
|
+
throw new Error("expected allow result");
|
|
617
|
+
}
|
|
618
|
+
assert.deepEqual(result.updatedInput, {
|
|
619
|
+
questions: [
|
|
620
|
+
{
|
|
621
|
+
question: "Pick deployment target",
|
|
622
|
+
header: "Target",
|
|
623
|
+
multiSelect: true,
|
|
624
|
+
options: [
|
|
625
|
+
{
|
|
626
|
+
label: "Staging",
|
|
627
|
+
description: "Low-risk validation",
|
|
628
|
+
preview: "Deploy to staging first.",
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
label: "Production",
|
|
632
|
+
description: "Customer-facing rollout",
|
|
633
|
+
preview: "Deploy to production after approval.",
|
|
634
|
+
},
|
|
635
|
+
],
|
|
636
|
+
},
|
|
637
|
+
],
|
|
638
|
+
answers: {
|
|
639
|
+
"Pick deployment target": "Staging, Production",
|
|
640
|
+
},
|
|
641
|
+
annotations: {
|
|
642
|
+
"Pick deployment target": {
|
|
643
|
+
preview: "Deploy to staging first.\n\nDeploy to production after approval.",
|
|
644
|
+
notes: "Roll out in both environments",
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
const questionEvent = events.find((event) => event.event === "question_request");
|
|
650
|
+
assert.ok(questionEvent, "expected question request event");
|
|
651
|
+
assert.deepEqual(questionEvent.request, {
|
|
652
|
+
tool_call: {
|
|
653
|
+
tool_call_id: "tool-question",
|
|
654
|
+
title: "Pick deployment target",
|
|
655
|
+
kind: "other",
|
|
656
|
+
status: "in_progress",
|
|
657
|
+
content: [],
|
|
658
|
+
locations: [],
|
|
659
|
+
meta: { claudeCode: { toolName: "AskUserQuestion" } },
|
|
660
|
+
raw_input: {
|
|
661
|
+
prompt: {
|
|
662
|
+
question: "Pick deployment target",
|
|
663
|
+
header: "Target",
|
|
664
|
+
multi_select: true,
|
|
665
|
+
options: [
|
|
666
|
+
{
|
|
667
|
+
option_id: "question_0",
|
|
668
|
+
label: "Staging",
|
|
669
|
+
description: "Low-risk validation",
|
|
670
|
+
preview: "Deploy to staging first.",
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
option_id: "question_1",
|
|
674
|
+
label: "Production",
|
|
675
|
+
description: "Customer-facing rollout",
|
|
676
|
+
preview: "Deploy to production after approval.",
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
},
|
|
680
|
+
question_index: 0,
|
|
681
|
+
total_questions: 1,
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
prompt: {
|
|
685
|
+
question: "Pick deployment target",
|
|
686
|
+
header: "Target",
|
|
687
|
+
multi_select: true,
|
|
688
|
+
options: [
|
|
689
|
+
{
|
|
690
|
+
option_id: "question_0",
|
|
691
|
+
label: "Staging",
|
|
692
|
+
description: "Low-risk validation",
|
|
693
|
+
preview: "Deploy to staging first.",
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
option_id: "question_1",
|
|
697
|
+
label: "Production",
|
|
698
|
+
description: "Customer-facing rollout",
|
|
699
|
+
preview: "Deploy to production after approval.",
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
},
|
|
703
|
+
question_index: 0,
|
|
704
|
+
total_questions: 1,
|
|
705
|
+
});
|
|
706
|
+
});
|
|
131
707
|
test("normalizeToolKind maps known tool names", () => {
|
|
132
708
|
assert.equal(normalizeToolKind("Bash"), "execute");
|
|
133
709
|
assert.equal(normalizeToolKind("Delete"), "delete");
|
|
@@ -324,6 +900,9 @@ test("buildToolResultFields maps structured Write output to diff content", () =>
|
|
|
324
900
|
content: "new",
|
|
325
901
|
originalFile: "old",
|
|
326
902
|
structuredPatch: [],
|
|
903
|
+
gitDiff: {
|
|
904
|
+
repository: "acme/project",
|
|
905
|
+
},
|
|
327
906
|
}, base);
|
|
328
907
|
assert.equal(fields.status, "completed");
|
|
329
908
|
assert.deepEqual(fields.content, [
|
|
@@ -333,16 +912,24 @@ test("buildToolResultFields maps structured Write output to diff content", () =>
|
|
|
333
912
|
new_path: "src/main.ts",
|
|
334
913
|
old: "old",
|
|
335
914
|
new: "new",
|
|
915
|
+
repository: "acme/project",
|
|
336
916
|
},
|
|
337
917
|
]);
|
|
338
918
|
});
|
|
339
|
-
test("buildToolResultFields preserves Edit diff content from input", () => {
|
|
919
|
+
test("buildToolResultFields preserves Edit diff content from input and structured repository", () => {
|
|
340
920
|
const base = createToolCall("tc-e", "Edit", {
|
|
341
921
|
file_path: "src/main.ts",
|
|
342
922
|
old_string: "old",
|
|
343
923
|
new_string: "new",
|
|
344
924
|
});
|
|
345
|
-
const fields = buildToolResultFields(false, [{ text: "Updated successfully" }], base
|
|
925
|
+
const fields = buildToolResultFields(false, [{ text: "Updated successfully" }], base, {
|
|
926
|
+
result: {
|
|
927
|
+
filePath: "src/main.ts",
|
|
928
|
+
gitDiff: {
|
|
929
|
+
repository: "acme/project",
|
|
930
|
+
},
|
|
931
|
+
},
|
|
932
|
+
});
|
|
346
933
|
assert.equal(fields.status, "completed");
|
|
347
934
|
assert.deepEqual(fields.content, [
|
|
348
935
|
{
|
|
@@ -351,6 +938,120 @@ test("buildToolResultFields preserves Edit diff content from input", () => {
|
|
|
351
938
|
new_path: "src/main.ts",
|
|
352
939
|
old: "old",
|
|
353
940
|
new: "new",
|
|
941
|
+
repository: "acme/project",
|
|
942
|
+
},
|
|
943
|
+
]);
|
|
944
|
+
});
|
|
945
|
+
test("buildToolResultFields prefers structured Bash stdout over token-saver output", () => {
|
|
946
|
+
const base = createToolCall("tc-bash", "Bash", { command: "npm test" });
|
|
947
|
+
const fields = buildToolResultFields(false, {
|
|
948
|
+
stdout: "real stdout",
|
|
949
|
+
stderr: "",
|
|
950
|
+
interrupted: false,
|
|
951
|
+
tokenSaverOutput: "compressed output for model",
|
|
952
|
+
}, base, {
|
|
953
|
+
result: {
|
|
954
|
+
stdout: "real stdout",
|
|
955
|
+
stderr: "",
|
|
956
|
+
interrupted: false,
|
|
957
|
+
tokenSaverOutput: "compressed output for model",
|
|
958
|
+
},
|
|
959
|
+
});
|
|
960
|
+
assert.equal(fields.raw_output, "real stdout");
|
|
961
|
+
assert.deepEqual(fields.output_metadata, {
|
|
962
|
+
bash: {
|
|
963
|
+
token_saver_active: true,
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
test("buildToolResultFields adds Bash auto-backgrounded metadata and message", () => {
|
|
968
|
+
const base = createToolCall("tc-bash-bg", "Bash", { command: "npm run watch" });
|
|
969
|
+
const fields = buildToolResultFields(false, {
|
|
970
|
+
stdout: "",
|
|
971
|
+
stderr: "",
|
|
972
|
+
interrupted: false,
|
|
973
|
+
backgroundTaskId: "task-42",
|
|
974
|
+
assistantAutoBackgrounded: true,
|
|
975
|
+
}, base, {
|
|
976
|
+
result: {
|
|
977
|
+
stdout: "",
|
|
978
|
+
stderr: "",
|
|
979
|
+
interrupted: false,
|
|
980
|
+
backgroundTaskId: "task-42",
|
|
981
|
+
assistantAutoBackgrounded: true,
|
|
982
|
+
},
|
|
983
|
+
});
|
|
984
|
+
assert.equal(fields.raw_output, "Command was auto-backgrounded by assistant mode with ID: task-42.");
|
|
985
|
+
assert.deepEqual(fields.output_metadata, {
|
|
986
|
+
bash: {
|
|
987
|
+
assistant_auto_backgrounded: true,
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
test("buildToolResultFields maps structured ReadMcpResource output to typed resource content", () => {
|
|
992
|
+
const base = createToolCall("tc-mcp", "ReadMcpResource", {
|
|
993
|
+
server: "docs",
|
|
994
|
+
uri: "file://manual.pdf",
|
|
995
|
+
});
|
|
996
|
+
const fields = buildToolResultFields(false, {
|
|
997
|
+
contents: [
|
|
998
|
+
{
|
|
999
|
+
uri: "file://manual.pdf",
|
|
1000
|
+
mimeType: "application/pdf",
|
|
1001
|
+
text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
|
|
1002
|
+
blobSavedTo: "C:\\tmp\\manual.pdf",
|
|
1003
|
+
},
|
|
1004
|
+
],
|
|
1005
|
+
}, base, {
|
|
1006
|
+
result: {
|
|
1007
|
+
contents: [
|
|
1008
|
+
{
|
|
1009
|
+
uri: "file://manual.pdf",
|
|
1010
|
+
mimeType: "application/pdf",
|
|
1011
|
+
text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
|
|
1012
|
+
blobSavedTo: "C:\\tmp\\manual.pdf",
|
|
1013
|
+
},
|
|
1014
|
+
],
|
|
1015
|
+
},
|
|
1016
|
+
});
|
|
1017
|
+
assert.equal(fields.status, "completed");
|
|
1018
|
+
assert.deepEqual(fields.content, [
|
|
1019
|
+
{
|
|
1020
|
+
type: "mcp_resource",
|
|
1021
|
+
uri: "file://manual.pdf",
|
|
1022
|
+
mime_type: "application/pdf",
|
|
1023
|
+
text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
|
|
1024
|
+
blob_saved_to: "C:\\tmp\\manual.pdf",
|
|
1025
|
+
},
|
|
1026
|
+
]);
|
|
1027
|
+
});
|
|
1028
|
+
test("buildToolResultFields restores ReadMcpResource blob paths from transcript JSON text", () => {
|
|
1029
|
+
const base = createToolCall("tc-mcp-history", "ReadMcpResource", {
|
|
1030
|
+
server: "docs",
|
|
1031
|
+
uri: "file://manual.pdf",
|
|
1032
|
+
});
|
|
1033
|
+
const transcriptJson = JSON.stringify({
|
|
1034
|
+
contents: [
|
|
1035
|
+
{
|
|
1036
|
+
uri: "file://manual.pdf",
|
|
1037
|
+
mimeType: "application/pdf",
|
|
1038
|
+
text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
|
|
1039
|
+
blobSavedTo: "C:\\tmp\\manual.pdf",
|
|
1040
|
+
},
|
|
1041
|
+
],
|
|
1042
|
+
});
|
|
1043
|
+
const fields = buildToolResultFields(false, transcriptJson, base, {
|
|
1044
|
+
type: "tool_result",
|
|
1045
|
+
tool_use_id: "tc-mcp-history",
|
|
1046
|
+
content: transcriptJson,
|
|
1047
|
+
});
|
|
1048
|
+
assert.deepEqual(fields.content, [
|
|
1049
|
+
{
|
|
1050
|
+
type: "mcp_resource",
|
|
1051
|
+
uri: "file://manual.pdf",
|
|
1052
|
+
mime_type: "application/pdf",
|
|
1053
|
+
text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
|
|
1054
|
+
blob_saved_to: "C:\\tmp\\manual.pdf",
|
|
354
1055
|
},
|
|
355
1056
|
]);
|
|
356
1057
|
});
|
|
@@ -395,7 +1096,7 @@ test("permissionOptionsFromSuggestions uses persistent label when settings scope
|
|
|
395
1096
|
type: "addRules",
|
|
396
1097
|
behavior: "allow",
|
|
397
1098
|
destination: "localSettings",
|
|
398
|
-
rules: [{ toolName: "Bash", ruleContent: "
|
|
1099
|
+
rules: [{ toolName: "Bash", ruleContent: "npm install" }],
|
|
399
1100
|
},
|
|
400
1101
|
]);
|
|
401
1102
|
assert.deepEqual(options, [
|
|
@@ -405,13 +1106,13 @@ test("permissionOptionsFromSuggestions uses persistent label when settings scope
|
|
|
405
1106
|
]);
|
|
406
1107
|
});
|
|
407
1108
|
test("permissionResultFromOutcome keeps Bash allow_always suggestions unchanged", () => {
|
|
408
|
-
const allow = permissionResultFromOutcome({ outcome: "selected", option_id: "allow_always" }, "tool-1", { command: "
|
|
1109
|
+
const allow = permissionResultFromOutcome({ outcome: "selected", option_id: "allow_always" }, "tool-1", { command: "npm install" }, [
|
|
409
1110
|
{
|
|
410
1111
|
type: "addRules",
|
|
411
1112
|
behavior: "allow",
|
|
412
1113
|
destination: "localSettings",
|
|
413
1114
|
rules: [
|
|
414
|
-
{ toolName: "Bash", ruleContent: "
|
|
1115
|
+
{ toolName: "Bash", ruleContent: "npm install" },
|
|
415
1116
|
{ toolName: "WebFetch", ruleContent: "https://example.com" },
|
|
416
1117
|
{ toolName: "Bash", ruleContent: "dir /B" },
|
|
417
1118
|
],
|
|
@@ -427,7 +1128,7 @@ test("permissionResultFromOutcome keeps Bash allow_always suggestions unchanged"
|
|
|
427
1128
|
behavior: "allow",
|
|
428
1129
|
destination: "localSettings",
|
|
429
1130
|
rules: [
|
|
430
|
-
{ toolName: "Bash", ruleContent: "
|
|
1131
|
+
{ toolName: "Bash", ruleContent: "npm install" },
|
|
431
1132
|
{ toolName: "WebFetch", ruleContent: "https://example.com" },
|
|
432
1133
|
{ toolName: "Bash", ruleContent: "dir /B" },
|
|
433
1134
|
],
|
|
@@ -492,7 +1193,7 @@ test("looksLikeAuthRequired detects login hints", () => {
|
|
|
492
1193
|
assert.equal(looksLikeAuthRequired("normal tool output"), false);
|
|
493
1194
|
});
|
|
494
1195
|
test("agent sdk version compatibility check matches pinned version", () => {
|
|
495
|
-
assert.equal(resolveInstalledAgentSdkVersion(), "0.2.
|
|
1196
|
+
assert.equal(resolveInstalledAgentSdkVersion(), "0.2.74");
|
|
496
1197
|
assert.equal(agentSdkVersionCompatibilityError(), undefined);
|
|
497
1198
|
});
|
|
498
1199
|
test("mapSessionMessagesToUpdates maps message content blocks", () => {
|
|
@@ -607,3 +1308,83 @@ test("mapSdkSessions normalizes and sorts sessions", () => {
|
|
|
607
1308
|
},
|
|
608
1309
|
]);
|
|
609
1310
|
});
|
|
1311
|
+
test("buildSessionListOptions scopes repo-local listings to worktrees", () => {
|
|
1312
|
+
assert.deepEqual(buildSessionListOptions("C:/repo"), {
|
|
1313
|
+
dir: "C:/repo",
|
|
1314
|
+
includeWorktrees: true,
|
|
1315
|
+
limit: 50,
|
|
1316
|
+
});
|
|
1317
|
+
assert.deepEqual(buildSessionListOptions(undefined), {
|
|
1318
|
+
limit: 50,
|
|
1319
|
+
});
|
|
1320
|
+
});
|
|
1321
|
+
test("buildToolResultFields extracts ExitPlanMode ultraplan metadata from structured results", () => {
|
|
1322
|
+
const base = createToolCall("tc-plan", "ExitPlanMode", {});
|
|
1323
|
+
const fields = buildToolResultFields(false, [{ text: "Plan ready for approval" }], base, {
|
|
1324
|
+
result: {
|
|
1325
|
+
plan: "Plan contents",
|
|
1326
|
+
isUltraplan: true,
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
assert.deepEqual(fields.output_metadata, {
|
|
1330
|
+
exit_plan_mode: {
|
|
1331
|
+
is_ultraplan: true,
|
|
1332
|
+
},
|
|
1333
|
+
});
|
|
1334
|
+
});
|
|
1335
|
+
test("buildToolResultFields extracts TodoWrite verification metadata from structured results", () => {
|
|
1336
|
+
const base = createToolCall("tc-todo", "TodoWrite", {
|
|
1337
|
+
todos: [{ content: "Verify changes", status: "pending", activeForm: "Verifying changes" }],
|
|
1338
|
+
});
|
|
1339
|
+
const fields = buildToolResultFields(false, [{ text: "Todos have been modified successfully." }], base, {
|
|
1340
|
+
data: {
|
|
1341
|
+
oldTodos: [],
|
|
1342
|
+
newTodos: [],
|
|
1343
|
+
verificationNudgeNeeded: true,
|
|
1344
|
+
},
|
|
1345
|
+
});
|
|
1346
|
+
assert.deepEqual(fields.output_metadata, {
|
|
1347
|
+
todo_write: {
|
|
1348
|
+
verification_nudge_needed: true,
|
|
1349
|
+
},
|
|
1350
|
+
});
|
|
1351
|
+
});
|
|
1352
|
+
test("mapAvailableModels preserves optional fast and auto mode metadata", () => {
|
|
1353
|
+
const mapped = mapAvailableModels([
|
|
1354
|
+
{
|
|
1355
|
+
value: "sonnet",
|
|
1356
|
+
displayName: "Claude Sonnet",
|
|
1357
|
+
description: "Balanced model",
|
|
1358
|
+
supportsEffort: true,
|
|
1359
|
+
supportedEffortLevels: ["low", "medium", "high", "max"],
|
|
1360
|
+
supportsAdaptiveThinking: true,
|
|
1361
|
+
supportsFastMode: true,
|
|
1362
|
+
supportsAutoMode: false,
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
value: "haiku",
|
|
1366
|
+
displayName: "Claude Haiku",
|
|
1367
|
+
description: "Fast model",
|
|
1368
|
+
supportsEffort: false,
|
|
1369
|
+
},
|
|
1370
|
+
]);
|
|
1371
|
+
assert.deepEqual(mapped, [
|
|
1372
|
+
{
|
|
1373
|
+
id: "sonnet",
|
|
1374
|
+
display_name: "Claude Sonnet",
|
|
1375
|
+
description: "Balanced model",
|
|
1376
|
+
supports_effort: true,
|
|
1377
|
+
supported_effort_levels: ["low", "medium", "high"],
|
|
1378
|
+
supports_adaptive_thinking: true,
|
|
1379
|
+
supports_fast_mode: true,
|
|
1380
|
+
supports_auto_mode: false,
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
id: "haiku",
|
|
1384
|
+
display_name: "Claude Haiku",
|
|
1385
|
+
description: "Fast model",
|
|
1386
|
+
supports_effort: false,
|
|
1387
|
+
supported_effort_levels: [],
|
|
1388
|
+
},
|
|
1389
|
+
]);
|
|
1390
|
+
});
|