use super::super::*;
use super::streaming_frag::{args_from_outputs, collect_fragments, write_and_bash_tools};
#[test]
#[ignore = "mutates process-global METRALE_BUFFER_TOOL_ARGS; run serially with --ignored --test-threads=1"]
fn kill_switch_buffers_full_args_no_fragments() {
let _guard = env_guard::set("METRALE_BUFFER_TOOL_ARGS", "1");
let mut det = StreamingToolDetector::new_with_tools(write_and_bash_tools());
let chunks = [
"<tool_call>",
"<function=Write>",
"<parameter=file_path>",
"/tmp/x.rs",
"</parameter>",
"<parameter=content>",
"hello",
"</parameter>",
"</function>",
"</tool_call>",
];
let mut outputs = Vec::new();
for c in chunks {
outputs.extend(det.process(c));
}
let frag_count = outputs
.iter()
.filter(|o| matches!(o, DetectorOutput::ToolCallArgsFragment { .. }))
.count();
let delta_count = outputs
.iter()
.filter(|o| matches!(o, DetectorOutput::ToolCallDelta { .. }))
.count();
assert_eq!(frag_count, 0, "kill-switch must emit NO live fragments");
assert_eq!(
delta_count, 1,
"kill-switch must emit exactly one buffered ToolCallDelta"
);
let args: serde_json::Value = serde_json::from_str(&args_from_outputs(&outputs)).unwrap();
assert_eq!(args["file_path"], "/tmp/x.rs");
assert_eq!(args["content"], "hello");
}
mod env_guard {
use std::sync::{Mutex, MutexGuard, OnceLock};
static ENV_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
pub struct Guard {
key: &'static str,
prev: Option<String>,
_lock: MutexGuard<'static, ()>,
}
pub fn set(key: &'static str, val: &str) -> Guard {
let lock = ENV_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|p| p.into_inner());
let prev = std::env::var(key).ok();
unsafe {
std::env::set_var(key, val);
}
Guard {
key,
prev,
_lock: lock,
}
}
impl Drop for Guard {
fn drop(&mut self) {
unsafe {
match &self.prev {
Some(v) => std::env::set_var(self.key, v),
None => std::env::remove_var(self.key),
}
}
}
}
}
#[test]
fn bare_function_streams_incrementally_and_emits_call_once() {
let mut det = StreamingToolDetector::new_with_tools(write_and_bash_tools());
let full = "<function=Write>\n\
<parameter=file_path>\n/tmp/x.rs\n</parameter>\n\
<parameter=content>\nhello\n</parameter>\n\
</function>";
let bytes = full.as_bytes();
let mut outputs = Vec::new();
let mut i = 0;
while i < bytes.len() {
let end = (i + 5).min(bytes.len());
outputs.extend(det.process(&full[i..end]));
i = end;
}
outputs.extend(det.flush());
let start_pos = outputs
.iter()
.position(|o| matches!(o, DetectorOutput::ToolCallStart { .. }))
.expect("bare <function=> must emit ToolCallStart before the block closes");
let frag_positions: Vec<usize> = outputs
.iter()
.enumerate()
.filter(|(_, o)| matches!(o, DetectorOutput::ToolCallArgsFragment { .. }))
.map(|(i, _)| i)
.collect();
assert!(
frag_positions.len() >= 2,
"expected MULTIPLE incremental fragments for a bare function, got {}",
frag_positions.len()
);
assert!(
frag_positions.iter().all(|&p| p > start_pos),
"fragments must follow ToolCallStart"
);
let whole_calls = outputs
.iter()
.filter(|o| matches!(o, DetectorOutput::ToolCall(..)))
.count();
assert_eq!(
whole_calls, 0,
"a call streamed incrementally must not ALSO be emitted whole (duplicate delivery)"
);
let args: serde_json::Value = serde_json::from_str(&collect_fragments(&outputs)).unwrap();
assert_eq!(
args,
serde_json::json!({"file_path": "/tmp/x.rs", "content": "hello"})
);
}