Skip to content

Commit a54c878

Browse files
committed
test(acp): add comprehensive tests for tool call formatting and spec alignment
1 parent f257541 commit a54c878

1 file changed

Lines changed: 282 additions & 0 deletions

File tree

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
import { describe, expect, it } from "bun:test"
2+
import { toolCallFromPart, toolResultFromPart } from "../../src/acp/tool-format"
3+
4+
describe("toolCallFromPart", () => {
5+
describe("bash/shell/terminal", () => {
6+
it("formats bash command with description", () => {
7+
const result = toolCallFromPart("bash", { command: "ls -la", description: "List files", cwd: "/home" })
8+
expect(result.title).toBe("List files")
9+
expect(result.kind).toBe("other")
10+
expect(result.locations).toEqual([{ path: "/home" }])
11+
expect(result.rawInput).toEqual({ command: "ls -la", description: "List files", cwd: "/home" })
12+
})
13+
14+
it("falls back to command when no description", () => {
15+
const result = toolCallFromPart("shell", { command: "npm install" })
16+
expect(result.title).toBe("npm install")
17+
})
18+
19+
it("normalizes mcp__acp__ prefix", () => {
20+
const result = toolCallFromPart("mcp__acp__bash", { command: "echo hi" })
21+
expect(result.title).toBe("echo hi")
22+
})
23+
})
24+
25+
describe("bashoutput", () => {
26+
it("returns Tail Logs title", () => {
27+
const result = toolCallFromPart("bashoutput", {})
28+
expect(result.title).toBe("Tail Logs")
29+
expect(result.kind).toBe("execute")
30+
})
31+
})
32+
33+
describe("read/view", () => {
34+
it("formats read with file path", () => {
35+
const result = toolCallFromPart("read", { filePath: "/src/index.ts" })
36+
expect(result.title).toBe("Read /src/index.ts")
37+
expect(result.kind).toBe("read")
38+
expect(result.locations).toEqual([{ path: "/src/index.ts" }])
39+
})
40+
41+
it("uses 1-based line numbers when offset is provided", () => {
42+
const result = toolCallFromPart("read", { filePath: "/src/index.ts", offset: 10 })
43+
expect(result.locations).toEqual([{ path: "/src/index.ts", line: 11 }])
44+
})
45+
46+
it("omits line key when offset is zero", () => {
47+
const result = toolCallFromPart("read", { filePath: "/src/index.ts", offset: 0 })
48+
expect(result.locations).toEqual([{ path: "/src/index.ts" }])
49+
})
50+
51+
it("includes line range suffix", () => {
52+
const result = toolCallFromPart("read", { filePath: "/src/index.ts", offset: 10, limit: 20 })
53+
expect(result.title).toBe("Read /src/index.ts (11 - 30)")
54+
})
55+
56+
it("includes from-line suffix", () => {
57+
const result = toolCallFromPart("read", { filePath: "/src/index.ts", offset: 10 })
58+
expect(result.title).toBe("Read /src/index.ts (from line 11)")
59+
})
60+
61+
it("falls back when no file path", () => {
62+
const result = toolCallFromPart("read", {})
63+
expect(result.title).toBe("Read File")
64+
expect(result.locations).toEqual([])
65+
})
66+
})
67+
68+
describe("edit/str_replace", () => {
69+
it("formats edit with diff content", () => {
70+
const result = toolCallFromPart("edit", {
71+
filePath: "/src/app.ts",
72+
oldString: "foo",
73+
newString: "bar",
74+
})
75+
expect(result.title).toBe("Edit `/src/app.ts`")
76+
expect(result.kind).toBe("edit")
77+
expect(result.content).toEqual([{ type: "diff", path: "/src/app.ts", oldText: "foo", newText: "bar" }])
78+
expect(result.locations).toEqual([{ path: "/src/app.ts" }])
79+
})
80+
81+
it("handles missing file path", () => {
82+
const result = toolCallFromPart("str_replace", { oldString: "a", newString: "b" })
83+
expect(result.title).toBe("Edit")
84+
expect(result.content).toEqual([])
85+
})
86+
})
87+
88+
describe("write/create", () => {
89+
it("formats write with diff content (null oldText)", () => {
90+
const result = toolCallFromPart("write", { filePath: "/new.ts", content: "hello" })
91+
expect(result.title).toBe("Write /new.ts")
92+
expect(result.kind).toBe("edit")
93+
expect(result.content).toEqual([{ type: "diff", path: "/new.ts", oldText: null, newText: "hello" }])
94+
})
95+
})
96+
97+
describe("glob/find", () => {
98+
it("formats glob with path and pattern", () => {
99+
const result = toolCallFromPart("glob", { path: "/src", pattern: "*.ts" })
100+
expect(result.title).toBe("Find `/src` `*.ts`")
101+
expect(result.kind).toBe("search")
102+
})
103+
104+
it("absolutizes relative paths", () => {
105+
const result = toolCallFromPart("glob", { path: "src", pattern: "*.ts" })
106+
expect(result.locations[0].path.startsWith("/")).toBe(true)
107+
})
108+
109+
it("handles no path or pattern", () => {
110+
const result = toolCallFromPart("find", {})
111+
expect(result.title).toBe("Find")
112+
})
113+
})
114+
115+
describe("grep/search", () => {
116+
it("formats grep with pattern and path", () => {
117+
const result = toolCallFromPart("grep", { pattern: "TODO", path: "/src" })
118+
expect(result.title).toBe('grep "TODO" /src')
119+
expect(result.kind).toBe("search")
120+
})
121+
122+
it("truncates long patterns", () => {
123+
const long = "a".repeat(50)
124+
const result = toolCallFromPart("grep", { pattern: long })
125+
expect(result.title.length).toBeLessThanOrEqual(40)
126+
})
127+
})
128+
129+
describe("webfetch/fetch", () => {
130+
it("formats fetch with url", () => {
131+
const result = toolCallFromPart("webfetch", { url: "https://example.com", prompt: "get title" })
132+
expect(result.title).toBe("Fetch https://example.com")
133+
expect(result.kind).toBe("fetch")
134+
expect(result.content).toHaveLength(1)
135+
})
136+
})
137+
138+
describe("websearch", () => {
139+
it("formats search with query", () => {
140+
const result = toolCallFromPart("websearch", { query: "bun test" })
141+
expect(result.title).toBe('"bun test"')
142+
expect(result.kind).toBe("fetch")
143+
})
144+
})
145+
146+
describe("task", () => {
147+
it("formats task with description", () => {
148+
const result = toolCallFromPart("task", { description: "Research APIs", prompt: "find REST patterns" })
149+
expect(result.title).toBe("Research APIs")
150+
expect(result.kind).toBe("think")
151+
expect(result.content).toHaveLength(1)
152+
})
153+
})
154+
155+
describe("plan mode", () => {
156+
it("emits switch_mode kind for plan_enter", () => {
157+
const result = toolCallFromPart("plan_enter", {})
158+
expect(result.title).toBe("Enter Plan Mode")
159+
expect(result.kind).toBe("switch_mode")
160+
})
161+
162+
it("emits switch_mode kind for plan_exit", () => {
163+
const result = toolCallFromPart("plan_exit", {})
164+
expect(result.title).toBe("Exit Plan Mode")
165+
expect(result.kind).toBe("switch_mode")
166+
})
167+
})
168+
169+
describe("bash kind pinning", () => {
170+
it("uses kind 'other' instead of spec 'execute' to avoid Zed blue run-box styling", () => {
171+
const result = toolCallFromPart("bash", { command: "ls", description: "List files" })
172+
expect(result.kind).toBe("other")
173+
})
174+
})
175+
176+
describe("list", () => {
177+
it("uses read kind for directory listing", () => {
178+
const result = toolCallFromPart("list", { path: "/src" })
179+
expect(result.kind).toBe("read")
180+
})
181+
})
182+
183+
describe("default", () => {
184+
it("falls back to tool name for unknown tools", () => {
185+
const result = toolCallFromPart("unknownTool", {})
186+
expect(result.title).toBe("unknownTool")
187+
expect(result.kind).toBe("other")
188+
})
189+
190+
it("uses description if available", () => {
191+
const result = toolCallFromPart("custom", { description: "Custom action" })
192+
expect(result.title).toBe("Custom action")
193+
})
194+
})
195+
})
196+
197+
describe("toolResultFromPart", () => {
198+
describe("bash/shell/terminal", () => {
199+
it("returns stdout for success", () => {
200+
const result = toolResultFromPart("bash", { command: "ls" }, "file1\nfile2", false)
201+
expect(result.rawOutput).toEqual({ stdout: "file1\nfile2" })
202+
expect(result.content).toHaveLength(1)
203+
expect(result.content[0]).toEqual({ type: "content", content: { type: "text", text: "file1\nfile2" } })
204+
})
205+
206+
it("returns stderr for error", () => {
207+
const result = toolResultFromPart("shell", { command: "bad" }, "command not found", true)
208+
expect(result.rawOutput).toEqual({ stderr: "command not found" })
209+
})
210+
})
211+
212+
describe("edit/str_replace", () => {
213+
it("includes diff content on success", () => {
214+
const result = toolResultFromPart(
215+
"edit",
216+
{ filePath: "/src/app.ts", oldString: "foo", newString: "bar" },
217+
"Applied edit",
218+
false,
219+
)
220+
expect(result.rawOutput).toEqual({ stdout: "Applied edit" })
221+
expect(result.content).toHaveLength(2)
222+
expect(result.content[1]).toEqual({ type: "diff", path: "/src/app.ts", oldText: "foo", newText: "bar" })
223+
})
224+
225+
it("skips diff content on error", () => {
226+
const result = toolResultFromPart(
227+
"edit",
228+
{ filePath: "/src/app.ts", oldString: "foo", newString: "bar" },
229+
"old_string not found",
230+
true,
231+
)
232+
expect(result.content).toHaveLength(1)
233+
})
234+
})
235+
236+
describe("write/create", () => {
237+
it("includes diff content with null oldText on success", () => {
238+
const result = toolResultFromPart("write", { filePath: "/new.ts", content: "hello" }, "Created", false)
239+
expect(result.content).toHaveLength(2)
240+
expect(result.content[1]).toEqual({ type: "diff", path: "/new.ts", oldText: null, newText: "hello" })
241+
})
242+
243+
it("skips diff on error", () => {
244+
const result = toolResultFromPart("write", { filePath: "/new.ts", content: "hello" }, "Permission denied", true)
245+
expect(result.content).toHaveLength(1)
246+
})
247+
})
248+
249+
describe("patch/apply_patch", () => {
250+
it("includes patch text content on success", () => {
251+
const patch = "--- a/file\n+++ b/file\n@@ -1 +1 @@\n-old\n+new"
252+
const result = toolResultFromPart("patch", { filePath: "/file", diff: patch }, "Patched", false)
253+
expect(result.content).toHaveLength(2)
254+
expect(result.content[1]).toEqual({ type: "content", content: { type: "text", text: patch } })
255+
})
256+
257+
it("skips patch content on error", () => {
258+
const result = toolResultFromPart("apply_patch", { filePath: "/file", diff: "bad" }, "Failed", true)
259+
expect(result.content).toHaveLength(1)
260+
})
261+
})
262+
263+
describe("default", () => {
264+
it("returns stdout for success", () => {
265+
const result = toolResultFromPart("unknown", {}, "some output", false)
266+
expect(result.rawOutput).toEqual({ stdout: "some output" })
267+
expect(result.content).toHaveLength(1)
268+
})
269+
270+
it("returns stderr for error", () => {
271+
const result = toolResultFromPart("unknown", {}, "error msg", true)
272+
expect(result.rawOutput).toEqual({ stderr: "error msg" })
273+
})
274+
275+
it("wraps error output in markdown fence", () => {
276+
const result = toolResultFromPart("unknown", {}, "some error", true)
277+
const text = (result.content[0] as any).content.text
278+
expect(text).toContain("```")
279+
expect(text).toContain("some error")
280+
})
281+
})
282+
})

0 commit comments

Comments
 (0)