use std::sync::Arc;
use futures::executor::block_on;
use super::*;
use crate::{
completion::{CompletionRequest, Message},
effect::{FamilyDescriptor, family, tool_key},
operation::Finish,
serve::{Dispatch, ErasedHandler, Reply, Serve, catch_panics},
tool::ToolResult,
};
fn tool(name: &str) -> EffectKind {
EffectKind::ToolCall {
name: name.to_owned(),
args: "{}".to_owned(),
}
}
fn answer(text: &str) -> Result<Answered, ErrorReport> {
Ok(Answered::ToolResult {
result: ToolResult::skipped(text),
})
}
fn json<T: serde::Serialize>(value: &T) -> serde_json::Value {
serde_json::to_value(value).expect("serializes")
}
fn ids(records: &[EffectRecord]) -> Vec<u64> {
records.iter().map(|record| record.id.as_u64()).collect()
}
struct Streamer;
impl Serve for Streamer {
type Family = family::Dynamic;
fn descriptor(&self) -> HandlerDescriptor {
HandlerDescriptor {
key: HandlerKey::from("streamer"),
family: FamilyDescriptor::Custom {
kind: "test".into(),
},
layers: Vec::new(),
}
}
async fn serve(&self, _kind: EffectKind, _dispatch: Dispatch) -> Reply {
Reply::written(
crate::message::Origin::new("test", "test", "test"),
|mut writer| async move {
writer.text("hel").await.expect("open");
writer.text("lo").await.expect("open");
writer.finish(Finish::default()).await.expect("open");
},
)
}
}
#[test]
fn records_keep_dispatch_order_and_origin_whatever_order_they_resolve_in() {
let buffer = RecordBuffer::new();
let origin = DispatchContext {
parent: Some(EffectId::from_raw(9)),
scope: Some(Arc::from("agent-1")),
purpose: Some("summary".into()),
};
for raw in 1..=3 {
let id = EffectId::from_raw(raw);
buffer.begin(id, tool_key("t"), tool("t"), origin.clone());
}
buffer.resolve(EffectId::from_raw(3), answer("three"));
buffer.resolve(EffectId::from_raw(1), answer("one"));
assert_eq!((buffer.resolved(), buffer.in_flight()), (2, 1));
assert_eq!(ids(&buffer.records()), [1, 3], "a copy leaves them");
assert!(
buffer.take(EffectId::from_raw(2)).is_none(),
"still in flight"
);
let taken = buffer.take_resolved();
assert_eq!(ids(&taken), [1, 3]);
assert_eq!(
format!("{buffer:?}"),
"RecordBuffer { resolved: 0, in_flight: 1 }"
);
let first = taken.first().expect("taken");
assert_eq!(first.parent, Some(EffectId::from_raw(9)));
assert_eq!(first.scope.as_deref(), Some("agent-1"));
assert_eq!(first.purpose.as_deref(), Some("summary"));
assert_eq!(first.disposition, Disposition::Ran);
assert!(first.reviewed.is_none());
assert_eq!(
(first.started_ms, first.finished_ms),
(None, None),
"untimed"
);
buffer.resolve(EffectId::from_raw(2), answer("two"));
let second = buffer.take(EffectId::from_raw(2)).expect("resolved");
assert_eq!(json(&second.outcome), json(&answer("two")));
assert_eq!(buffer.resolved() + buffer.in_flight(), 0);
}
#[test]
fn a_filtered_take_leaves_the_records_it_refuses() {
let buffer = RecordBuffer::new();
for raw in 1..=4 {
let id = EffectId::from_raw(raw);
buffer.begin(id, tool_key("t"), tool("t"), DispatchContext::default());
}
for raw in [4, 2, 1] {
buffer.resolve(EffectId::from_raw(raw), answer("done"));
}
let taken = buffer.take_resolved_where(|id| id != EffectId::from_raw(2));
assert_eq!(ids(&taken), [1, 4], "accepted ones, in dispatch order");
assert_eq!((buffer.resolved(), buffer.in_flight()), (1, 1));
assert_eq!(ids(&buffer.take_resolved()), [2], "the refused one stays");
}
#[test]
fn discard_drops_patch_replaces_and_tool_output_lands_before_the_outcome() {
let buffer = RecordBuffer::new();
let (patched, dropped) = (EffectId::from_raw(1), EffectId::from_raw(2));
buffer.begin(
patched,
tool_key("a"),
tool("a"),
DispatchContext::default(),
);
buffer.begin(
dropped,
tool_key("b"),
tool("b"),
DispatchContext::default(),
);
buffer.discard(dropped);
assert!(!buffer.contains(dropped) && buffer.contains(patched));
assert_eq!(buffer.remove(dropped), None, "already gone");
buffer.patch(patched, tool("c"));
buffer.tool_output(patched, ToolResultContext::default());
buffer.resolve(patched, answer("done"));
buffer.tool_output(patched, ToolResultContext::default());
let record = buffer.take(patched).expect("resolved");
assert_eq!(json(&record.kind), json(&tool("c")));
assert_eq!(record.published, Some(ToolResultContext::default()));
buffer.begin(
dropped,
tool_key("b"),
tool("b"),
DispatchContext::default(),
);
assert_eq!(buffer.remove(dropped), Some(tool_key("b")));
}
#[test]
fn a_recorded_stream_keeps_its_items_and_a_timed_buffer_stamps_its_ends() {
let buffer = RecordBuffer::new().keeping_stream_events().timed();
assert!(buffer.keep_events());
let id = EffectId::from_raw(4);
let kind = EffectKind::Completion {
request: CompletionRequest::new(Message::user("hi")).into(),
stream: true,
};
let handler = ErasedHandler::new(Streamer);
let key = handler.descriptor().key;
buffer.begin(id, key, kind.clone(), DispatchContext::default());
let recorded = Arc::new(buffer.clone());
let dispatch = Dispatch::new(id, true).recorded_by(recorded);
let outcome = block_on(async { handler.handle(kind, dispatch).await.into_outcome().await });
assert!(
matches!(outcome, Ok(Answered::Completion(_))),
"{outcome:?}"
);
assert!(
buffer.kept_events(id).is_some_and(|items| items >= 3),
"two deltas and the end"
);
assert_eq!(buffer.kept_events(EffectId::from_raw(5)), None);
let record = buffer.take(id).expect("resolved");
assert!(record.events.is_some(), "the items are kept");
let from = record.stream_origin.expect("the relay said who it is");
assert_eq!(from, crate::message::Origin::new("test", "test", "test"));
let (started, finished) = (record.started_ms, record.finished_ms);
let (started, finished) = (started.expect("timed"), finished.expect("timed"));
assert!(started <= finished);
assert!(started > 1_600_000_000_000, "milliseconds since the epoch");
}
#[test]
fn a_unary_dispatch_keeps_no_items() {
let buffer = RecordBuffer::new().keeping_stream_events();
let id = EffectId::from_raw(1);
buffer.begin(id, tool_key("t"), tool("t"), DispatchContext::default());
let origin = crate::message::Origin::new("test", "test", "test");
buffer.origin(id, &origin);
buffer.handlers(Vec::new());
buffer.resolve(id, answer("done"));
let record = buffer.take(id).expect("resolved");
assert!(record.events.is_none() && record.stream_origin.is_none());
}
async fn boom() -> Result<(), ErrorReport> {
explode();
Ok(())
}
fn explode() {
panic!("boom")
}
#[test]
fn a_panic_is_an_internal_error_recorded_as_the_outcome() {
let buffer = RecordBuffer::new();
let id = EffectId::from_raw(1);
buffer.begin(id, tool_key("t"), tool("t"), DispatchContext::default());
let recorder: Arc<dyn Recorder + Send + Sync> = Arc::new(buffer.clone());
let error = block_on(catch_panics(Some(recorder), id, boom())).expect_err("panicked");
assert_eq!(error.kind, crate::error::ErrorKind::Internal);
let record = buffer.take(id).expect("resolved");
assert_eq!(json(&record.outcome), json(&Err::<Answered, _>(error)));
let unrecorded = block_on(catch_panics(None, id, boom()));
assert!(unrecorded.is_err_and(|error| error.to_string().contains("panicked: boom")));
assert_eq!(
block_on(catch_panics(None, id, async { Ok(7) })).ok(),
Some(7)
);
}