use super::*;
use crate::api::types::{FunctionDefinition, MessageContent};
fn dummy_tool_def(name: &str) -> ToolDefinition {
ToolDefinition {
def_type: "function".to_string(),
function: FunctionDefinition {
name: name.to_string(),
description: format!("Test tool {}", name),
parameters: serde_json::json!({"type": "object", "properties": {}}),
},
}
}
fn assistant_msg_with_native_calls(calls: Vec<ToolCall>) -> Message {
Message {
role: "assistant".to_string(),
content: MessageContent::Text(String::new()),
reasoning_content: None,
tool_calls: Some(calls),
tool_call_id: None,
name: None,
}
}
fn assistant_msg_with_text(text: &str) -> Message {
Message {
role: "assistant".to_string(),
content: MessageContent::Text(text.to_string()),
reasoning_content: None,
tool_calls: None,
tool_call_id: None,
name: None,
}
}
#[test]
fn attach_tools_native_sets_tool_choice() {
let mut body = serde_json::json!({"model": "x"});
let tools = Some(vec![dummy_tool_def("foo")]);
attach_tools(&mut body, &tools, true);
assert!(body["tools"].is_array());
assert_eq!(body["tool_choice"], "auto");
}
#[test]
fn attach_tools_text_mode_omits_tool_choice() {
let mut body = serde_json::json!({"model": "x"});
let tools = Some(vec![dummy_tool_def("foo")]);
attach_tools(&mut body, &tools, false);
assert!(body["tools"].is_array());
assert!(
body.get("tool_choice").is_none(),
"tool_choice must NOT be sent when native FC is off"
);
}
#[test]
fn attach_tools_none_is_noop() {
let mut body = serde_json::json!({"model": "x"});
attach_tools(&mut body, &None, true);
assert!(body.get("tools").is_none());
assert!(body.get("tool_choice").is_none());
}
#[test]
fn native_fc_on_with_native_field_returns_native() {
let native = ToolCall {
id: "call_abc".to_string(),
call_type: "function".to_string(),
function: ToolFunction {
name: "shell_exec".to_string(),
arguments: r#"{"command":"ls"}"#.to_string(),
},
};
let msg = assistant_msg_with_native_calls(vec![native.clone()]);
let calls = extract_tool_calls(&msg, true);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].id, "call_abc");
assert_eq!(calls[0].function.name, "shell_exec");
}
#[test]
fn native_fc_on_with_text_falls_back_to_parser() {
let text = r#"I'll list the directory.
<tool>
<name>shell_exec</name>
<arguments>{"command": "ls -la"}</arguments>
</tool>"#;
let msg = assistant_msg_with_text(text);
let calls = extract_tool_calls(&msg, true);
assert_eq!(
calls.len(),
1,
"text-formatted tool calls must be extracted even with native FC enabled"
);
assert_eq!(calls[0].function.name, "shell_exec");
assert!(calls[0].id.starts_with("parsed_"));
let parsed: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(parsed["command"], "ls -la");
}
#[test]
fn native_fc_off_with_text_parses_xml() {
let text = r#"<tool>
<name>file_read</name>
<arguments>{"path": "src/main.rs"}</arguments>
</tool>"#;
let msg = assistant_msg_with_text(text);
let calls = extract_tool_calls(&msg, false);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "file_read");
}
#[test]
fn no_tool_calls_returns_empty() {
let msg = assistant_msg_with_text("just a plain answer with no tools");
let calls = extract_tool_calls(&msg, true);
assert!(calls.is_empty());
let calls = extract_tool_calls(&msg, false);
assert!(calls.is_empty());
}
#[test]
fn empty_native_field_falls_back() {
let mut msg = assistant_msg_with_text(
r#"<tool><name>file_read</name><arguments>{"path":"x"}</arguments></tool>"#,
);
msg.tool_calls = Some(vec![]);
let calls = extract_tool_calls(&msg, true);
assert_eq!(
calls.len(),
1,
"empty tool_calls:[] must trigger text fallback"
);
assert_eq!(calls[0].function.name, "file_read");
}
#[test]
fn multiple_text_tool_calls_all_extracted() {
let text = r#"<tool>
<name>file_read</name>
<arguments>{"path": "a"}</arguments>
</tool>
<tool>
<name>file_read</name>
<arguments>{"path": "b"}</arguments>
</tool>"#;
let msg = assistant_msg_with_text(text);
let calls = extract_tool_calls(&msg, true);
assert_eq!(calls.len(), 2);
assert_ne!(
calls[0].id, calls[1].id,
"synthetic ids must be unique even for repeated tool names"
);
}
#[test]
fn text_helper_matches_message_extraction() {
let text = r#"<tool>
<name>shell_exec</name>
<arguments>{"command": "echo hi"}</arguments>
</tool>"#;
let msg = assistant_msg_with_text(text);
let from_msg = extract_tool_calls(&msg, true);
let from_text = extract_tool_calls_from_text(text);
assert_eq!(from_msg.len(), from_text.len());
assert_eq!(from_msg[0].function.name, from_text[0].function.name);
assert_eq!(
from_msg[0].function.arguments,
from_text[0].function.arguments
);
}
#[test]
fn chat_and_chat_with_profile_extract_identically() {
let text = r#"<tool>
<name>file_read</name>
<arguments>{"path": "main.rs"}</arguments>
</tool>"#;
let msg = assistant_msg_with_text(text);
let via_main = extract_tool_calls(&msg, true);
let via_profile = extract_tool_calls(&msg, true);
assert_eq!(via_main.len(), via_profile.len());
assert_eq!(
via_main[0].function.name, via_profile[0].function.name,
"main and profile paths must extract identically"
);
}
#[test]
fn swl_runtime_parses_same_as_main_agent() {
let text = r#"<tool>
<name>shell_exec</name>
<arguments>{"command": "echo swl"}</arguments>
</tool>"#;
let mut msg = assistant_msg_with_text(text);
msg.tool_calls = Some(vec![]);
let main = extract_tool_calls(&msg, true);
let swl = extract_tool_calls(&msg, true);
assert_eq!(main.len(), 1);
assert_eq!(swl.len(), 1);
assert_eq!(main[0].function.name, swl[0].function.name);
assert_eq!(main[0].function.arguments, swl[0].function.arguments);
}