", "HTML tags")]
#[case("a & b", "a & b", "ampersand")]
#[case(""quoted"", "\"quoted\"", "quotes")]
fn test_html_unescape(#[case] input: &str, #[case] expected: &str, #[case] _description: &str) {
assert_eq!(html_unescape(input), expected);
}
#[test]
fn test_parse_simple_tool_call() {
let input = r#"
pwd && ls
"#;
let (calls, normal) =
try_tool_call_parse_xml(input, &XmlParserConfig::default(), None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "execute_bash");
assert_eq!(normal, Some("".to_string()));
let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(args["command"], "pwd && ls");
}
#[test]
fn test_parse_multiple_parameters() {
let input = r#"
San Francisco
CA
fahrenheit
"#;
let (calls, _) = try_tool_call_parse_xml(input, &XmlParserConfig::default(), None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "get_weather");
let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(args["city"], "San Francisco");
assert_eq!(args["state"], "CA");
assert_eq!(args["unit"], "fahrenheit");
}
#[test]
fn test_parse_with_normal_text() {
let input = r#"I'll help you with that.
Dallas
Let me check that for you."#;
let (calls, normal) =
try_tool_call_parse_xml(input, &XmlParserConfig::default(), None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "get_weather");
assert_eq!(
normal,
Some("I'll help you with that. Let me check that for you.".to_string())
);
}
#[test]
fn test_parse_multiple_tool_calls() {
let input = r#"
Dallas
Orlando
"#;
let (calls, _) = try_tool_call_parse_xml(input, &XmlParserConfig::default(), None).unwrap();
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].function.name, "get_weather");
assert_eq!(calls[1].function.name, "get_weather");
let args0: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
let args1: serde_json::Value = serde_json::from_str(&calls[1].function.arguments).unwrap();
assert_eq!(args0["city"], "Dallas");
assert_eq!(args1["city"], "Orlando");
}
#[test]
fn test_parse_json_parameter_value() {
// With schema-aware parsing, we need to provide a schema to parse JSON objects
let tools = vec![ToolDefinition {
name: "process_data".to_string(),
parameters: Some(serde_json::json!({
"type": "object",
"properties": {
"config": {"type": "object"}
}
})),
}];
let input = r#"
{"setting": "value", "count": 42}
"#;
let (calls, _) =
try_tool_call_parse_xml(input, &XmlParserConfig::default(), Some(&tools)).unwrap();
assert_eq!(calls.len(), 1);
let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
assert!(args["config"].is_object());
assert_eq!(args["config"]["setting"], "value");
assert_eq!(args["config"]["count"], 42);
}
#[test]
fn test_parse_no_tool_calls() {
let input = "This is just normal text without any tool calls.";
let (calls, normal) =
try_tool_call_parse_xml(input, &XmlParserConfig::default(), None).unwrap();
assert_eq!(calls.len(), 0);
assert_eq!(normal, Some(input.to_string()));
}
#[test]
fn test_parse_malformed_tool_call() {
let input = r#"
value
"#;
// Should handle gracefully - might parse or return empty
let result = try_tool_call_parse_xml(input, &XmlParserConfig::default(), None);
assert!(result.is_ok());
}
#[test]
fn test_parse_missing_parameter_closing_tag() {
let input = r#"
ls -la
"#;
let (calls, _) = try_tool_call_parse_xml(input, &XmlParserConfig::default(), None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "execute_bash");
let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(args["command"], "ls -la");
}
#[test]
fn test_parse_missing_function_closing_tag() {
let input = r#"
Boston
"#;
let (calls, _) = try_tool_call_parse_xml(input, &XmlParserConfig::default(), None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "get_weather");
let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
assert_eq!(args["city"], "Boston");
}
#[test]
fn test_parse_missing_both_closing_tags() {
let input = r#"
SELECT * FROM users
"#;
let (calls, _) = try_tool_call_parse_xml(input, &XmlParserConfig::default(), None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "run_query");
let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
// This matches the original SGLang python implementation.
assert_eq!(args["sql"], "SELECT * FROM users\n");
}
#[test]
fn test_parse_multiple_parameters_missing_closing_tags() {
let input = r#"
rust programming
10
"#;
let (calls, _) = try_tool_call_parse_xml(input, &XmlParserConfig::default(), None).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "search");
let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
// This matches the original SGLang python implementation.
assert_eq!(args["query"], "rust programming\n
\n10");
}
#[test]
fn test_schema_aware_type_conversion() {
// This test matches the Python test_parse_streaming_increment_multiple_parameters
// from the diff, showing schema-aware type conversion
let tools = vec![ToolDefinition {
name: "multi_param_func".to_string(),
parameters: Some(serde_json::json!({
"type": "object",
"properties": {
"param1": {"type": "string"},
"param2": {"type": "float"},
"param3": {"type": "integer"},
"param4": {"type": "boolean"},
"param5": {"type": "object"},
"param6": {"type": "array"},
"param7": {"type": "null"},
"param8": {"type": "other_type"}
},
"required": ["param1", "param2", "param3", "param4", "param5", "param6", "param7", "param8"]
})),
}];
let input = r#"
42
41.9
42
true
{"key": "value"}
[1, 2, 3]
null
{'arg1': 3, 'arg2': [1, 2]}
"#;
let (calls, _) =
try_tool_call_parse_xml(input, &XmlParserConfig::default(), Some(&tools)).unwrap();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].function.name, "multi_param_func");
let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
// param1 is type "string" so "42" stays as string
assert_eq!(args["param1"], "42");
// param2 is type "float" so 41.9 is parsed as float
assert_eq!(args["param2"], 41.9);
// param3 is type "integer" so 42 is parsed as integer
assert_eq!(args["param3"], 42);
// param4 is type "boolean" so "true" is parsed as bool
assert_eq!(args["param4"], true);
// param5 is type "object" so JSON is parsed
assert_eq!(args["param5"], serde_json::json!({"key": "value"}));
// param6 is type "array" so JSON array is parsed
assert_eq!(args["param6"], serde_json::json!([1, 2, 3]));
// param7 is type "null" so "null" is parsed as null
assert_eq!(args["param7"], serde_json::Value::Null);
// param8 is other_type, uses literal_eval which converts Python-style dict
assert_eq!(
args["param8"],
serde_json::json!({"arg1": 3, "arg2": [1, 2]})
);
}
#[test]
fn test_schema_aware_type_conversion_fallback() {
// Test that invalid values fall back to strings with warnings
let tools = vec![ToolDefinition {
name: "test_func".to_string(),
parameters: Some(serde_json::json!({
"type": "object",
"properties": {
"int_param": {"type": "integer"},
"float_param": {"type": "float"},
"bool_param": {"type": "boolean"}
}
})),
}];
let input = r#"
not_an_int
not_a_float
not_a_bool
"#;
let (calls, _) =
try_tool_call_parse_xml(input, &XmlParserConfig::default(), Some(&tools)).unwrap();
assert_eq!(calls.len(), 1);
let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
// All should fall back to strings
assert_eq!(args["int_param"], "not_an_int");
assert_eq!(args["float_param"], "not_a_float");
// bool_param with invalid value defaults to false
assert_eq!(args["bool_param"], false);
}
#[test]
fn test_no_schema_fallback_behavior() {
// Without schema, behavior should match old safe_parse_value logic
let input = r#"
42
true
hello
"#;
let (calls, _) = try_tool_call_parse_xml(input, &XmlParserConfig::default(), None).unwrap();
assert_eq!(calls.len(), 1);
let args: serde_json::Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
// Without schema, all values are returned as strings (no type inference)
assert_eq!(args["param1"], "42");
assert_eq!(args["param2"], "true");
assert_eq!(args["param3"], "hello");
}
}