use super::*;
fn parse(text: &str) -> TextToolParseResult {
parse_fenced_json_tool_calls(text)
}
fn arg<'a>(call: &'a serde_json::Value, key: &str) -> Option<&'a serde_json::Value> {
call.get("arguments")?.get(key)
}
#[test]
fn parses_a_single_clean_call() {
let out = parse("```tool\n{\"name\": \"read_file\", \"args\": {\"path\": \"a.rs\"}}\n```");
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "read_file");
assert_eq!(arg(&out.calls[0], "path").unwrap(), "a.rs");
}
#[test]
fn parses_tool_arguments_dialect_aliases() {
let out = parse("```tool\n{\"tool\": \"read_file\", \"arguments\": {\"path\": \"a.rs\"}}\n```");
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "read_file");
assert_eq!(arg(&out.calls[0], "path").unwrap(), "a.rs");
}
#[test]
fn unwraps_generic_tool_wrapper_envelope() {
let out = parse(
"```tool\n{\"name\":\"tool\",\"args\":{\"name\":\"look\",\"args\":{\"intent\":\"read\",\"file\":\"src/lib.rs\"}}}\n```",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "look");
assert_eq!(arg(&out.calls[0], "intent").unwrap(), "read");
assert_eq!(arg(&out.calls[0], "file").unwrap(), "src/lib.rs");
}
#[test]
fn strips_harmony_channel_suffix_from_tool_name() {
let out = parse(
"```tool\n{\"name\":\"run<|channel|>commentary\",\"args\":{\"command\":\"cargo test\"}}\n```",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "run");
assert_eq!(arg(&out.calls[0], "command").unwrap(), "cargo test");
}
#[test]
fn canonical_keys_win_over_aliases() {
let out = parse(
"```tool\n{\"name\": \"canonical\", \"tool\": \"alias\", \"args\": {\"k\": 1}, \"arguments\": {\"k\": 2}}\n```",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "canonical");
assert_eq!(arg(&out.calls[0], "k").unwrap(), 1);
}
#[test]
fn content_with_backticks_heredoc_brace_and_tag_survives() {
let content = "```\nx := `raw`\n<<EOF\n}\n</tool>\n```";
let json_content = serde_json::to_string(content).unwrap();
let src = format!(
"```tool\n{{\"name\": \"write_file\", \"args\": {{\"path\": \"f.go\", \"content\": {json_content}}}}}\n```"
);
let out = parse(&src);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(arg(&out.calls[0], "content").unwrap(), content);
}
#[test]
fn multiple_fences_yield_multiple_calls() {
let src = "```tool\n{\"name\": \"a\", \"args\": {}}\n```\nsome prose\n```tool\n{\"name\": \"b\", \"args\": {\"k\": 1}}\n```";
let out = parse(src);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 2);
assert_eq!(out.calls[0]["name"], "a");
assert_eq!(out.calls[1]["name"], "b");
assert!(out.prose.contains("some prose"));
}
#[test]
fn content_starting_with_heredoc_opener_is_just_a_string() {
let content = "<<EOF\npackage main\n";
let json_content = serde_json::to_string(content).unwrap();
let src = format!(
"```tool\n{{\"name\": \"write_file\", \"args\": {{\"content\": {json_content}}}}}\n```"
);
let out = parse(&src);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(arg(&out.calls[0], "content").unwrap(), content);
}
#[test]
fn array_body_is_rejected_as_non_object() {
let out = parse("```tool\n[{\"name\": \"a\", \"args\": {}}]\n```");
assert!(out.calls.is_empty(), "calls: {:?}", out.calls);
assert_eq!(out.errors.len(), 1);
assert!(
out.errors[0].contains("non-object") || out.errors[0].contains("one or more JSON"),
"got: {}",
out.errors[0]
);
}
#[test]
fn trailing_bytes_after_object_salvages_the_object() {
let out = parse("```tool\n{\"name\": \"a\", \"args\": {}} trailing\n```");
assert_eq!(out.calls.len(), 1, "calls: {:?}", out.calls);
assert_eq!(out.calls[0]["name"], "a");
assert_eq!(out.errors.len(), 1, "errors: {:?}", out.errors);
}
#[test]
fn missing_name_rejected() {
let out = parse("```tool\n{\"args\": {\"path\": \"a\"}}\n```");
assert!(out.calls.is_empty());
assert_eq!(out.errors.len(), 1);
assert!(out.errors[0].contains("missing a non-empty string `name`"));
}
#[test]
fn empty_name_rejected() {
let out = parse("```tool\n{\"name\": \" \", \"args\": {}}\n```");
assert!(out.calls.is_empty());
assert!(out.errors[0].contains("`name`"));
}
#[test]
fn args_not_object_rejected() {
let out = parse("```tool\n{\"name\": \"a\", \"args\": \"oops\"}\n```");
assert!(out.calls.is_empty());
assert_eq!(out.errors.len(), 1);
assert!(out.errors[0].contains("must be a JSON object"));
}
#[test]
fn absent_args_is_empty_object() {
let out = parse("```tool\n{\"name\": \"list_dir\"}\n```");
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert!(out.calls[0]["arguments"].is_object());
assert_eq!(out.calls[0]["arguments"].as_object().unwrap().len(), 0);
}
#[test]
fn truncated_string_is_unterminated_not_half_applied() {
let out = parse("```tool\n{\"name\": \"write_file\", \"args\": {\"content\": \"half a str");
assert!(out.calls.is_empty(), "must not dispatch a truncated call");
assert_eq!(out.errors.len(), 1);
assert!(
out.errors[0].contains("Unterminated"),
"got: {}",
out.errors[0]
);
}
#[test]
fn complete_object_without_close_fence_is_accepted() {
let out = parse("```tool\n{\"name\": \"a\", \"args\": {\"k\": 1}}");
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "a");
}
#[test]
fn json_fence_accepts_with_protocol_violation() {
let out = parse("```json\n{\"name\": \"a\", \"args\": {}}\n```");
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "a");
assert!(
out.violations
.iter()
.any(|v| v.contains("protocol_violation")),
"violations: {:?}",
out.violations
);
}
#[test]
fn tool_like_fence_drift_accepts_with_protocol_violation() {
for (src, opener) in [
(
"```tool_code\n{\"name\": \"a\", \"args\": {\"k\": 1}}\n```",
"```tool_code",
),
(
"```tool python\n{\"name\": \"a\", \"args\": {\"k\": 1}}\n```",
"```tool python",
),
(
"```function_call\n{\"name\": \"a\", \"args\": {\"k\": 1}}\n```",
"```function_call",
),
(
"~~~tool\n{\"name\": \"a\", \"args\": {\"k\": 1}}\n~~~",
"~~~tool",
),
] {
let out = parse(src);
assert!(
out.errors.is_empty(),
"errors for {opener}: {:?}",
out.errors
);
assert_eq!(out.calls.len(), 1, "calls for {opener}: {:?}", out.calls);
assert_eq!(out.calls[0]["name"], "a");
assert_eq!(arg(&out.calls[0], "k").unwrap(), 1);
assert!(
out.violations.iter().any(|v| v.contains(opener)),
"violations for {opener}: {:?}",
out.violations
);
}
}
#[test]
fn invalid_tool_like_fence_drift_reports_error_and_violation() {
let out = parse("```tool_code\nnot json\n```");
assert!(out.calls.is_empty());
assert_eq!(out.errors.len(), 1);
assert!(out.errors[0].contains("not valid JSON"));
assert!(
out.violations.iter().any(|v| v.contains("```tool_code")),
"violations: {:?}",
out.violations
);
}
#[test]
fn bare_json_tool_call_accepts_with_protocol_violation() {
let out = parse("{\"name\": \"a\", \"args\": {\"k\": 1}}");
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert!(out.prose.is_empty(), "prose: {:?}", out.prose);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "a");
assert_eq!(arg(&out.calls[0], "k").unwrap(), 1);
assert!(
out.violations
.iter()
.any(|v| v.contains("bare JSON object")),
"violations: {:?}",
out.violations
);
}
#[test]
fn chat_template_envelope_recovers_multiple_inline_argument_calls() {
let out = parse(
"<tool_calls>\n<tool>\n{\"name\":\"look\",\"file\":\"src/writer.zig\"}\n{\"name\":\"look\",\"file\":\"src/parser.zig\"}\n</tool_calls>",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert!(out.prose.is_empty(), "prose: {:?}", out.prose);
assert_eq!(out.calls.len(), 2);
assert_eq!(out.calls[0]["name"], "look");
assert_eq!(
arg(&out.calls[0], "file"),
Some(&serde_json::json!("src/writer.zig"))
);
assert_eq!(out.calls[1]["name"], "look");
assert_eq!(
arg(&out.calls[1], "file"),
Some(&serde_json::json!("src/parser.zig"))
);
assert!(
out.violations
.iter()
.any(|violation| violation.contains("chat-template")),
"violations: {:?}",
out.violations
);
}
#[test]
fn chat_template_envelope_accepts_optional_per_call_close_markers() {
let out = parse(
"<tool_calls><tool>{\"name\":\"a\",\"args\":{\"k\":1}}</tool><tool>{\"tool\":\"b\",\"arguments\":{\"v\":2}}</tool></tool_calls>",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 2);
assert_eq!(out.calls[0]["name"], "a");
assert_eq!(arg(&out.calls[0], "k"), Some(&serde_json::json!(1)));
assert_eq!(out.calls[1]["name"], "b");
assert_eq!(arg(&out.calls[1], "v"), Some(&serde_json::json!(2)));
}
#[test]
fn malformed_chat_template_envelope_never_dispatches_partial_calls() {
let out = parse("<tool_calls>\n<tool>\n{\"name\":\"look\",\"file\":\"src/writer.zig\"");
assert!(
out.calls.is_empty(),
"partial calls must not dispatch: {:?}",
out.calls
);
assert_eq!(out.errors.len(), 1);
assert!(
out.errors[0].contains("<tool_calls>") && out.errors[0].contains("not executed"),
"error: {}",
out.errors[0]
);
}
#[test]
fn truncated_chat_template_second_marker_never_dispatches_first_call() {
let out =
parse("<tool_calls><tool>{\"name\":\"look\",\"file\":\"src/lib.rs\"}<tool></tool_calls>");
assert!(
out.calls.is_empty(),
"partial calls must not dispatch: {:?}",
out.calls
);
assert_eq!(out.errors.len(), 1);
assert!(
out.errors[0].contains("ended without a complete JSON object")
&& out.errors[0].contains("not executed"),
"error: {}",
out.errors[0]
);
}
#[test]
fn unmatched_chat_template_tool_close_is_rejected() {
let out = parse("<tool_calls></tool></tool_calls>");
assert!(out.calls.is_empty());
assert_eq!(out.errors.len(), 1);
assert!(out.errors[0].contains("unmatched `</tool>`"));
}
#[test]
fn chat_template_without_tool_marker_is_rejected() {
let out = parse("<tool_calls>{\"name\":\"look\",\"file\":\"src/writer.zig\"}</tool_calls>");
assert!(out.calls.is_empty());
assert_eq!(out.errors.len(), 1);
assert!(out.errors[0].contains("no `<tool>` marker"));
}
#[test]
fn legacy_tagged_markup_under_json_reports_protocol_violation() {
let out = parse("<tool_call>\na({})\n</tool_call>");
assert!(out.calls.is_empty());
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert!(
out.violations.iter().any(|v| v.contains("<tool_call>")),
"violations: {:?}",
out.violations
);
}
#[test]
fn unrelated_fence_stays_in_prose() {
let out = parse("```python\nprint('hi')\n```");
assert!(out.calls.is_empty());
assert!(out.errors.is_empty());
assert!(out.prose.contains("print('hi')"));
}
#[test]
fn unrelated_tilde_fence_stays_in_prose() {
let out = parse("~~~python\nprint('hi')\n~~~");
assert!(out.calls.is_empty());
assert!(out.errors.is_empty());
assert!(out.prose.contains("print('hi')"));
}
#[test]
fn embedded_backtick_fence_does_not_close_early() {
let content = "before\n```\nafter";
let json_content = serde_json::to_string(content).unwrap();
let src = format!("```tool\n{{\"name\": \"w\", \"args\": {{\"c\": {json_content}}}}}\n```");
let out = parse(&src);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(arg(&out.calls[0], "c").unwrap(), content);
}
#[test]
fn content_with_close_tool_tag_survives() {
let content = "x </tool> y";
let json_content = serde_json::to_string(content).unwrap();
let src = format!("```tool\n{{\"name\": \"w\", \"args\": {{\"c\": {json_content}}}}}\n```");
let out = parse(&src);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(arg(&out.calls[0], "c").unwrap(), content);
}
#[test]
fn xml_envelope_three_calls_without_close_parses() {
let out = parse(
"<tool_calls>\n<look>\n<file>\nsrc/writer.zig\n</file>\n</look>\n\
<look>\n<file>\nsrc/parser.zig\n</file>\n</look>\n\
<look>\n<file>\nsrc/root.zig\n</file>\n</look>",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert!(out.prose.is_empty(), "prose: {:?}", out.prose);
assert_eq!(out.calls.len(), 3);
for (call, file) in out
.calls
.iter()
.zip(["src/writer.zig", "src/parser.zig", "src/root.zig"])
{
assert_eq!(call["name"], "look");
assert_eq!(arg(call, "file").unwrap(), file);
}
assert!(
out.violations.iter().any(|v| v.contains("chat-template")),
"violations: {:?}",
out.violations
);
}
#[test]
fn xml_envelope_single_multi_arg_call_without_close_parses() {
let out = parse(
"<tool_calls>\n<look>\n<file>\nsrc/writer.zig\n</file>\n<intent>\nread\n</intent>\n</look>",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "look");
assert_eq!(arg(&out.calls[0], "file").unwrap(), "src/writer.zig");
assert_eq!(arg(&out.calls[0], "intent").unwrap(), "read");
}
#[test]
fn xml_envelope_multiline_arg_value_kept_verbatim() {
let out = parse(
"<tool_calls>\n<write>\n<content>\nline one\nline two\n</content>\n</write>\n</tool_calls>",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "write");
assert_eq!(arg(&out.calls[0], "content").unwrap(), "line one\nline two");
}
#[test]
fn tool_code_tag_with_inline_json_args_parses() {
let out = parse(
"<tool_code>\n{ \"name\": \"look\", \"file\": \"src/writer.zig\", \"intent\": \"read\" }",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "look");
assert_eq!(arg(&out.calls[0], "file").unwrap(), "src/writer.zig");
assert_eq!(arg(&out.calls[0], "intent").unwrap(), "read");
assert!(
out.violations.iter().any(|v| v.contains("chat-template")),
"violations: {:?}",
out.violations
);
}
#[test]
fn tool_code_tag_with_multiple_json_objects_parses() {
let out = parse(
"<tool_code>\n{\"name\":\"a\",\"args\":{\"k\":1}}\n{\"name\":\"b\",\"args\":{\"v\":2}}\n</tool_code>",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 2);
assert_eq!(out.calls[0]["name"], "a");
assert_eq!(arg(&out.calls[0], "k").unwrap(), 1);
assert_eq!(out.calls[1]["name"], "b");
assert_eq!(arg(&out.calls[1], "v").unwrap(), 2);
}
#[test]
fn bare_tool_call_tag_with_json_parses() {
let out = parse("<tool_call>\n{\"name\":\"look\",\"args\":{\"file\":\"a.rs\"}}\n</tool_call>");
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "look");
assert_eq!(arg(&out.calls[0], "file").unwrap(), "a.rs");
}
#[test]
fn prose_before_envelope_parses_and_keeps_prose() {
let out = parse(
"I will read the writer next.\n<tool_calls>\n<tool>\n{\"name\":\"look\",\"file\":\"src/writer.zig\"}\n</tool_calls>",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "look");
assert_eq!(arg(&out.calls[0], "file").unwrap(), "src/writer.zig");
assert!(
out.prose.contains("I will read the writer next."),
"prose: {:?}",
out.prose
);
}
#[test]
fn prose_after_envelope_close_survives() {
let out = parse(
"<tool_calls>\n<tool>\n{\"name\":\"look\",\"file\":\"a.rs\"}\n</tool_calls>\nDone reading.",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert!(
out.prose.contains("Done reading."),
"prose: {:?}",
out.prose
);
}
#[test]
fn truncated_inner_xml_tag_is_violation_zero_calls() {
let out = parse("<tool_calls>\n<look>\n<file>\nsrc/writer.zig");
assert!(out.calls.is_empty(), "calls: {:?}", out.calls);
assert_eq!(out.errors.len(), 1);
assert!(
out.errors[0].contains("<tool_calls>") && out.errors[0].contains("not executed"),
"error: {}",
out.errors[0]
);
}
#[test]
fn envelope_with_garbage_body_is_violation_not_none() {
let out = parse("<tool_code>\nlol this is not json at all\n</tool_code>");
assert!(out.calls.is_empty(), "calls: {:?}", out.calls);
assert_eq!(out.errors.len(), 1, "errors: {:?}", out.errors);
assert!(
out.errors[0].contains("not executed"),
"error: {}",
out.errors[0]
);
}
#[test]
fn prose_before_canonical_tool_fence_still_parses() {
let out = parse("Here is the call:\n```tool\n{\"name\": \"a\", \"args\": {\"k\": 1}}\n```");
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert!(
out.violations.is_empty(),
"violations: {:?}",
out.violations
);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "a");
assert_eq!(arg(&out.calls[0], "k").unwrap(), 1);
assert!(
out.prose.contains("Here is the call:"),
"prose: {:?}",
out.prose
);
}
#[test]
fn xml_envelope_duplicate_arg_is_violation_zero_calls() {
let out = parse(
"<tool_calls>\n<look>\n<file>\na.rs\n</file>\n<file>\nb.rs\n</file>\n</look>\n</tool_calls>",
);
assert!(out.calls.is_empty(), "calls: {:?}", out.calls);
assert_eq!(out.errors.len(), 1, "errors: {:?}", out.errors);
assert!(
out.errors[0].contains("<file>") && out.errors[0].contains("not executed"),
"error: {}",
out.errors[0]
);
}
#[test]
fn tool_code_json_string_with_literal_close_tag_survives() {
let out = parse(
"<tool_code>\n{ \"name\": \"write\", \"args\": { \"content\": \"</tool_code>\" } }\n</tool_code>",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "write");
assert_eq!(arg(&out.calls[0], "content").unwrap(), "</tool_code>");
}
#[test]
fn tool_marker_json_string_with_literal_envelope_close_survives() {
let out = parse(
"<tool_calls>\n<tool>\n{ \"name\": \"write\", \"content\": \"</tool_calls>\" }\n</tool_calls>",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "write");
assert_eq!(arg(&out.calls[0], "content").unwrap(), "</tool_calls>");
}
#[test]
fn xml_arg_value_with_literal_envelope_close_survives() {
let out = parse(
"<tool_calls>\n<write>\n<content>\n</tool_calls>\n</content>\n</write>\n</tool_calls>",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "write");
assert_eq!(arg(&out.calls[0], "content").unwrap(), "</tool_calls>");
}
#[test]
fn batch_three_objects_in_one_tool_fence_parses() {
let out = parse(
"```tool\n\
{\"name\": \"look\", \"args\": {\"file\": \"src/writer.zig\"}}\n\
{\"name\": \"look\", \"args\": {\"file\": \"src/parser.zig\"}}\n\
{\"name\": \"look\", \"args\": {\"file\": \"src/document.zig\"}}\n\
```",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 3, "calls: {:?}", out.calls);
for (call, file) in
out.calls
.iter()
.zip(["src/writer.zig", "src/parser.zig", "src/document.zig"])
{
assert_eq!(call["name"], "look");
assert_eq!(arg(call, "file").unwrap(), file);
}
assert_eq!(out.calls[0]["id"], "tc_0");
assert_eq!(out.calls[2]["id"], "tc_2");
}
#[test]
fn batch_double_tool_fence_separator_parses() {
let out = parse(
"```tool\n{\"name\": \"look\", \"args\": {\"file\": \"a.zig\"}}\n\
```tool\n{\"name\": \"look\", \"args\": {\"file\": \"b.zig\"}}",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 2, "calls: {:?}", out.calls);
assert_eq!(arg(&out.calls[0], "file").unwrap(), "a.zig");
assert_eq!(arg(&out.calls[1], "file").unwrap(), "b.zig");
}
#[test]
fn batch_salvages_complete_objects_before_truncated_tail() {
let out = parse(
"```tool\n{\"name\": \"look\", \"args\": {\"file\": \"a.zig\"}}\n\
{\"name\": \"look\", \"args\": {\"file\": \"b.zig\"}}\n\
{\"name\": \"look\", \"args\": {\"file\": \"trunc",
);
assert_eq!(out.calls.len(), 2, "calls: {:?}", out.calls);
assert_eq!(arg(&out.calls[0], "file").unwrap(), "a.zig");
assert_eq!(arg(&out.calls[1], "file").unwrap(), "b.zig");
assert_eq!(out.errors.len(), 1, "errors: {:?}", out.errors);
assert!(
out.errors[0].contains("Unterminated"),
"error: {}",
out.errors[0]
);
}
#[test]
fn batch_tool_calls_xml_three_nested_looks_parses() {
let out = parse(
"<tool_calls>\n<look>\n<file>\nsrc/writer.zig\n</file>\n</look>\n\
<look>\n<file>\nsrc/parser.zig\n</file>\n</look>\n\
<look>\n<file>\nsrc/document.zig\n</file>\n</look>\n</tool_calls>",
);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 3, "calls: {:?}", out.calls);
for call in &out.calls {
assert_eq!(call["name"], "look");
}
}
#[test]
fn batch_single_object_is_one_call() {
let out = parse("```tool\n{\"name\": \"look\", \"args\": {\"file\": \"a.zig\"}}\n```");
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
}
fn verbatim_block(
name: &str,
other_args_json: &str,
arg: &str,
tag: &str,
payload: &str,
) -> String {
let n = payload.matches('\n').count();
let sep = if other_args_json.is_empty() { "" } else { ", " };
format!(
"```tool\n{{ \"name\": \"{name}\", \"args\": {{ {other_args_json}{sep}\"{arg}\": \"<<{tag}:{n}\" }} }}\n\
<<{tag}:{n}\n{payload}{tag}\n```",
)
}
#[test]
fn tc19_zig_body_round_trips_byte_exact() {
let payload = include_str!("fixtures/tc19_zig_tests.zig");
let block = verbatim_block(
"edit_file",
"\"path\": \"src/writer.zig\"",
"new_string",
"Z",
payload,
);
let out = parse(&block);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(out.calls[0]["name"], "edit_file");
assert_eq!(arg(&out.calls[0], "path").unwrap(), "src/writer.zig");
assert_eq!(
arg(&out.calls[0], "new_string").unwrap().as_str().unwrap(),
payload
);
}
#[test]
fn verbatim_body_containing_the_tag_line_round_trips() {
let payload = "before\nEND\nmiddle\nEND\nafter\n";
let block = verbatim_block(
"write_file",
"\"path\": \"a.txt\"",
"content",
"END",
payload,
);
let out = parse(&block);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(
arg(&out.calls[0], "content").unwrap().as_str().unwrap(),
payload
);
}
#[test]
fn verbatim_body_with_all_promised_literals_round_trips() {
let payload = "```\n<<EOF\n}\n</tool>\n</tool_calls>\nconst raw = `go run`;\n```\nEOF\n";
let block = verbatim_block("write_file", "\"path\": \"b.txt\"", "content", "T", payload);
let out = parse(&block);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(
arg(&out.calls[0], "content").unwrap().as_str().unwrap(),
payload
);
}
#[test]
fn verbatim_empty_body_round_trips() {
let block = verbatim_block("write_file", "\"path\": \"empty.txt\"", "content", "T", "");
let out = parse(&block);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(arg(&out.calls[0], "content").unwrap().as_str().unwrap(), "");
}
#[test]
fn verbatim_crlf_body_round_trips() {
let payload = "line one\r\nline two\r\n\r\nline four\r\n";
let block = verbatim_block(
"write_file",
"\"path\": \"crlf.txt\"",
"content",
"T",
payload,
);
let out = parse(&block);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(
arg(&out.calls[0], "content").unwrap().as_str().unwrap(),
payload
);
}
#[test]
fn verbatim_tool_fence_in_body_is_content_not_split() {
let payload = "line a\n```tool\nline c\n";
let block = verbatim_block("write_file", "\"path\": \"d.txt\"", "content", "T", payload);
let out = parse(&block);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(
arg(&out.calls[0], "content").unwrap().as_str().unwrap(),
payload
);
}
#[test]
fn verbatim_two_bodies_matched_by_opener() {
let block = "```tool\n{ \"name\": \"edit_file\", \"args\": { \"path\": \"x.rs\", \
\"old_string\": \"<<OLD:1\", \"new_string\": \"<<NEW:2\" } }\n\
<<NEW:2\nNEW a\\b\nNEW c\nNEW\n\
<<OLD:1\nOLD \"q\"\nOLD\n```";
let out = parse(block);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(
arg(&out.calls[0], "new_string").unwrap().as_str().unwrap(),
"NEW a\\b\nNEW c\n"
);
assert_eq!(
arg(&out.calls[0], "old_string").unwrap().as_str().unwrap(),
"OLD \"q\"\n"
);
}
#[test]
fn verbatim_coexists_with_batched_object() {
let block = "```tool\n{ \"name\": \"read_file\", \"args\": { \"path\": \"a.zig\" } }\n\
{ \"name\": \"write_file\", \"args\": { \"path\": \"b.zig\", \"content\": \"<<C:1\" } }\n\
<<C:1\nconst x = \"a\\tb\";\nC\n```";
let out = parse(block);
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 2, "calls: {:?}", out.calls);
assert_eq!(out.calls[0]["name"], "read_file");
assert_eq!(out.calls[1]["name"], "write_file");
assert_eq!(
arg(&out.calls[1], "content").unwrap().as_str().unwrap(),
"const x = \"a\\tb\";\n"
);
}
#[test]
fn verbatim_wrong_count_fails_loud() {
let block = "```tool\n{ \"name\": \"write_file\", \"args\": { \"path\": \"a\", \"content\": \"<<C:5\" } }\n\
<<C:5\nl1\nl2\nC\n```";
let out = parse(block);
assert!(out.calls.is_empty(), "must not dispatch: {:?}", out.calls);
assert!(
out.errors
.iter()
.any(|e| e.contains("recount") || e.contains("did not close")),
"expected actionable recount error, got {:?}",
out.errors
);
}
fn assert_verbatim_rejected(block: &str, expected_error_count: usize) {
let out = parse(block);
assert_eq!(out.calls, Vec::<serde_json::Value>::new());
assert_eq!(out.errors.len(), expected_error_count, "{:?}", out.errors);
}
#[test]
fn verbatim_count_overflow_never_downgrades_or_dispatches() {
assert_verbatim_rejected(
"```tool\n{ \"name\": \"write_file\", \"args\": { \"content\": \"<<C:184467440737095516160\" } }\n\
<<C:184467440737095516160\nbody\nC\n```",
1,
);
}
#[test]
fn verbatim_counted_close_with_suffix_never_drops_bytes_or_dispatches() {
assert_verbatim_rejected(
"```tool\n{ \"name\": \"write_file\", \"args\": { \"content\": \"<<C:1\" } }\n\
<<C:1\nbody\nC trailing payload\n```",
1,
);
}
#[test]
fn verbatim_counted_declaration_without_body_never_dispatches() {
assert_verbatim_rejected(
"```tool\n{\"name\":\"write_file\",\"args\":{\"content\":\"<<C:1\"}}\n```",
1,
);
}
#[test]
fn verbatim_mismatched_counted_body_never_dispatches() {
assert_verbatim_rejected(
"```tool\n{\"name\":\"write_file\",\"args\":{\"content\":\"<<C:1\"}}\n\
<<D:1\nbody\nD\n```",
1,
);
}
#[test]
fn verbatim_one_of_two_missing_counted_bodies_never_dispatches() {
assert_verbatim_rejected(
"```tool\n{\"name\":\"edit_file\",\"args\":{\"old\":\"<<OLD:1\",\"new\":\"<<NEW:1\"}}\n\
<<OLD:1\nbefore\nOLD\n```",
1,
);
}
#[test]
fn verbatim_duplicate_counted_declarations_need_distinct_bodies() {
assert_verbatim_rejected(
"```tool\n{\"name\":\"edit_file\",\"args\":{\"old\":\"<<BODY:1\",\"new\":\"<<BODY:1\"}}\n\
<<BODY:1\nonly one\nBODY\n```",
1,
);
}
#[test]
fn verbatim_counted_crlf_close_round_trips() {
let out = parse(
"```tool\r\n{\"name\":\"write_file\",\"args\":{\"content\":\"<<C:1\"}}\r\n\
<<C:1\r\nbody\r\n C\r\n```",
);
assert_eq!(out.errors, Vec::<String>::new());
assert_eq!(out.calls.len(), 1);
assert_eq!(arg(&out.calls[0], "content").unwrap(), "body\r\n");
}
#[test]
fn literal_heredoc_opener_value_without_body_survives() {
let out = parse("```tool\n{\"name\": \"write_file\", \"args\": {\"path\": \"a\", \"content\": \"<<EOF\"}}\n```");
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(
arg(&out.calls[0], "content").unwrap().as_str().unwrap(),
"<<EOF"
);
}
#[test]
fn orphan_trailing_heredoc_fails_loud() {
let block = "```tool\n{ \"name\": \"write_file\", \"args\": { \"path\": \"a\", \"content\": \"x\" } }\n\
<<ORPHAN:1\nstray\nORPHAN\n```";
let out = parse(block);
assert!(
!out.errors.is_empty(),
"expected a fail-loud mismatch, got {:?}",
out.errors
);
}
#[test]
fn pure_json_block_is_unchanged_by_verbatim_lane() {
let out = parse("```tool\n{\"name\": \"write_file\", \"args\": {\"path\": \"a\", \"content\": \"x\\ny\"}}\n```");
assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
assert_eq!(out.calls.len(), 1);
assert_eq!(
arg(&out.calls[0], "content").unwrap().as_str().unwrap(),
"x\ny"
);
}