use crate::brain::provider::custom_openai_compatible::{
BareToolArrayMatch, classify_bare_tool_array, extract_balanced_json, extract_text_tool_calls,
};
#[test]
fn balanced_json_simple() {
assert_eq!(extract_balanced_json(r#"{"a":1}"#), Some(7));
assert_eq!(extract_balanced_json(r#"{"a":{"b":2}} trailing"#), Some(13));
}
#[test]
fn balanced_json_strings_with_braces() {
let s = r#"{"cmd":"echo { nested } end"} trailing"#;
let consumed = extract_balanced_json(s).expect("balanced");
assert_eq!(&s[..consumed], r#"{"cmd":"echo { nested } end"}"#);
}
#[test]
fn balanced_json_escaped_quotes() {
let s = r#"{"msg":"he said \"hi\" then left"}"#;
let consumed = extract_balanced_json(s).expect("balanced");
assert_eq!(consumed, s.len());
}
#[test]
fn balanced_json_unbalanced_returns_none() {
assert_eq!(extract_balanced_json(r#"{"a":1"#), None);
assert_eq!(extract_balanced_json("not json"), None);
}
#[test]
fn extract_tool_call_closed() {
let text = r#"sure, running it. <tool_call>{"name":"bash","arguments":{"command":"ls"}}</tool_call> done."#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "ls");
assert!(!cleaned.contains("<tool_call>"));
assert!(cleaned.contains("sure, running it"));
assert!(cleaned.contains("done"));
}
#[test]
fn extract_tool_call_open_ended() {
let text = r#"<tool_call>{"name":"web_search","arguments":{"query":"rust traits"}}"#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "web_search");
assert_eq!(calls[0].1["query"], "rust traits");
assert!(cleaned.trim().is_empty());
}
#[test]
fn extract_tool_call_nested_braces() {
let text = r#"<tool_call>{"name":"set","arguments":{"obj":{"k":"v"},"n":1}}</tool_call>"#;
let (calls, _) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "set");
assert_eq!(calls[0].1["obj"]["k"], "v");
assert_eq!(calls[0].1["n"], 1);
}
#[test]
fn extract_multiple_tool_calls() {
let text = concat!(
"first <tool_call>{\"name\":\"a\",\"arguments\":{}}</tool_call> ",
"then <tool_call>{\"name\":\"b\",\"arguments\":{\"x\":2}}</tool_call>"
);
let (calls, _) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].0, "a");
assert_eq!(calls[1].0, "b");
assert_eq!(calls[1].1["x"], 2);
}
#[test]
fn extract_tool_call_with_field_aliases() {
let text = r#"<tool_call>{"tool_name":"bash","args":{"command":"pwd"}}</tool_call>"#;
let (calls, _) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "pwd");
}
#[test]
fn extract_tool_call_stringified_arguments() {
let text = r#"<tool_call>{"name":"run","arguments":"{\"cmd\":\"go\"}"}</tool_call>"#;
let (calls, _) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "run");
assert_eq!(calls[0].1["cmd"], "go");
}
#[test]
fn extract_function_format() {
let text = r#"<function=web_search><parameter=query>rust</parameter></function>"#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "web_search");
assert_eq!(calls[0].1["query"], "rust");
assert!(cleaned.trim().is_empty());
}
#[test]
fn skips_prose_mention_with_invalid_json() {
let text = "the <tool_call> tag is special";
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 0);
assert_eq!(cleaned, text);
}
#[test]
fn noop_without_markers() {
let text = "just prose, no tool tags here";
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 0);
assert_eq!(cleaned, text);
}
#[test]
fn ignores_tool_call_without_name() {
let text = r#"<tool_call>{"arguments":{"x":1}}</tool_call>"#;
let (calls, _) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 0);
}
#[test]
fn extract_bare_tool_call_openai_envelope() {
let text = r#"tool_call:{"id":"call_001","type":"function","function":{"name":"bash","arguments":{"command":"ls -la"}}}"#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1, "must recover bare tool_call: prefix");
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "ls -la");
assert!(
cleaned.trim().is_empty(),
"the whole envelope must be stripped, got: {cleaned:?}"
);
}
#[test]
fn extract_bare_tool_call_with_preceding_text() {
let text = r#"I'll check that. tool_call:{"id":"c1","type":"function","function":{"name":"bash","arguments":{"command":"pwd"}}} done."#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "bash");
assert!(cleaned.contains("I'll check that"));
assert!(cleaned.contains("done"));
assert!(!cleaned.contains("tool_call:"));
}
#[test]
fn bare_marker_rejects_non_boundary_prefix() {
let text = r#"set_tool_call:{"x":1} and more"#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 0);
assert_eq!(cleaned, text);
}
#[test]
fn extract_tool_calls_array_envelope() {
let text = r#"{"tool_calls":[{"id":"c1","type":"function","function":{"name":"bash","arguments":{"command":"ls"}}},{"id":"c2","type":"function","function":{"name":"web_search","arguments":{"query":"rust"}}}]}"#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 2, "both envelope entries must be recovered");
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "ls");
assert_eq!(calls[1].0, "web_search");
assert_eq!(calls[1].1["query"], "rust");
assert!(cleaned.trim().is_empty());
}
#[test]
fn openai_nested_function_name_without_wrapper() {
let text =
r#"{"id":"c1","type":"function","function":{"name":"bash","arguments":{"command":"ls"}}}"#;
let (calls, _) = extract_text_tool_calls(text);
assert_eq!(
calls.len(),
0,
"bare OpenAI JSON without a marker must NOT be auto-extracted"
);
}
#[test]
fn extract_singular_tool_call_envelope() {
let text = r#"{"tool_call":{"name":"bash","arguments":{"command":"ls -la"}}}"#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "ls -la");
assert!(cleaned.trim().is_empty());
}
#[test]
fn extract_singular_envelope_with_malformed_json_missing_colons() {
let text = r#"{"tool_call" {"name" "bash" "arguments" {"command" "git status"}}}"#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(
calls.len(),
1,
"malformed singular envelope must be recovered"
);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "git status");
assert!(cleaned.trim().is_empty());
}
#[test]
fn singular_envelope_rejects_plural_match() {
let text = r#"{"tool_calls":[{"id":"c1","type":"function","function":{"name":"bash","arguments":{"command":"ls"}}}]}"#;
let (calls, _) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "ls");
}
#[test]
fn extract_claude_style_bash_invocation() {
let text = "Let me search for the OpenCode repo.\n\n\
<bash>\n\
<command>\n\
curl -s \"https://api.github.com/search/repositories?q=opencode+oauth\" | python3 -c \"import json,sys; print(json.load(sys.stdin))\"\n\
</command>\n\
</bash>";
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "bash");
assert!(
calls[0].1["command"]
.as_str()
.unwrap_or("")
.starts_with("curl"),
"command arg must round-trip"
);
assert!(cleaned.contains("Let me search"));
assert!(!cleaned.contains("<bash>"));
assert!(!cleaned.contains("</bash>"));
}
#[test]
fn claude_style_multiple_params() {
let text = "<edit_file>\n\
<path>src/main.rs</path>\n\
<old_string>foo</old_string>\n\
<new_string>bar</new_string>\n\
</edit_file>";
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "edit_file");
assert_eq!(calls[0].1["path"], "src/main.rs");
assert_eq!(calls[0].1["old_string"], "foo");
assert_eq!(calls[0].1["new_string"], "bar");
assert!(cleaned.trim().is_empty());
}
#[test]
fn claude_style_ignores_unknown_tag_names() {
let text = "The page has a <html><body>Hello</body></html> structure.";
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 0);
assert_eq!(cleaned, text);
}
#[test]
fn tool_calls_inside_prose_is_ignored() {
let text = r#"The field called "tool_calls" is an array. Another sentence."#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 0);
assert_eq!(cleaned, text);
}
#[test]
fn balanced_json_accepts_arrays() {
assert_eq!(extract_balanced_json(r#"[1,2,3]"#), Some(7));
assert_eq!(
extract_balanced_json(r#"[{"a":1},{"b":2}] trailing"#),
Some(17)
);
assert_eq!(extract_balanced_json(r#"[1,2"#), None);
}
#[test]
fn extract_bare_array_single_call_compact() {
let text = r#"Sure! [{"id":"call_1","type":"function","function":{"name":"bash","arguments":{"command":"ls"}}}] done."#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1, "got {:?}", calls);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "ls");
assert!(!cleaned.contains("call_1"));
assert!(cleaned.contains("Sure!"));
assert!(cleaned.contains("done"));
}
#[test]
fn extract_bare_array_pretty_printed_matches_log_shape() {
let text = "Good idea. I'll add automatic sitemap discovery.\n\n[\n {\n \"id\": \"call_1\",\n \"type\": \"function\",\n \"function\": {\n \"name\": \"edit_file\",\n \"arguments\": {\n \"path\": \"/x/scraper.rs\",\n \"operation\": \"replace\",\n \"old_text\": \"foo\",\n \"new_text\": \"bar\"\n }\n }\n }\n]";
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1, "got {:?}", calls);
assert_eq!(calls[0].0, "edit_file");
assert_eq!(calls[0].1["operation"], "replace");
assert!(cleaned.contains("Good idea"));
assert!(!cleaned.contains("call_1"));
assert!(!cleaned.contains("edit_file"));
}
#[test]
fn extract_bare_array_multiple_calls() {
let text = r#"[{"id":"call_a","type":"function","function":{"name":"bash","arguments":{"command":"git status"}}},{"id":"call_b","type":"function","function":{"name":"read_file","arguments":{"path":"/x"}}}]"#;
let (calls, _cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 2, "got {:?}", calls);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[1].0, "read_file");
assert_eq!(calls[1].1["path"], "/x");
}
#[test]
fn extract_bare_array_with_stringified_arguments() {
let text = r#"[{"id":"call_1","type":"function","function":{"name":"glob","arguments":"{\"pattern\":\"**/*.rs\"}"}}]"#;
let (calls, _) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1, "got {:?}", calls);
assert_eq!(calls[0].0, "glob");
assert_eq!(calls[0].1["pattern"], "**/*.rs");
}
#[test]
fn bare_array_anchor_requires_call_prefix() {
let text = r#"Here is JSON: [{"id":"banana_1","type":"function","function":{"name":"foo","arguments":{}}}] end."#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 0);
assert_eq!(cleaned, text);
}
#[test]
fn bare_array_with_prose_id_call_mention_is_ignored() {
let text = r#"The field "id":"call_xyz" is what OpenAI returns."#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 0);
assert_eq!(cleaned, text);
}
#[test]
fn classify_bare_tool_array_states() {
use BareToolArrayMatch::*;
assert_eq!(classify_bare_tool_array(""), Prefix);
assert_eq!(classify_bare_tool_array(" "), Prefix);
assert_eq!(classify_bare_tool_array("\n\n"), Prefix);
assert_eq!(classify_bare_tool_array("["), Prefix);
assert_eq!(classify_bare_tool_array("[\n"), Prefix);
assert_eq!(classify_bare_tool_array("[ {"), Prefix);
assert_eq!(classify_bare_tool_array("[\n {\n \"id\""), Prefix);
assert_eq!(classify_bare_tool_array("[{\"id\":"), Prefix);
assert_eq!(classify_bare_tool_array("[{\"id\":\"cal"), Prefix);
assert_eq!(classify_bare_tool_array("[{\"id\":\"call_"), Full);
assert_eq!(classify_bare_tool_array("[ {\"id\": \"call_1\"}]"), Full);
assert_eq!(
classify_bare_tool_array("[\n {\n \"id\": \"call_abc\"\n }\n]"),
Full
);
assert_eq!(classify_bare_tool_array("Hello"), None);
assert_eq!(classify_bare_tool_array("{not array"), None);
assert_eq!(classify_bare_tool_array("[1,2,3]"), None);
assert_eq!(classify_bare_tool_array("[{\"name\":\"x\"}]"), None);
assert_eq!(classify_bare_tool_array("[{\"id\":42}]"), None);
assert_eq!(classify_bare_tool_array("[{\"id\":\"banana_\""), None);
}
#[test]
fn extract_dict_by_call_id_single() {
let text = r#"{"call_5f8d9c7b4a3e2f1c8d6e5b9a": {"name": "read_file", "arguments": {"path": "/tmp/x"}}}"#;
let (calls, remaining) = extract_text_tool_calls(text);
assert_eq!(
calls.len(),
1,
"should extract one tool call from dict-by-id"
);
assert_eq!(calls[0].0, "read_file");
assert_eq!(calls[0].1["path"], "/tmp/x");
assert!(
remaining.trim().is_empty(),
"the matched object should be stripped from the remaining text, got: {remaining:?}",
);
}
#[test]
fn extract_dict_by_call_id_multiple_keys() {
let text = r#"{"call_aaa": {"name":"bash","arguments":{"command":"ls"}},"call_bbb": {"name":"read_file","arguments":{"path":"/x"}}}"#;
let (calls, _) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[1].0, "read_file");
}
#[test]
fn extract_dict_by_call_id_inside_markdown_fence() {
let text = "Pushed fix.\n\n```json\n{\"call_7b8f9e6d4c5a3b2e1d7c6a9f\": {\"name\": \"bash\", \"arguments\": {\"command\": \"git push\"}}}\n```\n\nMore prose after.";
let (calls, _remaining) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "git push");
}
#[test]
fn dict_by_call_id_prose_mention_ignored() {
let text = r#"The "call_id" field is set in your config."#;
let (calls, remaining) = extract_text_tool_calls(text);
assert!(calls.is_empty(), "prose `call_id` must not be extracted");
assert_eq!(remaining, text, "prose must pass through unchanged");
}
#[test]
fn dict_by_call_id_with_function_envelope_form() {
let text = r#"{"call_xyz": {"type":"function","function":{"name":"bash","arguments":"{\"command\":\"pwd\"}"}}}"#;
let (calls, _) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "pwd");
}
#[test]
fn extract_bare_command_args_single() {
let text = r#"{"command": "cd ~/srv/rs/opencrabs && cat src/rtk/tracker.rs | head -150"}"#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1, "must synthesize a bash call");
assert_eq!(calls[0].0, "bash");
assert_eq!(
calls[0].1["command"],
"cd ~/srv/rs/opencrabs && cat src/rtk/tracker.rs | head -150"
);
assert!(
cleaned.trim().is_empty(),
"bare-args JSON must be stripped from visible text, got: {cleaned:?}"
);
}
#[test]
fn extract_bare_command_args_multiple_blobs_user_screenshot() {
let text = r#"{"command": "cd ~/srv/rs/opencrabs && cat src/rtk/tracker.rs | head -150"}
{"command": "cd ~/srv/rs/opencrabs && cat src/rtk/mod.rs | head -200"}
{"command": "cd ~/srv/rs/opencrabs && grep -r \"rtk::rewrite\\|rtk_rewrite\\|Rtk Result\" src/brain/tools/ --include=\"*.rs\" | head -20"}
{"command": "cd ~/srv/rs/opencrabs && git diff v0.3.24..v0.3.25 -- src/brain/tools/bash.rs | head -100"}
{"command": "cd ~/srv/rs/opencrabs && cat src/brain/tools/bash.rs | grep -A 30 -B 5 'rtk\\|spawn_blocking\\|block_on' | head -80"}"#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 5, "all five blobs must become tool calls");
for call in &calls {
assert_eq!(call.0, "bash");
assert!(
call.1["command"]
.as_str()
.unwrap()
.starts_with("cd ~/srv/rs/opencrabs"),
"command must round-trip exactly: {call:?}"
);
}
assert!(
cleaned.trim().is_empty(),
"all bare-args blobs must be stripped, leftover: {cleaned:?}"
);
}
#[test]
fn extract_bare_command_args_with_optional_keys() {
let text = r#"{"command": "cargo build", "timeout_secs": 600}"#;
let (calls, _) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "cargo build");
assert_eq!(calls[0].1["timeout_secs"], 600);
}
#[test]
fn bare_command_args_ignored_when_extra_unknown_keys() {
let text = r#"{"command": "ls", "note": "example", "id": "x"}"#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert!(
calls.is_empty(),
"object with non-bash keys must not synthesize"
);
assert!(
cleaned.contains("ls"),
"non-tool prose must pass through, got: {cleaned:?}"
);
}
#[test]
fn bare_command_args_ignored_when_command_not_string() {
let text = r#"{"command": 42}"#;
let (calls, _) = extract_text_tool_calls(text);
assert!(calls.is_empty(), "non-string command must not synthesize");
}
#[test]
fn bare_command_args_alongside_prose_strips_only_json() {
let text = "Running the build now: {\"command\": \"cargo build\"} please wait.";
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "cargo build");
assert!(
cleaned.contains("Running the build") && cleaned.contains("please wait"),
"surrounding prose must remain, got: {cleaned:?}"
);
}
#[test]
fn invoke_style_single_clean_with_qwen_wrapper() {
let text = r#"<qwen:tool_call>
<invoke name="read_file">
<parameter name="path">/tmp/x.dart</parameter>
<parameter name="start_line">10</parameter>
<parameter name="line_count">50</parameter>
</invoke>
</qwen:tool_call>"#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "read_file");
assert_eq!(calls[0].1["path"], "/tmp/x.dart");
assert_eq!(calls[0].1["start_line"], 10);
assert_eq!(calls[0].1["line_count"], 50);
assert!(
cleaned.trim().is_empty(),
"wrapper + invoke must be stripped, leftover: {cleaned:?}"
);
}
#[test]
fn invoke_style_user_screenshot_2026_05_27_malformed() {
let text = r#"<qwen:tool_call>
invoke name="read_file">
<parameter name="path">~/srv/dart/heyiolo/lib/presentation/buyer_intent_screen/widgets/iolo_chat_widget.dart</parameter>
<parameter name="start_line">1888</parameter>
<parameter name="line_count">50</parameter>
</invoke>
<parameter name="read_file">
<parameter name="path">~/srv/dart/heyiolo/lib/presentation/propositions_screen/propositions_screen.dart</parameter>
<parameter name="start_line">360</parameter>
<parameter name="line_count">30</parameter>
</invoke>
</qwen:tool_call>"#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(
calls.len(),
2,
"both invokes (missing-< and parameter-not-invoke) must recover, got {} calls: {:?}",
calls.len(),
calls
);
assert_eq!(calls[0].0, "read_file");
assert!(
calls[0].1["path"]
.as_str()
.unwrap()
.ends_with("iolo_chat_widget.dart"),
);
assert_eq!(calls[0].1["start_line"], 1888);
assert_eq!(calls[0].1["line_count"], 50);
assert_eq!(calls[1].0, "read_file");
assert!(
calls[1].1["path"]
.as_str()
.unwrap()
.ends_with("propositions_screen.dart"),
);
assert_eq!(calls[1].1["start_line"], 360);
assert_eq!(calls[1].1["line_count"], 30);
assert!(
!cleaned.contains("qwen:tool_call"),
"qwen wrapper must be gone from rendered content, got: {cleaned:?}"
);
assert!(
!cleaned.contains("<invoke") && !cleaned.contains("invoke name="),
"invoke blocks must be gone, got: {cleaned:?}"
);
}
#[test]
fn invoke_style_anthropic_function_calls_wrapper() {
let text = r#"<function_calls>
<invoke name="bash">
<parameter name="command">ls -la</parameter>
</invoke>
</function_calls>"#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "ls -la");
assert!(
!cleaned.contains("function_calls"),
"function_calls wrapper must be stripped, got: {cleaned:?}"
);
}
#[test]
fn invoke_style_standalone_no_wrapper() {
let text = r#"<invoke name="bash">
<parameter name="command">pwd</parameter>
</invoke>"#;
let (calls, _cleaned) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "pwd");
}
#[test]
fn invoke_style_unknown_tool_name_ignored() {
let text = r#"This is just prose with <invoke name="not_a_real_tool"> mentioned."#;
let (calls, cleaned) = extract_text_tool_calls(text);
assert!(
calls.is_empty(),
"unknown tool name must not dispatch a call"
);
assert!(cleaned.contains("just prose"));
}
#[test]
fn invoke_style_value_type_coercion() {
let text = r#"<invoke name="read_file">
<parameter name="path">/tmp/x</parameter>
<parameter name="start_line">42</parameter>
<parameter name="hashline">true</parameter>
</invoke>"#;
let (calls, _) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].1["path"], "/tmp/x");
assert_eq!(
calls[0].1["start_line"], 42,
"numeric string must coerce to integer"
);
assert_eq!(
calls[0].1["hashline"], true,
"boolean string must coerce to bool"
);
}
#[test]
fn invoke_style_single_quoted_name_attribute() {
let text = r#"<invoke name='bash'>
<parameter name='command'>echo hi</parameter>
</invoke>"#;
let (calls, _) = extract_text_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, "bash");
assert_eq!(calls[0].1["command"], "echo hi");
}