use super::strip_bare_role_literal;
use crate::tool_parser::{DetectorOutput, StreamingToolDetector};
fn stream_names(chunks: &[&str]) -> Vec<String> {
let mut det = StreamingToolDetector::new();
let mut names = Vec::new();
for &c in chunks {
let mut delta = c.to_string();
let inside_tool_call = det.inside_tool_call();
strip_bare_role_literal(&mut delta, inside_tool_call);
if delta.is_empty() {
continue;
}
for o in det.process(&delta) {
if let DetectorOutput::ToolCallStart { name, .. } = o {
names.push(name);
}
}
}
for o in det.flush() {
if let DetectorOutput::ToolCallStart { name, .. } = o {
names.push(name);
}
}
names
}
#[test]
fn tool_search_name_split_after_tool_streams_intact() {
let names = stream_names(&[
"<tool_call>\n{\"name\": \"",
"tool",
"_search\", \"arguments\": {\"query\": \"CRM\"}}",
"\n</tool_call>",
]);
assert_eq!(names, vec!["tool_search".to_string()]);
}
#[test]
fn tool_call_name_split_after_tool_streams_intact() {
let names = stream_names(&[
"<tool_call>\n{\"name\": \"",
"tool",
"_call\", \"arguments\": {}}",
"\n</tool_call>",
]);
assert_eq!(names, vec!["tool_call".to_string()]);
}
#[test]
fn tool_describe_name_split_after_tool_streams_intact() {
let names = stream_names(&[
"<tool_call>\n{\"name\": \"",
"tool",
"_describe\", \"arguments\": {\"id\": 7}}",
"\n</tool_call>",
]);
assert_eq!(names, vec!["tool_describe".to_string()]);
}
#[test]
fn ordinary_name_streams_intact() {
let names = stream_names(&[
"<tool_call>\n{\"name\": \"get",
"_weather\", \"arguments\": {\"city\": \"NYC\"}}",
"\n</tool_call>",
]);
assert_eq!(names, vec!["get_weather".to_string()]);
}
#[test]
fn bare_role_literal_still_stripped_outside_tool_call() {
for lit in ["user", "assistant", "tool", " tool "] {
let mut d = lit.to_string();
strip_bare_role_literal(&mut d, false);
assert!(
d.is_empty(),
"bare role literal {lit:?} must be stripped in content"
);
}
}
#[test]
fn bare_role_literal_preserved_inside_tool_call() {
for lit in ["user", "assistant", "tool"] {
let mut d = lit.to_string();
strip_bare_role_literal(&mut d, true);
assert_eq!(d, lit, "fragment {lit:?} must survive inside a tool call");
}
}
#[test]
fn dsml_split_opener_preserves_tool_fragment_and_streams_call() {
let mut det = StreamingToolDetector::new();
let chunks = [
"\n\n",
"<|DSM",
"L",
"|",
"tool",
"_calls>\n<|DSML|invoke name=\"get_weather\">\n",
"<|DSML|parameter name=\"city\" string=\"true\">Paris</|DSML|parameter>\n",
"</|DSML|invoke>\n</|DSML|tool_calls>",
];
let mut outputs = Vec::new();
for chunk in chunks {
let mut delta = chunk.to_string();
strip_bare_role_literal(&mut delta, det.inside_tool_call());
if !delta.is_empty() {
outputs.extend(det.process(&delta));
}
}
outputs.extend(det.flush());
let mut content = String::new();
let mut calls = Vec::new();
for output in outputs {
match output {
DetectorOutput::Content(text) => content.push_str(&text),
DetectorOutput::ToolCall(call, index) => calls.push((index, call)),
_ => {}
}
}
assert!(content.is_empty(), "DSML envelope leaked: {content:?}");
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].0, 0);
assert_eq!(calls[0].1.function.name, "get_weather");
assert_eq!(calls[0].1.function.arguments, r#"{"city":"Paris"}"#);
}
#[test]
fn ordinary_content_untouched() {
for inside in [false, true] {
let mut d = "the tool ran".to_string();
strip_bare_role_literal(&mut d, inside);
assert_eq!(d, "the tool ran");
}
}