use super::*;
fn dispatched_count(
calls: &[(Option<&str>, Option<&str>, &serde_json::Value)],
require_call_id: bool,
) -> usize {
match validate_tool_call_batch(calls, require_call_id) {
Ok(validated) => validated.len(), Err(_) => 0, }
}
#[test]
fn batch_valid_then_malformed_dispatches_zero() {
let a = serde_json::json!("{\"op\":\"status\"}");
let bad = serde_json::json!("{\"op\": "); let calls = [
(Some("id1"), Some("git"), &a),
(Some("id2"), Some("write_file"), &bad),
];
assert_eq!(
dispatched_count(&calls, true),
0,
"a malformed sibling rejects the whole batch — the valid mutating call must NOT run first"
);
}
#[test]
fn batch_malformed_then_valid_dispatches_zero() {
let bad = serde_json::json!("not json");
let b = serde_json::json!("{}");
let calls = [
(Some("id1"), Some("git"), &bad),
(Some("id2"), Some("list_dir"), &b),
];
assert_eq!(dispatched_count(&calls, true), 0);
}
#[test]
fn batch_missing_call_id_dispatches_zero_when_required() {
let a = serde_json::json!("{}");
let calls = [(None, Some("git"), &a)];
assert_eq!(dispatched_count(&calls, true), 0);
assert_eq!(dispatched_count(&calls, false), 1);
}
#[test]
fn batch_duplicate_call_ids_dispatch_zero() {
let a = serde_json::json!("{}");
let calls = [
(Some("dup"), Some("git"), &a),
(Some("dup"), Some("list_dir"), &a),
];
assert_eq!(
dispatched_count(&calls, true),
0,
"duplicate ids mis-correlate results — reject the batch"
);
}
#[test]
fn batch_malformed_argument_json_dispatches_zero() {
let bad = serde_json::json!("{\"path\": \"a"); let calls = [(Some("id1"), Some("write_file"), &bad)];
assert_eq!(dispatched_count(&calls, true), 0);
}
#[test]
fn batch_all_valid_dispatches_every_call() {
let a = serde_json::json!("{\"op\":\"status\"}");
let b = serde_json::json!(serde_json::json!({"path": "x"})); let c = serde_json::Value::Null; let calls = [
(Some("id1"), Some("git"), &a),
(Some("id2"), Some("write_file"), &b),
(Some("id3"), Some("list_dir"), &c),
];
let out = validate_tool_call_batch(&calls, true).expect("all valid");
assert_eq!(out.len(), 3);
assert_eq!(
out.iter().map(|v| v.name.as_str()).collect::<Vec<_>>(),
vec!["git", "write_file", "list_dir"]
);
assert_eq!(out[0].call_id, "id1");
}
#[test]
fn batch_missing_id_is_correlation_impossible() {
let a = serde_json::json!("{}");
let calls = [(None, Some("git"), &a)];
assert!(matches!(
validate_tool_call_batch(&calls, true),
Err(BatchRejection::CorrelationImpossible(_))
));
}
#[test]
fn batch_blank_id_is_correlation_impossible() {
let a = serde_json::json!("{}");
let calls = [(Some(" "), Some("git"), &a)];
assert!(matches!(
validate_tool_call_batch(&calls, true),
Err(BatchRejection::CorrelationImpossible(_))
));
}
#[test]
fn batch_duplicate_id_is_correlation_impossible() {
let a = serde_json::json!("{}");
let calls = [
(Some("dup"), Some("git"), &a),
(Some("dup"), Some("list_dir"), &a),
];
assert!(matches!(
validate_tool_call_batch(&calls, true),
Err(BatchRejection::CorrelationImpossible(_))
));
}
#[test]
fn batch_bad_args_with_valid_ids_is_content_invalid() {
let bad = serde_json::json!("not json");
let calls = [(Some("id1"), Some("git"), &bad)];
assert!(matches!(
validate_tool_call_batch(&calls, true),
Err(BatchRejection::ContentInvalid(_))
));
}
#[test]
fn validate_accepts_a_string_encoded_object() {
let (name, args) = validate_tool_call(
Some("write_file"),
&serde_json::json!("{\"path\":\"a.txt\"}"),
)
.expect("valid");
assert_eq!(name, "write_file");
assert_eq!(args["path"], "a.txt");
}
#[test]
fn validate_accepts_an_object_value_directly() {
let (name, args) =
validate_tool_call(Some("git"), &serde_json::json!({"op": "status"})).expect("valid");
assert_eq!(name, "git");
assert_eq!(args["op"], "status");
}
#[test]
fn validate_treats_absent_or_empty_arguments_as_no_args() {
for raw in [
serde_json::Value::Null,
serde_json::json!(""),
serde_json::json!(" "),
] {
let (_, args) = validate_tool_call(Some("list_dir"), &raw).expect("valid no-args");
assert_eq!(args, serde_json::json!({}), "raw={raw:?}");
}
}
#[test]
fn validate_rejects_unparseable_arguments_instead_of_coercing_to_null() {
let err = validate_tool_call(Some("write_file"), &serde_json::json!("{\"path\": \"a"))
.expect_err("truncated JSON must be rejected");
assert!(err.contains("not valid JSON"), "got: {err}");
assert!(err.contains("write_file"), "names the tool: {err}");
}
#[test]
fn validate_rejects_non_object_json_arguments() {
for raw in [serde_json::json!("[1,2,3]"), serde_json::json!("\"bare\"")] {
let err =
validate_tool_call(Some("git"), &raw).expect_err("non-object args must be rejected");
assert!(
err.contains("must be a JSON object"),
"raw={raw:?} got: {err}"
);
}
assert!(validate_tool_call(Some("git"), &serde_json::json!(42)).is_err());
}
#[test]
fn validate_rejects_a_missing_or_blank_name() {
assert!(validate_tool_call(None, &serde_json::json!({})).is_err());
assert!(validate_tool_call(Some(""), &serde_json::json!({})).is_err());
assert!(validate_tool_call(Some(" "), &serde_json::json!({})).is_err());
}
#[test]
fn malformed_calls_are_never_dispatched_invocation_count_is_zero() {
let batch = vec![
serde_json::json!({"name": "git", "arguments": "{\"op\":\"status\"}"}),
serde_json::json!({"name": "write_file", "arguments": "{\"path\": \"a"}),
serde_json::json!({"arguments": "{}"}),
serde_json::json!({"name": "list_dir"}),
serde_json::json!({"name": "git", "arguments": "[1,2]"}),
];
let mut invocations = 0usize;
let mut dispatched_names = Vec::new();
for call in &batch {
match validate_tool_call(call["name"].as_str(), &call["arguments"]) {
Ok((name, _args)) => {
invocations += 1;
dispatched_names.push(name);
}
Err(_reason) => {
}
}
}
assert_eq!(
invocations, 2,
"exactly the two well-formed calls dispatch; the three malformed ones invoke nothing"
);
assert_eq!(dispatched_names, vec!["git", "list_dir"]);
}
#[test]
fn withdraw_tool_calls_only_touches_an_assistant_turn() {
let calls = serde_json::json!([{"type": "function"}]);
let mut assistant =
serde_json::json!({"role": "assistant", "content": null, "tool_calls": calls});
withdraw_tool_calls(&mut assistant);
assert!(assistant.get("tool_calls").is_none());
assert!(
assistant["content"].is_string(),
"no null assistant content"
);
let mut other = serde_json::json!({"role": "user", "content": "hi", "tool_calls": calls});
let before = other.clone();
withdraw_tool_calls(&mut other);
assert_eq!(other, before, "a non-assistant message is left alone");
}
#[test]
fn withdraw_gives_blank_string_content_the_placeholder_too() {
for blank in [serde_json::json!(""), serde_json::json!(" \n")] {
let mut turn = serde_json::json!({"role": "assistant", "content": blank, "tool_calls": []});
withdraw_tool_calls(&mut turn);
assert!(
turn["content"]
.as_str()
.is_some_and(|c| !c.trim().is_empty()),
"{turn}"
);
}
let mut turn =
serde_json::json!({"role": "assistant", "content": "Reading.", "tool_calls": []});
withdraw_tool_calls(&mut turn);
assert_eq!(turn["content"], "Reading.", "real text is kept");
}
#[test]
fn the_re_ask_names_the_validators_reason() {
let mut strikes = 0;
let text = reask_uncorrelatable(
&mut strikes,
"tool call #2 repeats call id \"dup\" — ambiguous result routing",
None,
None,
)
.unwrap();
assert!(text.contains("repeats call id"), "{text}");
assert!(
!text.contains("without call ids"),
"no false claim for duplicates: {text}"
);
}