@antmanler/claude-code-acp 0.12.6
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 +222 -0
- package/README.md +53 -0
- package/dist/acp-agent.js +908 -0
- package/dist/index.js +20 -0
- package/dist/lib.js +6 -0
- package/dist/mcp-server.js +731 -0
- package/dist/settings.js +422 -0
- package/dist/tests/acp-agent.test.js +753 -0
- package/dist/tests/extract-lines.test.js +79 -0
- package/dist/tests/replace-and-calculate-location.test.js +266 -0
- package/dist/tests/settings.test.js +462 -0
- package/dist/tools.js +555 -0
- package/dist/utils.js +150 -0
- package/package.json +73 -0
|
@@ -0,0 +1,753 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { spawn, spawnSync } from "child_process";
|
|
3
|
+
import { ClientSideConnection, ndJsonStream, } from "@agentclientprotocol/sdk";
|
|
4
|
+
import { nodeToWebWritable, nodeToWebReadable } from "../utils.js";
|
|
5
|
+
import { markdownEscape, toolInfoFromToolUse, toolUpdateFromToolResult } from "../tools.js";
|
|
6
|
+
import { toAcpNotifications, promptToClaude } from "../acp-agent.js";
|
|
7
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration", () => {
|
|
10
|
+
let child;
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
const valid = spawnSync("tsc", { stdio: "inherit" });
|
|
13
|
+
if (valid.status) {
|
|
14
|
+
throw new Error("failed to compile");
|
|
15
|
+
}
|
|
16
|
+
// Start the subprocess
|
|
17
|
+
child = spawn("npm", ["run", "--silent", "dev"], {
|
|
18
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
19
|
+
env: process.env,
|
|
20
|
+
});
|
|
21
|
+
child.on("error", (error) => {
|
|
22
|
+
console.error("Error starting subprocess:", error);
|
|
23
|
+
});
|
|
24
|
+
child.on("exit", (exit) => {
|
|
25
|
+
console.error("Exited with", exit);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
afterAll(() => {
|
|
29
|
+
child.kill();
|
|
30
|
+
});
|
|
31
|
+
class TestClient {
|
|
32
|
+
constructor(agent) {
|
|
33
|
+
this.files = new Map();
|
|
34
|
+
this.receivedText = "";
|
|
35
|
+
this.agent = agent;
|
|
36
|
+
this.resolveAvailableCommands = () => { };
|
|
37
|
+
this.availableCommandsPromise = new Promise((resolve) => {
|
|
38
|
+
this.resolveAvailableCommands = resolve;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
takeReceivedText() {
|
|
42
|
+
const text = this.receivedText;
|
|
43
|
+
this.receivedText = "";
|
|
44
|
+
return text;
|
|
45
|
+
}
|
|
46
|
+
async requestPermission(params) {
|
|
47
|
+
const optionId = params.options.find((p) => p.kind === "allow_once").optionId;
|
|
48
|
+
return { outcome: { outcome: "selected", optionId } };
|
|
49
|
+
}
|
|
50
|
+
async sessionUpdate(params) {
|
|
51
|
+
console.error("RECEIVED", JSON.stringify(params, null, 4));
|
|
52
|
+
switch (params.update.sessionUpdate) {
|
|
53
|
+
case "agent_message_chunk": {
|
|
54
|
+
if (params.update.content.type === "text") {
|
|
55
|
+
this.receivedText += params.update.content.text;
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case "available_commands_update":
|
|
60
|
+
this.resolveAvailableCommands(params.update.availableCommands);
|
|
61
|
+
break;
|
|
62
|
+
default:
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async writeTextFile(params) {
|
|
67
|
+
this.files.set(params.path, params.content);
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
async readTextFile(params) {
|
|
71
|
+
const content = this.files.get(params.path) ?? "";
|
|
72
|
+
return {
|
|
73
|
+
content,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function setupTestSession(cwd) {
|
|
78
|
+
let client;
|
|
79
|
+
const input = nodeToWebWritable(child.stdin);
|
|
80
|
+
const output = nodeToWebReadable(child.stdout);
|
|
81
|
+
const stream = ndJsonStream(input, output);
|
|
82
|
+
const connection = new ClientSideConnection((agent) => {
|
|
83
|
+
client = new TestClient(agent);
|
|
84
|
+
return client;
|
|
85
|
+
}, stream);
|
|
86
|
+
await connection.initialize({
|
|
87
|
+
protocolVersion: 1,
|
|
88
|
+
clientCapabilities: {
|
|
89
|
+
fs: {
|
|
90
|
+
readTextFile: true,
|
|
91
|
+
writeTextFile: true,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const newSessionResponse = await connection.newSession({
|
|
96
|
+
cwd,
|
|
97
|
+
mcpServers: [],
|
|
98
|
+
});
|
|
99
|
+
return { client: client, connection, newSessionResponse };
|
|
100
|
+
}
|
|
101
|
+
it("should connect to the ACP subprocess", async () => {
|
|
102
|
+
const { client, connection, newSessionResponse } = await setupTestSession("./");
|
|
103
|
+
await connection.prompt({
|
|
104
|
+
prompt: [
|
|
105
|
+
{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: "Hello",
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
sessionId: newSessionResponse.sessionId,
|
|
111
|
+
});
|
|
112
|
+
expect(client.takeReceivedText()).not.toEqual("");
|
|
113
|
+
}, 30000);
|
|
114
|
+
it("should include available commands", async () => {
|
|
115
|
+
const { client, connection, newSessionResponse } = await setupTestSession(__dirname);
|
|
116
|
+
const commands = await client.availableCommandsPromise;
|
|
117
|
+
expect(commands).toContainEqual({
|
|
118
|
+
name: "quick-math",
|
|
119
|
+
description: "10 * 3 = 30 (project)",
|
|
120
|
+
input: null,
|
|
121
|
+
});
|
|
122
|
+
expect(commands).toContainEqual({
|
|
123
|
+
name: "say-hello",
|
|
124
|
+
description: "Say hello (project)",
|
|
125
|
+
input: { hint: "[name]" },
|
|
126
|
+
});
|
|
127
|
+
await connection.prompt({
|
|
128
|
+
prompt: [
|
|
129
|
+
{
|
|
130
|
+
type: "text",
|
|
131
|
+
text: "/quick-math",
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
sessionId: newSessionResponse.sessionId,
|
|
135
|
+
});
|
|
136
|
+
expect(client.takeReceivedText()).toContain("30");
|
|
137
|
+
await connection.prompt({
|
|
138
|
+
prompt: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: "/say-hello GPT-5",
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
sessionId: newSessionResponse.sessionId,
|
|
145
|
+
});
|
|
146
|
+
expect(client.takeReceivedText()).toContain("Hello GPT-5");
|
|
147
|
+
}, 30000);
|
|
148
|
+
it("/compact works", async () => {
|
|
149
|
+
const { client, connection, newSessionResponse } = await setupTestSession(__dirname);
|
|
150
|
+
const commands = await client.availableCommandsPromise;
|
|
151
|
+
expect(commands).toContainEqual({
|
|
152
|
+
description: "Clear conversation history but keep a summary in context. Optional: /compact [instructions for summarization]",
|
|
153
|
+
input: {
|
|
154
|
+
hint: "<optional custom summarization instructions>",
|
|
155
|
+
},
|
|
156
|
+
name: "compact",
|
|
157
|
+
});
|
|
158
|
+
// Error case (no previous message)
|
|
159
|
+
await connection.prompt({
|
|
160
|
+
prompt: [{ type: "text", text: "/compact" }],
|
|
161
|
+
sessionId: newSessionResponse.sessionId,
|
|
162
|
+
});
|
|
163
|
+
expect(client.takeReceivedText()).toBe("");
|
|
164
|
+
// Send something
|
|
165
|
+
await connection.prompt({
|
|
166
|
+
prompt: [{ type: "text", text: "Hi" }],
|
|
167
|
+
sessionId: newSessionResponse.sessionId,
|
|
168
|
+
});
|
|
169
|
+
// Clear response
|
|
170
|
+
client.takeReceivedText();
|
|
171
|
+
// Test with instruction
|
|
172
|
+
await connection.prompt({
|
|
173
|
+
prompt: [
|
|
174
|
+
{
|
|
175
|
+
type: "text",
|
|
176
|
+
text: "/compact greeting",
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
sessionId: newSessionResponse.sessionId,
|
|
180
|
+
});
|
|
181
|
+
expect(client.takeReceivedText()).toContain("");
|
|
182
|
+
}, 30000);
|
|
183
|
+
});
|
|
184
|
+
describe("tool conversions", () => {
|
|
185
|
+
it("should handle Bash nicely", () => {
|
|
186
|
+
const tool_use = {
|
|
187
|
+
type: "tool_use",
|
|
188
|
+
id: "toolu_01VtsS2mxUFwpBJZYd7BmbC9",
|
|
189
|
+
name: "Bash",
|
|
190
|
+
input: {
|
|
191
|
+
command: "rm README.md.rm",
|
|
192
|
+
description: "Delete README.md.rm file",
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
|
196
|
+
kind: "execute",
|
|
197
|
+
title: "`rm README.md.rm`",
|
|
198
|
+
content: [
|
|
199
|
+
{
|
|
200
|
+
content: {
|
|
201
|
+
text: "Delete README.md.rm file",
|
|
202
|
+
type: "text",
|
|
203
|
+
},
|
|
204
|
+
type: "content",
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
it("should handle Glob nicely", () => {
|
|
210
|
+
const tool_use = {
|
|
211
|
+
type: "tool_use",
|
|
212
|
+
id: "toolu_01VtsS2mxUFwpBJZYd7BmbC9",
|
|
213
|
+
name: "Glob",
|
|
214
|
+
input: {
|
|
215
|
+
pattern: "*/**.ts",
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
|
219
|
+
kind: "search",
|
|
220
|
+
title: "Find `*/**.ts`",
|
|
221
|
+
content: [],
|
|
222
|
+
locations: [],
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
it("should handle Task tool calls", () => {
|
|
226
|
+
const tool_use = {
|
|
227
|
+
type: "tool_use",
|
|
228
|
+
id: "toolu_01ANYHYDsXcDPKgxhg7us9bj",
|
|
229
|
+
name: "Task",
|
|
230
|
+
input: {
|
|
231
|
+
description: "Handle user's work request",
|
|
232
|
+
prompt: 'The user has asked me to "Create a Task to do the work!" but hasn\'t specified what specific work they want done. I need to:\n\n1. First understand what work needs to be done by examining the current state of the repository\n2. Look at the git status to see what files have been modified\n3. Check if there are any obvious tasks that need completion based on the current state\n4. If the work isn\'t clear from the context, ask the user to specify what work they want accomplished\n\nThe git status shows: "M src/tests/acp-agent.test.ts" - there\'s a modified test file that might need attention.\n\nPlease examine the repository state and determine what work needs to be done, then either complete it or ask the user for clarification on the specific task they want accomplished.',
|
|
233
|
+
subagent_type: "general-purpose",
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
|
237
|
+
kind: "think",
|
|
238
|
+
title: "Handle user's work request",
|
|
239
|
+
content: [
|
|
240
|
+
{
|
|
241
|
+
content: {
|
|
242
|
+
text: 'The user has asked me to "Create a Task to do the work!" but hasn\'t specified what specific work they want done. I need to:\n\n1. First understand what work needs to be done by examining the current state of the repository\n2. Look at the git status to see what files have been modified\n3. Check if there are any obvious tasks that need completion based on the current state\n4. If the work isn\'t clear from the context, ask the user to specify what work they want accomplished\n\nThe git status shows: "M src/tests/acp-agent.test.ts" - there\'s a modified test file that might need attention.\n\nPlease examine the repository state and determine what work needs to be done, then either complete it or ask the user for clarification on the specific task they want accomplished.',
|
|
243
|
+
type: "text",
|
|
244
|
+
},
|
|
245
|
+
type: "content",
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
it("should handle LS tool calls", () => {
|
|
251
|
+
const tool_use = {
|
|
252
|
+
type: "tool_use",
|
|
253
|
+
id: "toolu_01EEqsX7Eb9hpx87KAHVPTey",
|
|
254
|
+
name: "LS",
|
|
255
|
+
input: {
|
|
256
|
+
path: "/Users/test/github/claude-code-acp",
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
|
260
|
+
kind: "search",
|
|
261
|
+
title: "List the `/Users/test/github/claude-code-acp` directory's contents",
|
|
262
|
+
content: [],
|
|
263
|
+
locations: [],
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
it("should handle Grep tool calls", () => {
|
|
267
|
+
const tool_use = {
|
|
268
|
+
type: "tool_use",
|
|
269
|
+
id: "toolu_016j8oGSD3eAZ9KT62Y7Jsjb",
|
|
270
|
+
name: "Grep",
|
|
271
|
+
input: {
|
|
272
|
+
pattern: ".*",
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
|
276
|
+
kind: "search",
|
|
277
|
+
title: 'grep ".*"',
|
|
278
|
+
content: [],
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
it("should handle Write tool calls", () => {
|
|
282
|
+
const tool_use = {
|
|
283
|
+
type: "tool_use",
|
|
284
|
+
id: "toolu_01ABC123XYZ789",
|
|
285
|
+
name: "Write",
|
|
286
|
+
input: {
|
|
287
|
+
file_path: "/Users/test/project/example.txt",
|
|
288
|
+
content: "Hello, World!\nThis is test content.",
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
|
292
|
+
kind: "edit",
|
|
293
|
+
title: "Write /Users/test/project/example.txt",
|
|
294
|
+
content: [
|
|
295
|
+
{
|
|
296
|
+
type: "diff",
|
|
297
|
+
path: "/Users/test/project/example.txt",
|
|
298
|
+
oldText: null,
|
|
299
|
+
newText: "Hello, World!\nThis is test content.",
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
locations: [{ path: "/Users/test/project/example.txt" }],
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
it("should handle mcp__acp__Write tool calls", () => {
|
|
306
|
+
const tool_use = {
|
|
307
|
+
type: "tool_use",
|
|
308
|
+
id: "toolu_01GHI789JKL456",
|
|
309
|
+
name: "mcp__acp__Write",
|
|
310
|
+
input: {
|
|
311
|
+
file_path: "/Users/test/project/config.json",
|
|
312
|
+
content: '{"version": "1.0.0"}',
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
|
316
|
+
kind: "edit",
|
|
317
|
+
title: "Write /Users/test/project/config.json",
|
|
318
|
+
content: [
|
|
319
|
+
{
|
|
320
|
+
type: "diff",
|
|
321
|
+
path: "/Users/test/project/config.json",
|
|
322
|
+
oldText: null,
|
|
323
|
+
newText: '{"version": "1.0.0"}',
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
locations: [{ path: "/Users/test/project/config.json" }],
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
it("should handle Read tool calls", () => {
|
|
330
|
+
const tool_use = {
|
|
331
|
+
type: "tool_use",
|
|
332
|
+
id: "toolu_01MNO456PQR789",
|
|
333
|
+
name: "Read",
|
|
334
|
+
input: {
|
|
335
|
+
file_path: "/Users/test/project/readme.md",
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
|
339
|
+
kind: "read",
|
|
340
|
+
title: "Read File",
|
|
341
|
+
content: [],
|
|
342
|
+
locations: [{ path: "/Users/test/project/readme.md", line: 0 }],
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
it("should handle mcp__acp__Read tool calls", () => {
|
|
346
|
+
const tool_use = {
|
|
347
|
+
type: "tool_use",
|
|
348
|
+
id: "toolu_01YZA789BCD123",
|
|
349
|
+
name: "mcp__acp__Read",
|
|
350
|
+
input: {
|
|
351
|
+
file_path: "/Users/test/project/data.json",
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
|
355
|
+
kind: "read",
|
|
356
|
+
title: "Read /Users/test/project/data.json",
|
|
357
|
+
content: [],
|
|
358
|
+
locations: [{ path: "/Users/test/project/data.json", line: 0 }],
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
it("should handle mcp__acp__Read with limit", () => {
|
|
362
|
+
const tool_use = {
|
|
363
|
+
type: "tool_use",
|
|
364
|
+
id: "toolu_01EFG456HIJ789",
|
|
365
|
+
name: "mcp__acp__Read",
|
|
366
|
+
input: {
|
|
367
|
+
file_path: "/Users/test/project/large.txt",
|
|
368
|
+
limit: 100,
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
|
372
|
+
kind: "read",
|
|
373
|
+
title: "Read /Users/test/project/large.txt (1 - 100)",
|
|
374
|
+
content: [],
|
|
375
|
+
locations: [{ path: "/Users/test/project/large.txt", line: 0 }],
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
it("should handle mcp__acp__Read with offset and limit", () => {
|
|
379
|
+
const tool_use = {
|
|
380
|
+
type: "tool_use",
|
|
381
|
+
id: "toolu_01KLM789NOP456",
|
|
382
|
+
name: "mcp__acp__Read",
|
|
383
|
+
input: {
|
|
384
|
+
file_path: "/Users/test/project/large.txt",
|
|
385
|
+
offset: 50,
|
|
386
|
+
limit: 100,
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
|
390
|
+
kind: "read",
|
|
391
|
+
title: "Read /Users/test/project/large.txt (51 - 150)",
|
|
392
|
+
content: [],
|
|
393
|
+
locations: [{ path: "/Users/test/project/large.txt", line: 50 }],
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
it("should handle mcp__acp__Read with only offset", () => {
|
|
397
|
+
const tool_use = {
|
|
398
|
+
type: "tool_use",
|
|
399
|
+
id: "toolu_01QRS123TUV789",
|
|
400
|
+
name: "mcp__acp__Read",
|
|
401
|
+
input: {
|
|
402
|
+
file_path: "/Users/test/project/large.txt",
|
|
403
|
+
offset: 200,
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
|
407
|
+
kind: "read",
|
|
408
|
+
title: "Read /Users/test/project/large.txt (from line 201)",
|
|
409
|
+
content: [],
|
|
410
|
+
locations: [{ path: "/Users/test/project/large.txt", line: 200 }],
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
it("should handle KillBash entries", () => {
|
|
414
|
+
const tool_use = {
|
|
415
|
+
type: "tool_use",
|
|
416
|
+
id: "toolu_01PhLms5fuvmdjy2bb6dfUKT",
|
|
417
|
+
name: "KillShell",
|
|
418
|
+
input: {
|
|
419
|
+
shell_id: "bash_1",
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
|
423
|
+
kind: "execute",
|
|
424
|
+
title: `Kill Process`,
|
|
425
|
+
content: [],
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
it("should handle BashOutput entries", () => {
|
|
429
|
+
const tool_use = {
|
|
430
|
+
type: "tool_use",
|
|
431
|
+
id: "toolu_01SJUWPtj1QspgANgtpqGPuN",
|
|
432
|
+
name: "BashOutput",
|
|
433
|
+
input: {
|
|
434
|
+
bash_id: "bash_1",
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
|
|
438
|
+
kind: "execute",
|
|
439
|
+
title: `Tail Logs`,
|
|
440
|
+
content: [],
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
it("should handle plan entries", () => {
|
|
444
|
+
const received = {
|
|
445
|
+
type: "assistant",
|
|
446
|
+
message: {
|
|
447
|
+
id: "msg_017eNosJgww7F5qD4a8BcAcx",
|
|
448
|
+
type: "message",
|
|
449
|
+
role: "assistant",
|
|
450
|
+
container: null,
|
|
451
|
+
model: "claude-sonnet-4-20250514",
|
|
452
|
+
content: [
|
|
453
|
+
{
|
|
454
|
+
type: "tool_use",
|
|
455
|
+
id: "toolu_01HaXZ4LfdchSeSR8ygt4zyq",
|
|
456
|
+
name: "TodoWrite",
|
|
457
|
+
input: {
|
|
458
|
+
todos: [
|
|
459
|
+
{
|
|
460
|
+
content: "Analyze existing test coverage and identify gaps",
|
|
461
|
+
status: "in_progress",
|
|
462
|
+
activeForm: "Analyzing existing test coverage",
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
content: "Add comprehensive edge case tests",
|
|
466
|
+
status: "pending",
|
|
467
|
+
activeForm: "Adding comprehensive edge case tests",
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
content: "Add performance and timing tests",
|
|
471
|
+
status: "pending",
|
|
472
|
+
activeForm: "Adding performance and timing tests",
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
content: "Add error handling and panic behavior tests",
|
|
476
|
+
status: "pending",
|
|
477
|
+
activeForm: "Adding error handling tests",
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
content: "Add concurrent access and race condition tests",
|
|
481
|
+
status: "pending",
|
|
482
|
+
activeForm: "Adding concurrent access tests",
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
content: "Add tests for Each function with various data types",
|
|
486
|
+
status: "pending",
|
|
487
|
+
activeForm: "Adding Each function tests",
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
content: "Add benchmark tests for performance measurement",
|
|
491
|
+
status: "pending",
|
|
492
|
+
activeForm: "Adding benchmark tests",
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
content: "Improve test organization and helper functions",
|
|
496
|
+
status: "pending",
|
|
497
|
+
activeForm: "Improving test organization",
|
|
498
|
+
},
|
|
499
|
+
],
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
stop_reason: null,
|
|
504
|
+
stop_sequence: null,
|
|
505
|
+
usage: {
|
|
506
|
+
input_tokens: 6,
|
|
507
|
+
cache_creation_input_tokens: 326,
|
|
508
|
+
cache_read_input_tokens: 17265,
|
|
509
|
+
cache_creation: {
|
|
510
|
+
ephemeral_5m_input_tokens: 326,
|
|
511
|
+
ephemeral_1h_input_tokens: 0,
|
|
512
|
+
},
|
|
513
|
+
output_tokens: 1,
|
|
514
|
+
service_tier: "standard",
|
|
515
|
+
server_tool_use: null,
|
|
516
|
+
},
|
|
517
|
+
context_management: null,
|
|
518
|
+
},
|
|
519
|
+
parent_tool_use_id: null,
|
|
520
|
+
session_id: "d056596f-e328-41e9-badd-b07122ae5227",
|
|
521
|
+
uuid: "b7c3330c-de8f-4bba-ac53-68c7f76ffeb5",
|
|
522
|
+
};
|
|
523
|
+
expect(toAcpNotifications(received.message.content, received.message.role, "test", {}, {}, console)).toStrictEqual([
|
|
524
|
+
{
|
|
525
|
+
sessionId: "test",
|
|
526
|
+
update: {
|
|
527
|
+
sessionUpdate: "plan",
|
|
528
|
+
entries: [
|
|
529
|
+
{
|
|
530
|
+
content: "Analyze existing test coverage and identify gaps",
|
|
531
|
+
priority: "medium",
|
|
532
|
+
status: "in_progress",
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
content: "Add comprehensive edge case tests",
|
|
536
|
+
priority: "medium",
|
|
537
|
+
status: "pending",
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
content: "Add performance and timing tests",
|
|
541
|
+
priority: "medium",
|
|
542
|
+
status: "pending",
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
content: "Add error handling and panic behavior tests",
|
|
546
|
+
priority: "medium",
|
|
547
|
+
status: "pending",
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
content: "Add concurrent access and race condition tests",
|
|
551
|
+
priority: "medium",
|
|
552
|
+
status: "pending",
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
content: "Add tests for Each function with various data types",
|
|
556
|
+
priority: "medium",
|
|
557
|
+
status: "pending",
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
content: "Add benchmark tests for performance measurement",
|
|
561
|
+
priority: "medium",
|
|
562
|
+
status: "pending",
|
|
563
|
+
},
|
|
564
|
+
{
|
|
565
|
+
content: "Improve test organization and helper functions",
|
|
566
|
+
priority: "medium",
|
|
567
|
+
status: "pending",
|
|
568
|
+
},
|
|
569
|
+
],
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
]);
|
|
573
|
+
});
|
|
574
|
+
it("should return empty update for successful edit result", () => {
|
|
575
|
+
const toolUse = {
|
|
576
|
+
type: "tool_use",
|
|
577
|
+
id: "toolu_01MNO345",
|
|
578
|
+
name: "mcp__acp__Edit",
|
|
579
|
+
input: {
|
|
580
|
+
file_path: "/Users/test/project/test.txt",
|
|
581
|
+
old_string: "old",
|
|
582
|
+
new_string: "new",
|
|
583
|
+
},
|
|
584
|
+
};
|
|
585
|
+
const toolResult = {
|
|
586
|
+
content: [
|
|
587
|
+
{
|
|
588
|
+
type: "text",
|
|
589
|
+
text: "not valid json",
|
|
590
|
+
},
|
|
591
|
+
],
|
|
592
|
+
tool_use_id: "test",
|
|
593
|
+
is_error: false,
|
|
594
|
+
type: "tool_result",
|
|
595
|
+
};
|
|
596
|
+
const update = toolUpdateFromToolResult(toolResult, toolUse);
|
|
597
|
+
// Should return empty object when parsing fails
|
|
598
|
+
expect(update).toEqual({});
|
|
599
|
+
});
|
|
600
|
+
it("should return content update for edit failure", () => {
|
|
601
|
+
const toolUse = {
|
|
602
|
+
type: "tool_use",
|
|
603
|
+
id: "toolu_01MNO345",
|
|
604
|
+
name: "mcp__acp__Edit",
|
|
605
|
+
input: {
|
|
606
|
+
file_path: "/Users/test/project/test.txt",
|
|
607
|
+
old_string: "old",
|
|
608
|
+
new_string: "new",
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
const toolResult = {
|
|
612
|
+
content: [
|
|
613
|
+
{
|
|
614
|
+
type: "text",
|
|
615
|
+
text: "Failed to find `old_string`",
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
tool_use_id: "test",
|
|
619
|
+
is_error: true,
|
|
620
|
+
type: "tool_result",
|
|
621
|
+
};
|
|
622
|
+
const update = toolUpdateFromToolResult(toolResult, toolUse);
|
|
623
|
+
// Should return empty object when parsing fails
|
|
624
|
+
expect(update).toEqual({
|
|
625
|
+
content: [
|
|
626
|
+
{
|
|
627
|
+
content: { type: "text", text: "```\nFailed to find `old_string`\n```" },
|
|
628
|
+
type: "content",
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
describe("escape markdown", () => {
|
|
635
|
+
it("should escape markdown characters", () => {
|
|
636
|
+
let text = "Hello *world*!";
|
|
637
|
+
let escaped = markdownEscape(text);
|
|
638
|
+
expect(escaped).toEqual("```\nHello *world*!\n```");
|
|
639
|
+
text = "for example:\n```markdown\nHello *world*!\n```\n";
|
|
640
|
+
escaped = markdownEscape(text);
|
|
641
|
+
expect(escaped).toEqual("````\nfor example:\n```markdown\nHello *world*!\n```\n````");
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
describe("prompt conversion", () => {
|
|
645
|
+
it("should not change built-in slash commands", () => {
|
|
646
|
+
const message = promptToClaude({
|
|
647
|
+
sessionId: "test",
|
|
648
|
+
prompt: [
|
|
649
|
+
{
|
|
650
|
+
type: "text",
|
|
651
|
+
text: "/compact args",
|
|
652
|
+
},
|
|
653
|
+
],
|
|
654
|
+
});
|
|
655
|
+
expect(message.message.content).toEqual([
|
|
656
|
+
{
|
|
657
|
+
text: "/compact args",
|
|
658
|
+
type: "text",
|
|
659
|
+
},
|
|
660
|
+
]);
|
|
661
|
+
});
|
|
662
|
+
it("should remove MCP prefix from MCP slash commands", () => {
|
|
663
|
+
const message = promptToClaude({
|
|
664
|
+
sessionId: "test",
|
|
665
|
+
prompt: [
|
|
666
|
+
{
|
|
667
|
+
type: "text",
|
|
668
|
+
text: "/mcp:server:name args",
|
|
669
|
+
},
|
|
670
|
+
],
|
|
671
|
+
});
|
|
672
|
+
expect(message.message.content).toEqual([
|
|
673
|
+
{
|
|
674
|
+
text: "/server:name (MCP) args",
|
|
675
|
+
type: "text",
|
|
676
|
+
},
|
|
677
|
+
]);
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("SDK behavior", () => {
|
|
681
|
+
it("query has a 'default' model", async () => {
|
|
682
|
+
const q = query({ prompt: "hi" });
|
|
683
|
+
const models = await q.supportedModels();
|
|
684
|
+
const defaultModel = models.find((m) => m.value === "default");
|
|
685
|
+
expect(defaultModel).toBeDefined();
|
|
686
|
+
}, 10000);
|
|
687
|
+
it("custom session id", async () => {
|
|
688
|
+
const sessionId = randomUUID();
|
|
689
|
+
const q = query({
|
|
690
|
+
prompt: "hi",
|
|
691
|
+
options: {
|
|
692
|
+
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
693
|
+
extraArgs: { "session-id": sessionId },
|
|
694
|
+
settingSources: ["user", "project", "local"],
|
|
695
|
+
includePartialMessages: true,
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
const { value } = await q.next();
|
|
699
|
+
expect(value).toMatchObject({ type: "system", subtype: "init", session_id: sessionId });
|
|
700
|
+
}, 10000);
|
|
701
|
+
});
|
|
702
|
+
describe("permission requests", () => {
|
|
703
|
+
it("should include title field in tool permission request structure", () => {
|
|
704
|
+
// Test various tool types to ensure title is correctly generated
|
|
705
|
+
const testCases = [
|
|
706
|
+
{
|
|
707
|
+
toolUse: {
|
|
708
|
+
type: "tool_use",
|
|
709
|
+
id: "test-1",
|
|
710
|
+
name: "Write",
|
|
711
|
+
input: { file_path: "/test/file.txt", content: "test" },
|
|
712
|
+
},
|
|
713
|
+
expectedTitlePart: "/test/file.txt",
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
toolUse: {
|
|
717
|
+
type: "tool_use",
|
|
718
|
+
id: "test-2",
|
|
719
|
+
name: "Bash",
|
|
720
|
+
input: { command: "ls -la", description: "List files" },
|
|
721
|
+
},
|
|
722
|
+
expectedTitlePart: "`ls -la`",
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
toolUse: {
|
|
726
|
+
type: "tool_use",
|
|
727
|
+
id: "test-3",
|
|
728
|
+
name: "mcp__acp__Read",
|
|
729
|
+
input: { file_path: "/test/data.json" },
|
|
730
|
+
},
|
|
731
|
+
expectedTitlePart: "/test/data.json",
|
|
732
|
+
},
|
|
733
|
+
];
|
|
734
|
+
for (const testCase of testCases) {
|
|
735
|
+
// Get the tool info that would be used in requestPermission
|
|
736
|
+
const toolInfo = toolInfoFromToolUse(testCase.toolUse);
|
|
737
|
+
// Verify toolInfo has a title
|
|
738
|
+
expect(toolInfo.title).toBeDefined();
|
|
739
|
+
expect(toolInfo.title).toContain(testCase.expectedTitlePart);
|
|
740
|
+
// Verify the structure that our fix creates for requestPermission
|
|
741
|
+
const requestStructure = {
|
|
742
|
+
toolCall: {
|
|
743
|
+
toolCallId: testCase.toolUse.id,
|
|
744
|
+
rawInput: testCase.toolUse.input,
|
|
745
|
+
title: toolInfo.title, // This is what commit 1785d86 adds
|
|
746
|
+
},
|
|
747
|
+
};
|
|
748
|
+
// Ensure the title field is present and populated
|
|
749
|
+
expect(requestStructure.toolCall.title).toBeDefined();
|
|
750
|
+
expect(requestStructure.toolCall.title).toContain(testCase.expectedTitlePart);
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
});
|