use std::fmt;
use std::sync::{Arc, Mutex, PoisonError};
use std::time::Duration;
use crate::AgentCat;
use crate::config::Options;
use crate::delivery::Observer;
use crate::event::Event;
use crate::pipeline::EventObject;
const TOOL_CALL: &str = "mcp:tools/call";
const CUSTOM: &str = "agentcat:custom";
const FLUSH: Duration = Duration::from_secs(5);
#[derive(Debug, Default)]
struct Recorder {
events: Mutex<Vec<Event>>,
}
impl Observer for Recorder {
fn finished(&self, event: &EventObject) {
self.events
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(Event::from_object(event));
}
}
#[derive(Clone, Debug, Default)]
pub struct Fake {
recorder: Arc<Recorder>,
handles: Arc<Mutex<Vec<AgentCat>>>,
}
impl Fake {
pub fn new() -> Self {
Self::default()
}
pub fn agentcat(&self, project_id: impl Into<String>, options: Options) -> AgentCat {
let handle = AgentCat::offline_observed(
project_id.into(),
options,
Arc::clone(&self.recorder) as Arc<dyn Observer>,
);
self.handles
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(handle.clone());
handle
}
pub async fn events(&self) -> Vec<Event> {
self.recorded().await.events
}
async fn recorded(&self) -> Recorded {
let handles: Vec<AgentCat> = self
.handles
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
let mut settled = true;
for handle in handles {
settled &= handle.flush(FLUSH).await;
}
let events = self
.recorder
.events
.lock()
.unwrap_or_else(PoisonError::into_inner)
.clone();
Recorded { events, settled }
}
pub async fn assert_tracked(&self, tool_name: &str) {
let recorded = self.recorded().await;
let found = recorded
.events
.iter()
.any(|e| e.event_type == TOOL_CALL && e.resource_name.as_deref() == Some(tool_name));
assert!(
found,
"expected a tracked call of tool `{tool_name}`, {recorded}"
);
}
pub async fn assert_tracked_count(&self, count: usize) {
let recorded = self.recorded().await;
let actual = recorded
.events
.iter()
.filter(|e| e.event_type == TOOL_CALL)
.count();
assert!(
actual == count,
"expected {count} tracked tool calls, found {actual}, {recorded}"
);
}
pub async fn assert_nothing_tracked(&self) {
let recorded = self.recorded().await;
assert!(
recorded.events.is_empty(),
"expected nothing was tracked, {recorded}"
);
}
pub async fn assert_custom_event_published(&self, resource_name: &str) {
let recorded = self.recorded().await;
let found = recorded
.events
.iter()
.any(|e| e.event_type == CUSTOM && e.resource_name.as_deref() == Some(resource_name));
assert!(
found,
"expected a custom event `{resource_name}`, {recorded}"
);
}
}
struct Recorded {
events: Vec<Event>,
settled: bool,
}
impl fmt::Display for Recorded {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("recorded: [")?;
for (i, e) in self.events.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(
f,
"{} {}",
e.event_type,
e.resource_name.as_deref().unwrap_or("-")
)?;
}
f.write_str("]")?;
if !self.settled {
write!(
f,
" (a handle did not finish delivering within the {} s flush: an event whose hooks \
take longer is not recorded yet)",
FLUSH.as_secs()
)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests;