use super::parse_text_tool_calls_with_tools;
use super::sample_tool_registry;
use crate::llm::tool_delimiter::wire_to_canonical;
const MULTIBLOCK: &str = include_str!("../../testdata/qwen36_multiblock_response.txt");
const EDIT_BLOCK: &str = include_str!("../../testdata/qwen36_reserved_token_response.txt");
#[test]
fn wire_form_silently_loses_calls_canonical_does_not() {
let wire = parse_text_tool_calls_with_tools(MULTIBLOCK, None);
assert_eq!(
wire.calls.len(),
0,
"wire form yields zero calls (the field bug)"
);
assert!(
wire.errors.is_empty(),
"wire form yields zero per-call diagnostics too — the calls are silently lost: {:?}",
wire.errors
);
let canon = parse_text_tool_calls_with_tools(&wire_to_canonical(MULTIBLOCK), None);
assert!(
!canon.calls.is_empty() || !canon.errors.is_empty(),
"canonicalized text must be visible to the parser (calls or diagnostics), \
not silently dropped: calls={:?} errors={:?}",
canon.calls,
canon.errors
);
}
#[test]
fn registered_tools_dispatch_only_after_canonicalization_for_tagged_blocks() {
let tools = sample_tool_registry();
let saw_edit_call = |calls: &[serde_json::Value]| {
calls.iter().any(|c| {
c.get("name").and_then(|v| v.as_str()) == Some("edit")
|| c.get("tool").and_then(|v| v.as_str()) == Some("edit")
})
};
let wire = parse_text_tool_calls_with_tools(EDIT_BLOCK, Some(&tools));
assert!(
saw_edit_call(&wire.calls),
"wire form: heredoc recovery must surface the edit (no silent loss): \
calls={:?} errors={:?}",
wire.calls,
wire.errors
);
let canon = parse_text_tool_calls_with_tools(&wire_to_canonical(EDIT_BLOCK), Some(&tools));
assert!(
saw_edit_call(&canon.calls) || !canon.errors.is_empty(),
"after remap the edit block must be visible (dispatched or diagnosed): \
calls={:?} errors={:?}",
canon.calls,
canon.errors
);
}
#[test]
fn streaming_and_non_streaming_remap_parse_identically() {
let tools = sample_tool_registry();
for fixture in [MULTIBLOCK, EDIT_BLOCK] {
let non_streaming_text = wire_to_canonical(fixture);
let non_streaming = parse_text_tool_calls_with_tools(&non_streaming_text, Some(&tools));
let mut assembled = String::new();
let bytes = fixture.as_bytes();
let mut i = 0;
while i < bytes.len() {
let mut end = (i + 3).min(bytes.len());
while end < bytes.len() && !fixture.is_char_boundary(end) {
end += 1;
}
assembled.push_str(&fixture[i..end]);
i = end;
}
let streaming_text = wire_to_canonical(&assembled);
let streaming = parse_text_tool_calls_with_tools(&streaming_text, Some(&tools));
assert_eq!(
non_streaming_text, streaming_text,
"assembled streaming text must equal the non-streaming text"
);
assert_eq!(
non_streaming.calls, streaming.calls,
"streaming and non-streaming paths must parse identical tool calls"
);
assert_eq!(
non_streaming.errors, streaming.errors,
"streaming and non-streaming paths must produce identical diagnostics"
);
}
}