Raw
1 #!/usr/bin/env node
2 // End-to-end smoke test for the ACP client.
3 //
4 // Spawns the mock ACP agent and drives the real AcpClient through the full
5 // handshake → session → prompt round-trip. Verifies that streaming updates
6 // reach the host, the inbound fs/read_text_file call is honored, and the
7 // prompt resolves with the agent's stop reason.
8 //
9 // Run: node test/smoke.mjs
10
11 import { fileURLToPath } from "node:url";
12 import { dirname, join } from "node:path";
13 import { build } from "esbuild";
14 import { createRequire } from "node:module";
15
16 const __dirname = dirname(fileURLToPath(import.meta.url));
17 const repoRoot = join(__dirname, "..");
18 const mockAgent = join(__dirname, "mock-agent", "mock-agent.mjs");
19
20 // Bundle the AcpClient on the fly so we can import the TypeScript source
21 // without a separate build step.
22 const bundle = await build({
23 entryPoints: [join(repoRoot, "src", "acp", "client.ts")],
24 bundle: true,
25 write: false,
26 format: "cjs",
27 platform: "node",
28 target: "node18",
29 external: ["vscode"],
30 logLevel: "silent"
31 });
32
33 const code = bundle.outputFiles[0].text;
34 const requireFromTest = createRequire(import.meta.url);
35
36 // Evaluate the bundle in a fresh module context.
37 const Module = requireFromTest("node:module");
38 const wrapped = Module.wrap(code);
39 const compiled = eval(wrapped);
40 const moduleObj = { exports: {} };
41 compiled(moduleObj.exports, requireFromTest, moduleObj, "client.bundle.js", repoRoot);
42 const { AcpClient } = moduleObj.exports;
43
44 function assert(cond, msg) {
45 if (!cond) {
46 console.error(`FAIL: ${msg}`);
47 process.exit(1);
48 }
49 }
50
51 const events = [];
52 let fsReadCalled = false;
53
54 const client = new AcpClient({
55 requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
56 readTextFile: async (params) => {
57 fsReadCalled = true;
58 return `// pretend contents of ${params.path}\n`;
59 },
60 writeTextFile: async () => {
61 // no writes in this scenario
62 }
63 });
64
65 client.on("update", (u) => events.push(u));
66 client.on("error", (err) => {
67 console.error("client error:", err.message);
68 process.exit(1);
69 });
70
71 client.spawn({ command: process.execPath, args: [mockAgent], cwd: repoRoot, env: {} });
72
73 try {
74 const sessionId = await client.initialize(repoRoot);
75 assert(typeof sessionId === "string" && sessionId.startsWith("sess_"), "got sessionId");
76 console.log(`OK initialize → session ${sessionId}`);
77
78 const stopReason = await client.prompt("hello world");
79 assert(stopReason === "end_turn", `stopReason was "${stopReason}"`);
80 console.log(`OK prompt → stopReason ${stopReason}`);
81
82 assert(fsReadCalled, "agent's fs/read_text_file request reached the host");
83 console.log("OK inbound fs/read_text_file round-tripped");
84
85 const chunks = events.filter((e) => e.update?.sessionUpdate === "agent_message_chunk");
86 const text = chunks.map((c) => c.update.content.text).join("");
87 assert(text === "echo: hello world (done)", `streamed text was "${text}"`);
88 console.log(`OK streamed ${chunks.length} message chunks → "${text}"`);
89
90 const tools = events.filter((e) => e.update?.sessionUpdate === "tool_call");
91 assert(tools.length === 1, `expected 1 tool_call update, got ${tools.length}`);
92 console.log(`OK received tool_call update`);
93
94 console.log("\nALL SMOKE CHECKS PASSED");
95 } finally {
96 client.dispose();
97 }