Skip to content

Commit 53ea866

Browse files
committed
test(tool): move prompt protocol tests
1 parent f101e9b commit 53ea866

3 files changed

Lines changed: 139 additions & 140 deletions

File tree

‎src/harness/tool/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,8 @@ fn json_value_kind(value: &Value) -> &'static str {
540540
}
541541
}
542542

543+
#[cfg(test)]
544+
mod prompt_test;
543545
#[cfg(test)]
544546
mod schema_test;
545547
#[cfg(test)]

‎src/harness/tool/prompt.rs‎

Lines changed: 0 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -165,143 +165,3 @@ fn parse_one(inner: &str, index: usize) -> Option<ToolCall> {
165165
invalid: None,
166166
})
167167
}
168-
169-
#[cfg(test)]
170-
mod test {
171-
use super::*;
172-
173-
fn schema(name: &str) -> ToolSchema {
174-
ToolSchema {
175-
name: name.to_string(),
176-
description: format!("{name} description"),
177-
parameters: serde_json::json!({"type": "object"}),
178-
format: Default::default(),
179-
}
180-
}
181-
182-
#[test]
183-
fn instructions_list_each_tool() {
184-
let text = prompt_tool_instructions(&[schema("read_file"), schema("write_file")]);
185-
assert!(text.contains("## Tool Use Protocol"));
186-
assert!(text.contains("<tool_call>"));
187-
assert!(text.contains("**read_file**"));
188-
assert!(text.contains("**write_file**"));
189-
}
190-
191-
#[test]
192-
fn with_tool_instructions_appends_to_system() {
193-
let msgs = vec![Message::system("You are helpful."), Message::user("hi")];
194-
let out = with_prompt_tool_instructions(&msgs, &[schema("read_file")]);
195-
assert_eq!(out.len(), 2);
196-
let Message::System(sys) = &out[0] else {
197-
panic!("first message should stay system")
198-
};
199-
// Original prompt preserved + protocol appended.
200-
let joined: String = sys
201-
.content
202-
.iter()
203-
.filter_map(|b| match b {
204-
ContentBlock::Text(t) => Some(t.as_str()),
205-
_ => None,
206-
})
207-
.collect();
208-
assert!(joined.contains("You are helpful."));
209-
assert!(joined.contains("Tool Use Protocol"));
210-
}
211-
212-
#[test]
213-
fn with_tool_instructions_inserts_system_when_absent() {
214-
let msgs = vec![Message::user("hi")];
215-
let out = with_prompt_tool_instructions(&msgs, &[schema("read_file")]);
216-
assert_eq!(out.len(), 2);
217-
assert!(matches!(out[0], Message::System(_)));
218-
}
219-
220-
#[test]
221-
fn empty_tools_leaves_messages_unchanged() {
222-
let msgs = vec![Message::user("hi")];
223-
assert_eq!(with_prompt_tool_instructions(&msgs, &[]), msgs);
224-
}
225-
226-
#[test]
227-
fn coalesces_consecutive_tool_results_into_one_user_turn() {
228-
let messages = vec![
229-
Message::user("question"),
230-
Message::assistant("calling tools"),
231-
Message::tool("call-1", "first"),
232-
Message::tool("call-2", "second"),
233-
Message::assistant("done"),
234-
];
235-
236-
let out = coalesce_prompt_tool_results(&messages);
237-
238-
assert_eq!(out.len(), 4);
239-
assert!(matches!(out[0], Message::User(_)));
240-
assert!(matches!(out[1], Message::Assistant(_)));
241-
assert!(matches!(out[2], Message::User(_)));
242-
assert_eq!(out[2].text(), "[Tool results]\nfirst\nsecond");
243-
assert!(matches!(out[3], Message::Assistant(_)));
244-
}
245-
246-
#[test]
247-
fn coalescing_without_tool_results_is_identity() {
248-
let messages = vec![Message::system("system"), Message::user("question")];
249-
assert_eq!(coalesce_prompt_tool_results(&messages), messages);
250-
}
251-
252-
#[test]
253-
fn parses_single_tool_call() {
254-
let text = r#"Let me read it.
255-
<tool_call>
256-
{"name": "read_file", "arguments": {"path": "a.txt"}}
257-
</tool_call>"#;
258-
let (cleaned, calls) = parse_prompt_tool_calls_from_text(text);
259-
assert_eq!(cleaned, "Let me read it.");
260-
assert_eq!(calls.len(), 1);
261-
assert_eq!(calls[0].name, "read_file");
262-
assert_eq!(calls[0].id, "call_1");
263-
assert_eq!(calls[0].arguments, serde_json::json!({"path": "a.txt"}));
264-
}
265-
266-
#[test]
267-
fn parses_multiple_calls_and_keeps_prose() {
268-
let text = r#"a<tool_call>{"name":"one","arguments":{}}</tool_call>b<tool_call>{"name":"two","arguments":{"x":1}}</tool_call>c"#;
269-
let (cleaned, calls) = parse_prompt_tool_calls_from_text(text);
270-
assert_eq!(cleaned, "abc");
271-
assert_eq!(calls.len(), 2);
272-
assert_eq!(calls[0].name, "one");
273-
assert_eq!(calls[1].name, "two");
274-
assert_eq!(calls[1].id, "call_2");
275-
}
276-
277-
#[test]
278-
fn missing_arguments_defaults_to_empty_object() {
279-
let (_, calls) =
280-
parse_prompt_tool_calls_from_text(r#"<tool_call>{"name":"noargs"}</tool_call>"#);
281-
assert_eq!(calls.len(), 1);
282-
assert_eq!(calls[0].arguments, serde_json::json!({}));
283-
}
284-
285-
#[test]
286-
fn malformed_block_is_dropped() {
287-
let (cleaned, calls) =
288-
parse_prompt_tool_calls_from_text("<tool_call>not json</tool_call>done");
289-
assert!(calls.is_empty());
290-
assert_eq!(cleaned, "done");
291-
}
292-
293-
#[test]
294-
fn unterminated_block_kept_as_text() {
295-
let text = "text <tool_call>{\"name\":\"x\"}";
296-
let (cleaned, calls) = parse_prompt_tool_calls_from_text(text);
297-
assert!(calls.is_empty());
298-
assert_eq!(cleaned, "text <tool_call>{\"name\":\"x\"}");
299-
}
300-
301-
#[test]
302-
fn no_blocks_returns_text_verbatim() {
303-
let (cleaned, calls) = parse_prompt_tool_calls_from_text("just a normal answer");
304-
assert!(calls.is_empty());
305-
assert_eq!(cleaned, "just a normal answer");
306-
}
307-
}

‎src/harness/tool/prompt_test.rs‎

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//! Tests for the prompt-guided tool-call protocol.
2+
3+
use super::*;
4+
use crate::harness::message::{ContentBlock, Message};
5+
6+
fn schema(name: &str) -> ToolSchema {
7+
ToolSchema {
8+
name: name.to_string(),
9+
description: format!("{name} description"),
10+
parameters: serde_json::json!({"type": "object"}),
11+
format: Default::default(),
12+
}
13+
}
14+
15+
#[test]
16+
fn prompt_instructions_list_each_tool() {
17+
let text = prompt_tool_instructions(&[schema("read_file"), schema("write_file")]);
18+
assert!(text.contains("## Tool Use Protocol"));
19+
assert!(text.contains("<tool_call>"));
20+
assert!(text.contains("**read_file**"));
21+
assert!(text.contains("**write_file**"));
22+
}
23+
24+
#[test]
25+
fn prompt_instructions_append_to_system() {
26+
let msgs = vec![Message::system("You are helpful."), Message::user("hi")];
27+
let out = with_prompt_tool_instructions(&msgs, &[schema("read_file")]);
28+
assert_eq!(out.len(), 2);
29+
let Message::System(system) = &out[0] else {
30+
panic!("first message should stay system")
31+
};
32+
let joined: String = system
33+
.content
34+
.iter()
35+
.filter_map(|block| match block {
36+
ContentBlock::Text(text) => Some(text.as_str()),
37+
_ => None,
38+
})
39+
.collect();
40+
assert!(joined.contains("You are helpful."));
41+
assert!(joined.contains("Tool Use Protocol"));
42+
}
43+
44+
#[test]
45+
fn prompt_instructions_insert_system_when_absent() {
46+
let msgs = vec![Message::user("hi")];
47+
let out = with_prompt_tool_instructions(&msgs, &[schema("read_file")]);
48+
assert_eq!(out.len(), 2);
49+
assert!(matches!(out[0], Message::System(_)));
50+
}
51+
52+
#[test]
53+
fn empty_tools_leave_messages_unchanged() {
54+
let msgs = vec![Message::user("hi")];
55+
assert_eq!(with_prompt_tool_instructions(&msgs, &[]), msgs);
56+
}
57+
58+
#[test]
59+
fn prompt_results_coalesce_consecutive_tool_messages() {
60+
let messages = vec![
61+
Message::user("question"),
62+
Message::assistant("calling tools"),
63+
Message::tool("call-1", "first"),
64+
Message::tool("call-2", "second"),
65+
Message::assistant("done"),
66+
];
67+
68+
let out = coalesce_prompt_tool_results(&messages);
69+
70+
assert_eq!(out.len(), 4);
71+
assert!(matches!(out[0], Message::User(_)));
72+
assert!(matches!(out[1], Message::Assistant(_)));
73+
assert!(matches!(out[2], Message::User(_)));
74+
assert_eq!(out[2].text(), "[Tool results]\nfirst\nsecond");
75+
assert!(matches!(out[3], Message::Assistant(_)));
76+
}
77+
78+
#[test]
79+
fn prompt_result_coalescing_without_tools_is_identity() {
80+
let messages = vec![Message::system("system"), Message::user("question")];
81+
assert_eq!(coalesce_prompt_tool_results(&messages), messages);
82+
}
83+
84+
#[test]
85+
fn prompt_parser_extracts_single_tool_call() {
86+
let text = r#"Let me read it.
87+
<tool_call>
88+
{"name": "read_file", "arguments": {"path": "a.txt"}}
89+
</tool_call>"#;
90+
let (cleaned, calls) = parse_prompt_tool_calls_from_text(text);
91+
assert_eq!(cleaned, "Let me read it.");
92+
assert_eq!(calls.len(), 1);
93+
assert_eq!(calls[0].name, "read_file");
94+
assert_eq!(calls[0].id, "call_1");
95+
assert_eq!(calls[0].arguments, serde_json::json!({"path": "a.txt"}));
96+
}
97+
98+
#[test]
99+
fn prompt_parser_extracts_multiple_calls_and_keeps_prose() {
100+
let text = r#"a<tool_call>{"name":"one","arguments":{}}</tool_call>b<tool_call>{"name":"two","arguments":{"x":1}}</tool_call>c"#;
101+
let (cleaned, calls) = parse_prompt_tool_calls_from_text(text);
102+
assert_eq!(cleaned, "abc");
103+
assert_eq!(calls.len(), 2);
104+
assert_eq!(calls[0].name, "one");
105+
assert_eq!(calls[1].name, "two");
106+
assert_eq!(calls[1].id, "call_2");
107+
}
108+
109+
#[test]
110+
fn prompt_parser_defaults_missing_arguments_to_empty_object() {
111+
let (_, calls) =
112+
parse_prompt_tool_calls_from_text(r#"<tool_call>{"name":"noargs"}</tool_call>"#);
113+
assert_eq!(calls.len(), 1);
114+
assert_eq!(calls[0].arguments, serde_json::json!({}));
115+
}
116+
117+
#[test]
118+
fn prompt_parser_drops_malformed_block() {
119+
let (cleaned, calls) = parse_prompt_tool_calls_from_text("<tool_call>not json</tool_call>done");
120+
assert!(calls.is_empty());
121+
assert_eq!(cleaned, "done");
122+
}
123+
124+
#[test]
125+
fn prompt_parser_keeps_unterminated_block_as_text() {
126+
let text = "text <tool_call>{\"name\":\"x\"}";
127+
let (cleaned, calls) = parse_prompt_tool_calls_from_text(text);
128+
assert!(calls.is_empty());
129+
assert_eq!(cleaned, "text <tool_call>{\"name\":\"x\"}");
130+
}
131+
132+
#[test]
133+
fn prompt_parser_returns_plain_text_verbatim() {
134+
let (cleaned, calls) = parse_prompt_tool_calls_from_text("just a normal answer");
135+
assert!(calls.is_empty());
136+
assert_eq!(cleaned, "just a normal answer");
137+
}

0 commit comments

Comments
 (0)