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_and_canonical_form_do_not_silently_lose_calls() {
let wire = parse_text_tool_calls_with_tools(MULTIBLOCK, None);
assert_eq!(
wire.calls.len(),
0,
"raw reserved-token wire form should not dispatch before canonicalization"
);
assert!(
!wire.errors.is_empty() || !wire.violations.is_empty(),
"raw wire form must surface a repair signal, not silently lose calls: \
errors={:?} violations={:?}",
wire.errors,
wire.violations
);
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"
);
}
}
const MALFORMED_OPENER: &str = include_str!("../../testdata/qwen_malformed_call_opener.txt");
#[test]
fn malformed_call_opener_recovers_complete_call() {
let tools = sample_tool_registry();
let result =
parse_text_tool_calls_with_tools(&wire_to_canonical(MALFORMED_OPENER), Some(&tools));
assert_eq!(
result.calls.len(),
1,
"malformed opener with a complete call must recover it; errors: {:?}",
result.errors
);
assert_eq!(result.calls[0]["name"], "run");
assert_eq!(
result.calls[0]["arguments"]["command"],
"cargo test -p core"
);
assert!(
result.errors.is_empty(),
"a recovered call must not also error: {:?}",
result.errors
);
assert!(
!result.prose.contains("[[CALL"),
"malformed opener must never reach visible assistant prose: {:?}",
result.prose
);
assert!(
!result.canonical.contains("[[CALL"),
"malformed opener must never reach canonical replay: {}",
result.canonical
);
}
#[test]
fn malformed_call_opener_truncated_emits_typed_error_not_prose() {
let tools = sample_tool_registry();
let wire = "[[CALL]\nedit({ action: \"create\", path: \"a.rs\"";
let result = parse_text_tool_calls_with_tools(&wire_to_canonical(wire), Some(&tools));
assert!(
result.calls.is_empty(),
"a truncated opener must not dispatch a call: {:?}",
result.calls
);
assert!(
result.errors.iter().any(|e| e.contains("TRUNCATED")),
"a truncated opener must emit a typed TRUNCATED diagnostic: {:?}",
result.errors
);
assert!(
!result.prose.contains("[[CALL") && !result.prose.contains("edit({"),
"the malformed stub and its partial body must not render as prose: {:?}",
result.prose
);
}
#[test]
fn literal_bracket_marker_inside_tool_argument_is_byte_identical() {
let tools = sample_tool_registry();
let text = "<tool_call>\nedit({ action: \"create\", path: \"a.txt\", \
content: \"[[CALL] literal\" })\n</tool_call>";
let result = parse_text_tool_calls_with_tools(text, Some(&tools));
assert_eq!(result.calls.len(), 1, "errors: {:?}", result.errors);
assert_eq!(
result.calls[0]["arguments"]["content"], "[[CALL] literal",
"a literal marker inside an argument must be byte-identical"
);
assert!(
result.errors.is_empty(),
"a byte-identical argument must not error: {:?}",
result.errors
);
}
#[test]
fn unknown_bracket_text_caller_stays_prose() {
let tools = sample_tool_registry();
let result = parse_text_tool_calls_with_tools(
&wire_to_canonical("[[CALLER] is not a call"),
Some(&tools),
);
assert!(
result.calls.is_empty(),
"unknown bracket text is not a call: {:?}",
result.calls
);
assert!(
result.errors.is_empty(),
"unknown bracket text is not an error: {:?}",
result.errors
);
assert!(
result.prose.contains("[[CALLER]"),
"unknown bracket text must stay in prose byte-identical: {:?}",
result.prose
);
}
#[test]
fn literal_bracket_marker_in_closed_fence_stays_prose() {
let tools = sample_tool_registry();
let text = "```\n[[CALL]\n```";
let result = parse_text_tool_calls_with_tools(&wire_to_canonical(text), Some(&tools));
assert!(
result.calls.is_empty(),
"a fenced marker is not a call: {:?}",
result.calls
);
assert!(
result.prose.contains("[[CALL]"),
"a fenced marker must stay byte-identical in prose: {:?}",
result.prose
);
}
#[test]
fn well_formed_wire_opener_still_recovers_after_canonicalization() {
let tools = sample_tool_registry();
let wire = "[[CALL]]\nrun({ command: \"cargo build\" })\n[[/CALL]]";
let result = parse_text_tool_calls_with_tools(&wire_to_canonical(wire), Some(&tools));
assert_eq!(result.calls.len(), 1, "errors: {:?}", result.errors);
assert_eq!(result.calls[0]["name"], "run");
assert!(result.errors.is_empty(), "{:?}", result.errors);
}