use std::cell::RefCell;
use std::rc::Rc;
use crate::agent_events::{AgentEvent, ToolCallStatus};
use super::{run_detector_loop_with_sink, StreamingDetectorContext};
async fn drive(session_id: &str, known: &[&str], chunks: &[&str]) -> Vec<AgentEvent> {
let captured: Rc<RefCell<Vec<AgentEvent>>> = Rc::new(RefCell::new(Vec::new()));
let sink_buf = captured.clone();
let known_tools = known.iter().map(|s| (*s).to_string()).collect();
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<String>();
for chunk in chunks {
tx.send((*chunk).to_string()).expect("send delta");
}
drop(tx);
run_detector_loop_with_sink(
StreamingDetectorContext {
session_id: session_id.to_string(),
known_tools,
},
rx,
move |event| sink_buf.borrow_mut().push(event.clone()),
)
.await;
let events = captured.borrow().clone();
events
}
#[tokio::test(flavor = "current_thread")]
async fn detector_loop_emits_start_and_promoted_through_sink() {
let events = drive(
"session-stream-promote",
&["read"],
&["read({ path: \"a.md\" })"],
)
.await;
assert_eq!(
events.len(),
2,
"expected start + promoted, got: {events:#?}"
);
match &events[0] {
AgentEvent::ToolCall {
parsing,
tool_name,
status,
..
} => {
assert_eq!(*parsing, Some(true));
assert_eq!(tool_name, "read");
assert_eq!(*status, ToolCallStatus::Pending);
}
other => panic!("expected ToolCall, got {other:?}"),
}
match &events[1] {
AgentEvent::ToolCallUpdate {
parsing,
status,
error_category,
raw_input,
raw_output,
..
} => {
assert_eq!(*parsing, Some(false));
assert_eq!(*status, ToolCallStatus::Pending);
assert!(error_category.is_none());
assert_eq!(
raw_input.as_ref(),
Some(&serde_json::json!({"path": "a.md"}))
);
assert!(raw_output.is_none(), "promoted args belong in raw_input");
}
other => panic!("expected ToolCallUpdate, got {other:?}"),
}
}
#[tokio::test(flavor = "current_thread")]
async fn detector_loop_finalizes_unclosed_tagged_block_as_aborted() {
let events = drive(
"session-stream-abort",
&["run"],
&["<tool_call>\nrun({ command: \"ls\""],
)
.await;
assert_eq!(events.len(), 2, "events={events:#?}");
match &events[1] {
AgentEvent::ToolCallUpdate {
status,
error_category,
parsing,
..
} => {
assert_eq!(*status, ToolCallStatus::Failed);
assert_eq!(
*error_category,
Some(crate::agent_events::ToolCallErrorCategory::ParseAborted)
);
assert_eq!(*parsing, Some(false));
}
other => panic!("expected ToolCallUpdate, got {other:?}"),
}
}