Skip to content

Commit 72358f2

Browse files
authored
Add tests + benchmark for parseAssistantMessage V1 + 2 (#3538)
1 parent a14b655 commit 72358f2

File tree

5 files changed

+764
-20
lines changed

5 files changed

+764
-20
lines changed
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
// npx jest src/core/assistant-message/__tests__/parseAssistantMessage.test.ts
2+
3+
import { TextContent, ToolUse } from "../../../shared/tools"
4+
5+
import { AssistantMessageContent, parseAssistantMessage as parseAssistantMessageV1 } from "../parseAssistantMessage"
6+
import { parseAssistantMessageV2 } from "../parseAssistantMessageV2"
7+
8+
const isEmptyTextContent = (block: AssistantMessageContent) =>
9+
block.type === "text" && (block as TextContent).content === ""
10+
11+
;[parseAssistantMessageV1, parseAssistantMessageV2].forEach((parser, index) => {
12+
describe(`parseAssistantMessageV${index + 1}`, () => {
13+
describe("text content parsing", () => {
14+
it("should parse a simple text message", () => {
15+
const message = "This is a simple text message"
16+
const result = parser(message)
17+
18+
expect(result).toHaveLength(1)
19+
expect(result[0]).toEqual({
20+
type: "text",
21+
content: message,
22+
partial: true, // Text is always partial when it's the last content
23+
})
24+
})
25+
26+
it("should parse a multi-line text message", () => {
27+
const message = "This is a multi-line\ntext message\nwith several lines"
28+
const result = parser(message)
29+
30+
expect(result).toHaveLength(1)
31+
expect(result[0]).toEqual({
32+
type: "text",
33+
content: message,
34+
partial: true, // Text is always partial when it's the last content
35+
})
36+
})
37+
38+
it("should mark text as partial when it's the last content in the message", () => {
39+
const message = "This is a partial text"
40+
const result = parser(message)
41+
42+
expect(result).toHaveLength(1)
43+
expect(result[0]).toEqual({
44+
type: "text",
45+
content: message,
46+
partial: true,
47+
})
48+
})
49+
})
50+
51+
describe("tool use parsing", () => {
52+
it("should parse a simple tool use", () => {
53+
const message = "<read_file><path>src/file.ts</path></read_file>"
54+
const result = parser(message).filter((block) => !isEmptyTextContent(block))
55+
56+
expect(result).toHaveLength(1)
57+
const toolUse = result[0] as ToolUse
58+
expect(toolUse.type).toBe("tool_use")
59+
expect(toolUse.name).toBe("read_file")
60+
expect(toolUse.params.path).toBe("src/file.ts")
61+
expect(toolUse.partial).toBe(false)
62+
})
63+
64+
it("should parse a tool use with multiple parameters", () => {
65+
const message =
66+
"<read_file><path>src/file.ts</path><start_line>10</start_line><end_line>20</end_line></read_file>"
67+
const result = parser(message).filter((block) => !isEmptyTextContent(block))
68+
69+
expect(result).toHaveLength(1)
70+
const toolUse = result[0] as ToolUse
71+
expect(toolUse.type).toBe("tool_use")
72+
expect(toolUse.name).toBe("read_file")
73+
expect(toolUse.params.path).toBe("src/file.ts")
74+
expect(toolUse.params.start_line).toBe("10")
75+
expect(toolUse.params.end_line).toBe("20")
76+
expect(toolUse.partial).toBe(false)
77+
})
78+
79+
it("should mark tool use as partial when it's not closed", () => {
80+
const message = "<read_file><path>src/file.ts</path>"
81+
const result = parser(message).filter((block) => !isEmptyTextContent(block))
82+
83+
expect(result).toHaveLength(1)
84+
const toolUse = result[0] as ToolUse
85+
expect(toolUse.type).toBe("tool_use")
86+
expect(toolUse.name).toBe("read_file")
87+
expect(toolUse.params.path).toBe("src/file.ts")
88+
expect(toolUse.partial).toBe(true)
89+
})
90+
91+
it("should handle a partial parameter in a tool use", () => {
92+
const message = "<read_file><path>src/file.ts"
93+
const result = parser(message).filter((block) => !isEmptyTextContent(block))
94+
95+
expect(result).toHaveLength(1)
96+
const toolUse = result[0] as ToolUse
97+
expect(toolUse.type).toBe("tool_use")
98+
expect(toolUse.name).toBe("read_file")
99+
expect(toolUse.params.path).toBe("src/file.ts")
100+
expect(toolUse.partial).toBe(true)
101+
})
102+
})
103+
104+
describe("mixed content parsing", () => {
105+
it("should parse text followed by a tool use", () => {
106+
const message = "Here's the file content: <read_file><path>src/file.ts</path></read_file>"
107+
const result = parser(message)
108+
109+
expect(result).toHaveLength(2)
110+
111+
const textContent = result[0] as TextContent
112+
expect(textContent.type).toBe("text")
113+
expect(textContent.content).toBe("Here's the file content:")
114+
expect(textContent.partial).toBe(false)
115+
116+
const toolUse = result[1] as ToolUse
117+
expect(toolUse.type).toBe("tool_use")
118+
expect(toolUse.name).toBe("read_file")
119+
expect(toolUse.params.path).toBe("src/file.ts")
120+
expect(toolUse.partial).toBe(false)
121+
})
122+
123+
it("should parse a tool use followed by text", () => {
124+
const message = "<read_file><path>src/file.ts</path></read_file>Here's what I found in the file."
125+
const result = parser(message).filter((block) => !isEmptyTextContent(block))
126+
127+
expect(result).toHaveLength(2)
128+
129+
const toolUse = result[0] as ToolUse
130+
expect(toolUse.type).toBe("tool_use")
131+
expect(toolUse.name).toBe("read_file")
132+
expect(toolUse.params.path).toBe("src/file.ts")
133+
expect(toolUse.partial).toBe(false)
134+
135+
const textContent = result[1] as TextContent
136+
expect(textContent.type).toBe("text")
137+
expect(textContent.content).toBe("Here's what I found in the file.")
138+
expect(textContent.partial).toBe(true)
139+
})
140+
141+
it("should parse multiple tool uses separated by text", () => {
142+
const message =
143+
"First file: <read_file><path>src/file1.ts</path></read_file>Second file: <read_file><path>src/file2.ts</path></read_file>"
144+
const result = parser(message)
145+
146+
expect(result).toHaveLength(4)
147+
148+
expect(result[0].type).toBe("text")
149+
expect((result[0] as TextContent).content).toBe("First file:")
150+
151+
expect(result[1].type).toBe("tool_use")
152+
expect((result[1] as ToolUse).name).toBe("read_file")
153+
expect((result[1] as ToolUse).params.path).toBe("src/file1.ts")
154+
155+
expect(result[2].type).toBe("text")
156+
expect((result[2] as TextContent).content).toBe("Second file:")
157+
158+
expect(result[3].type).toBe("tool_use")
159+
expect((result[3] as ToolUse).name).toBe("read_file")
160+
expect((result[3] as ToolUse).params.path).toBe("src/file2.ts")
161+
})
162+
})
163+
164+
describe("special cases", () => {
165+
it("should handle the write_to_file tool with content that contains closing tags", () => {
166+
const message = `<write_to_file><path>src/file.ts</path><content>
167+
function example() {
168+
// This has XML-like content: </content>
169+
return true;
170+
}
171+
</content><line_count>5</line_count></write_to_file>`
172+
173+
const result = parser(message).filter((block) => !isEmptyTextContent(block))
174+
175+
expect(result).toHaveLength(1)
176+
const toolUse = result[0] as ToolUse
177+
expect(toolUse.type).toBe("tool_use")
178+
expect(toolUse.name).toBe("write_to_file")
179+
expect(toolUse.params.path).toBe("src/file.ts")
180+
expect(toolUse.params.line_count).toBe("5")
181+
expect(toolUse.params.content).toContain("function example()")
182+
expect(toolUse.params.content).toContain("// This has XML-like content: </content>")
183+
expect(toolUse.params.content).toContain("return true;")
184+
expect(toolUse.partial).toBe(false)
185+
})
186+
187+
it("should handle empty messages", () => {
188+
const message = ""
189+
const result = parser(message)
190+
191+
expect(result).toHaveLength(0)
192+
})
193+
194+
it("should handle malformed tool use tags", () => {
195+
const message = "This has a <not_a_tool>malformed tag</not_a_tool>"
196+
const result = parser(message)
197+
198+
expect(result).toHaveLength(1)
199+
expect(result[0].type).toBe("text")
200+
expect((result[0] as TextContent).content).toBe(message)
201+
})
202+
203+
it("should handle tool use with no parameters", () => {
204+
const message = "<browser_action></browser_action>"
205+
const result = parser(message).filter((block) => !isEmptyTextContent(block))
206+
207+
expect(result).toHaveLength(1)
208+
const toolUse = result[0] as ToolUse
209+
expect(toolUse.type).toBe("tool_use")
210+
expect(toolUse.name).toBe("browser_action")
211+
expect(Object.keys(toolUse.params).length).toBe(0)
212+
expect(toolUse.partial).toBe(false)
213+
})
214+
215+
it("should handle nested tool tags that aren't actually nested", () => {
216+
const message =
217+
"<execute_command><command>echo '<read_file><path>test.txt</path></read_file>'</command></execute_command>"
218+
219+
const result = parser(message).filter((block) => !isEmptyTextContent(block))
220+
221+
expect(result).toHaveLength(1)
222+
const toolUse = result[0] as ToolUse
223+
expect(toolUse.type).toBe("tool_use")
224+
expect(toolUse.name).toBe("execute_command")
225+
expect(toolUse.params.command).toBe("echo '<read_file><path>test.txt</path></read_file>'")
226+
expect(toolUse.partial).toBe(false)
227+
})
228+
229+
it("should handle a tool use with a parameter containing XML-like content", () => {
230+
const message = "<search_files><regex><div>.*</div></regex><path>src</path></search_files>"
231+
const result = parser(message).filter((block) => !isEmptyTextContent(block))
232+
233+
expect(result).toHaveLength(1)
234+
const toolUse = result[0] as ToolUse
235+
expect(toolUse.type).toBe("tool_use")
236+
expect(toolUse.name).toBe("search_files")
237+
expect(toolUse.params.regex).toBe("<div>.*</div>")
238+
expect(toolUse.params.path).toBe("src")
239+
expect(toolUse.partial).toBe(false)
240+
})
241+
242+
it("should handle consecutive tool uses without text in between", () => {
243+
const message =
244+
"<read_file><path>file1.ts</path></read_file><read_file><path>file2.ts</path></read_file>"
245+
const result = parser(message).filter((block) => !isEmptyTextContent(block))
246+
247+
expect(result).toHaveLength(2)
248+
249+
const toolUse1 = result[0] as ToolUse
250+
expect(toolUse1.type).toBe("tool_use")
251+
expect(toolUse1.name).toBe("read_file")
252+
expect(toolUse1.params.path).toBe("file1.ts")
253+
expect(toolUse1.partial).toBe(false)
254+
255+
const toolUse2 = result[1] as ToolUse
256+
expect(toolUse2.type).toBe("tool_use")
257+
expect(toolUse2.name).toBe("read_file")
258+
expect(toolUse2.params.path).toBe("file2.ts")
259+
expect(toolUse2.partial).toBe(false)
260+
})
261+
262+
it("should handle whitespace in parameters", () => {
263+
const message = "<read_file><path> src/file.ts </path></read_file>"
264+
const result = parser(message).filter((block) => !isEmptyTextContent(block))
265+
266+
expect(result).toHaveLength(1)
267+
const toolUse = result[0] as ToolUse
268+
expect(toolUse.type).toBe("tool_use")
269+
expect(toolUse.name).toBe("read_file")
270+
expect(toolUse.params.path).toBe("src/file.ts")
271+
expect(toolUse.partial).toBe(false)
272+
})
273+
274+
it("should handle multi-line parameters", () => {
275+
const message = `<write_to_file><path>file.ts</path><content>
276+
line 1
277+
line 2
278+
line 3
279+
</content><line_count>3</line_count></write_to_file>`
280+
const result = parser(message).filter((block) => !isEmptyTextContent(block))
281+
282+
expect(result).toHaveLength(1)
283+
const toolUse = result[0] as ToolUse
284+
expect(toolUse.type).toBe("tool_use")
285+
expect(toolUse.name).toBe("write_to_file")
286+
expect(toolUse.params.path).toBe("file.ts")
287+
expect(toolUse.params.content).toContain("line 1")
288+
expect(toolUse.params.content).toContain("line 2")
289+
expect(toolUse.params.content).toContain("line 3")
290+
expect(toolUse.params.line_count).toBe("3")
291+
expect(toolUse.partial).toBe(false)
292+
})
293+
294+
it("should handle a complex message with multiple content types", () => {
295+
const message = `I'll help you with that task.
296+
297+
<read_file><path>src/index.ts</path></read_file>
298+
299+
Now let's modify the file:
300+
301+
<write_to_file><path>src/index.ts</path><content>
302+
// Updated content
303+
console.log("Hello world");
304+
</content><line_count>2</line_count></write_to_file>
305+
306+
Let's run the code:
307+
308+
<execute_command><command>node src/index.ts</command></execute_command>`
309+
310+
const result = parser(message)
311+
312+
expect(result).toHaveLength(6)
313+
314+
// First text block
315+
expect(result[0].type).toBe("text")
316+
expect((result[0] as TextContent).content).toBe("I'll help you with that task.")
317+
318+
// First tool use (read_file)
319+
expect(result[1].type).toBe("tool_use")
320+
expect((result[1] as ToolUse).name).toBe("read_file")
321+
322+
// Second text block
323+
expect(result[2].type).toBe("text")
324+
expect((result[2] as TextContent).content).toContain("Now let's modify the file:")
325+
326+
// Second tool use (write_to_file)
327+
expect(result[3].type).toBe("tool_use")
328+
expect((result[3] as ToolUse).name).toBe("write_to_file")
329+
330+
// Third text block
331+
expect(result[4].type).toBe("text")
332+
expect((result[4] as TextContent).content).toContain("Let's run the code:")
333+
334+
// Third tool use (execute_command)
335+
expect(result[5].type).toBe("tool_use")
336+
expect((result[5] as ToolUse).name).toBe("execute_command")
337+
})
338+
})
339+
})
340+
})

0 commit comments

Comments
 (0)