Something went wrong. Try again.
A 5e storytelling engine with an LLM DM
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399//! Tests for the turn bus and its listeners.
use super::*;use crate::play::worker::TurnEvent;use std::sync::atomic::AtomicBool;use std::sync::mpsc;
/// A listener that collects events for assertions.#[derive(Clone)]struct CollectingListener { inner: std::sync::Arc<std::sync::Mutex<Collected>>,}
#[derive(Default)]struct Collected { deltas: Vec<String>, tools: Vec<Text<'static>>, done: Option<String>, cancelled: Option<String>, failed: Option<String>, break_on_delta: bool, break_on_tool: bool,}
impl CollectingListener { fn new() -> Self { Self { inner: std::sync::Arc::new(std::sync::Mutex::new(Collected::default())), } }
fn break_on_delta(self) -> Self { self.inner.lock().unwrap().break_on_delta = true; self }
fn break_on_tool(self) -> Self { self.inner.lock().unwrap().break_on_tool = true; self }
fn deltas(&self) -> Vec<String> { self.inner.lock().unwrap().deltas.clone() }
fn done(&self) -> Option<String> { self.inner.lock().unwrap().done.clone() }
fn cancelled(&self) -> Option<String> { self.inner.lock().unwrap().cancelled.clone() }
fn failed(&self) -> Option<String> { self.inner.lock().unwrap().failed.clone() }}
impl Listener for CollectingListener { fn delta(&mut self, text: &str) -> Flow { let mut state = self.inner.lock().unwrap(); state.deltas.push(text.to_string()); if state.break_on_delta { Flow::Break } else { Flow::Continue } }
fn tool(&mut self, display: Option<&Text<'static>>) -> Flow { if let Some(text) = display { let mut state = self.inner.lock().unwrap(); state.tools.push(text.clone()); if state.break_on_tool { return Flow::Break; } } Flow::Continue }
fn done(&mut self, reply: &str) { self.inner.lock().unwrap().done = Some(reply.to_string()); }
fn cancelled(&mut self, partial: &str) { self.inner.lock().unwrap().cancelled = Some(partial.to_string()); }
fn failed(&mut self, error: &str) { self.inner.lock().unwrap().failed = Some(error.to_string()); }}
#[test]fn empty_bus_delta_returns_continue() { let mut bus = TurnBus::new(); assert_eq!(bus.delta("hello"), ControlFlow::Continue(()));}
#[test]fn empty_bus_tool_returns_continue() { let mut bus = TurnBus::new(); assert_eq!(bus.tool(None), ControlFlow::Continue(()));}
#[test]fn empty_bus_done_does_not_panic() { let mut bus = TurnBus::new(); bus.done("hello");}
#[test]fn empty_bus_cancelled_does_not_panic() { let mut bus = TurnBus::new(); bus.cancelled("partial");}
#[test]fn empty_bus_failed_does_not_panic() { let mut bus = TurnBus::new(); bus.failed("error");}
#[test]fn single_listener_receives_delta() { let mut bus = TurnBus::new(); let collector = CollectingListener::new(); bus.add(Box::new(collector.clone()));
let _ = bus.delta("You wake.");
assert_eq!(collector.deltas(), vec!["You wake."]);}
#[test]fn multiple_listeners_all_receive_delta() { let mut bus = TurnBus::new(); let c1 = CollectingListener::new(); let c2 = CollectingListener::new(); bus.add(Box::new(c1.clone())); bus.add(Box::new(c2.clone()));
let _ = bus.delta("Hello");
assert_eq!(c1.deltas(), vec!["Hello"]); assert_eq!(c2.deltas(), vec!["Hello"]);}
#[test]fn delta_break_stops_iteration() { let mut bus = TurnBus::new(); let c1 = CollectingListener::new().break_on_delta(); let c2 = CollectingListener::new(); bus.add(Box::new(c1.clone())); bus.add(Box::new(c2.clone()));
assert_eq!(bus.delta("stop"), ControlFlow::Break(())); // c2 should not have received the event assert!(c2.deltas().is_empty());}
#[test]fn tool_break_stops_iteration() { let mut bus = TurnBus::new(); let c1 = CollectingListener::new().break_on_tool(); let c2 = CollectingListener::new(); bus.add(Box::new(c1.clone())); bus.add(Box::new(c2.clone()));
let display: Text<'static> = Text::raw("rolled 20"); assert_eq!(bus.tool(Some(&display)), ControlFlow::Break(())); // c2 should not have received the event assert!(c2.deltas().is_empty());}
#[test]fn done_fans_to_all_listeners() { let mut bus = TurnBus::new(); let c1 = CollectingListener::new(); let c2 = CollectingListener::new(); bus.add(Box::new(c1.clone())); bus.add(Box::new(c2.clone()));
bus.done("finished");
assert_eq!(c1.done(), Some("finished".to_string())); assert_eq!(c2.done(), Some("finished".to_string()));}
#[test]fn cancelled_fans_to_all_listeners() { let mut bus = TurnBus::new(); let c1 = CollectingListener::new(); let c2 = CollectingListener::new(); bus.add(Box::new(c1.clone())); bus.add(Box::new(c2.clone()));
bus.cancelled("partial");
assert_eq!(c1.cancelled(), Some("partial".to_string())); assert_eq!(c2.cancelled(), Some("partial".to_string()));}
#[test]fn failed_fans_to_all_listeners() { let mut bus = TurnBus::new(); let c1 = CollectingListener::new(); let c2 = CollectingListener::new(); bus.add(Box::new(c1.clone())); bus.add(Box::new(c2.clone()));
bus.failed("something broke");
assert_eq!(c1.failed(), Some("something broke".to_string())); assert_eq!(c2.failed(), Some("something broke".to_string()));}
#[test]fn screen_listener_sends_delta_events() { let (sender, receiver) = mpsc::channel(); let cancel = Arc::new(AtomicBool::new(false)); let mut bus = TurnBus::new(); bus.add(Box::new(ScreenListener::new(sender, cancel)));
let _ = bus.delta("Hello");
assert_eq!( receiver.recv().unwrap(), TurnEvent::Delta("Hello".to_string()) );}
#[test]fn screen_listener_cancel_flag_breaks_on_delta() { let (sender, receiver) = mpsc::channel(); let cancel = Arc::new(AtomicBool::new(true)); let mut bus = TurnBus::new(); bus.add(Box::new(ScreenListener::new(sender, cancel)));
// The delta is still sent even when cancel is set (matching old // callback behavior: send first, then check cancel). assert_eq!(bus.delta("Hello"), ControlFlow::Break(())); assert_eq!( receiver.recv().unwrap(), TurnEvent::Delta("Hello".to_string()) );}
#[test]fn screen_listener_closed_channel_does_not_break() { let (sender, receiver) = mpsc::channel::<TurnEvent>(); drop(receiver); // close the receiver side let cancel = Arc::new(AtomicBool::new(false)); let mut bus = TurnBus::new(); bus.add(Box::new(ScreenListener::new(sender, cancel)));
// A closed channel does not cause a break (matching old callback // behavior where channel send failures were silently ignored). assert_eq!(bus.delta("Hello"), ControlFlow::Continue(()));}
#[test]fn tool_without_display_does_not_send() { let (sender, receiver) = mpsc::channel(); let cancel = Arc::new(AtomicBool::new(false)); let mut bus = TurnBus::new(); bus.add(Box::new(ScreenListener::new(sender, cancel)));
let _ = bus.tool(None);
// Nothing should have been sent; the channel should be empty assert!(receiver.try_recv().is_err());}
#[test]fn tool_with_display_sends_tool_event() { let (sender, receiver) = mpsc::channel(); let cancel = Arc::new(AtomicBool::new(false)); let mut bus = TurnBus::new(); bus.add(Box::new(ScreenListener::new(sender, cancel)));
let display: Text<'static> = Text::raw("rolled 20"); let _ = bus.tool(Some(&display));
assert!(matches!(receiver.recv().unwrap(), TurnEvent::Tool(_)));}
#[test]fn campaign_listener_accumulates_narration_text() { let mut bus = TurnBus::new(); let listener = CampaignListener::new(None); bus.add(Box::new(listener));
let _ = bus.delta("You "); let _ = bus.delta("wake.");
// No campaign to write to, but we verify it doesn't panic and the // narration was accumulated (internal state verified via done). bus.done("You wake.");}
#[test]fn campaign_listener_done_falls_back_to_reply_when_narration_empty() { // When no deltas were received, done() uses the reply as the text to // record. With no campaign attached, the method just returns // without panicking. let mut bus = TurnBus::new(); let listener = CampaignListener::new(None); bus.add(Box::new(listener));
bus.done("You wake.");}
#[test]fn campaign_listener_cancelled_is_a_noop() { let mut bus = TurnBus::new(); let listener = CampaignListener::new(None); bus.add(Box::new(listener));
bus.cancelled("partial reply");}
#[test]fn campaign_listener_failed_is_a_noop() { let mut bus = TurnBus::new(); let listener = CampaignListener::new(None); bus.add(Box::new(listener));
bus.failed("something broke");}
#[test]fn screen_listener_failed_sends_failed_event() { let (sender, receiver) = mpsc::channel(); let cancel = Arc::new(AtomicBool::new(false)); let mut bus = TurnBus::new(); bus.add(Box::new(ScreenListener::new(sender, cancel)));
bus.failed("something broke");
assert_eq!( receiver.recv().unwrap(), TurnEvent::Failed("something broke".to_string()) );}
#[test]fn default_listener_methods_do_not_panic() { // A listener that overrides nothing — exercises every default impl. struct NoopListener; impl Listener for NoopListener {}
let mut bus = TurnBus::new(); bus.add(Box::new(NoopListener));
assert_eq!(bus.delta("text"), ControlFlow::Continue(()));
let display: Text<'static> = Text::raw("roll"); assert_eq!(bus.tool(Some(&display)), ControlFlow::Continue(())); assert_eq!(bus.tool(None), ControlFlow::Continue(()));
bus.done("finished"); bus.cancelled("partial"); bus.failed("error");}
#[test]fn campaign_listener_does_not_replay_earlier_turns_narration() { use crate::campaign::Campaign; use crate::campaign::GameTime;
let dir = tempfile::TempDir::new().unwrap(); let campaign = Campaign::open(dir.path()).unwrap(); let mut bus = TurnBus::new(); bus.add(Box::new(CampaignListener::new(Some(campaign))));
// First turn. bus.turn_start("I wake up.", Speaker::Player); let _ = bus.delta("You wake in a cold cell."); bus.done("You wake in a cold cell.");
// Second turn, later that day. Without the reset in turn_start, this // would re-append the first turn's narration before its own. let campaign = Campaign::open(dir.path()).unwrap(); campaign .mark(GameTime::parse("#d1-0900").unwrap(), "time passes", None) .unwrap(); bus.turn_start("I sit up.", Speaker::Player); let _ = bus.delta("The bars are iron."); bus.done("The bars are iron.");
let campaign = Campaign::open(dir.path()).unwrap(); let sections = campaign.transcript_entries().unwrap(); let bodies: Vec<String> = sections.into_iter().map(|e| e.body).collect(); assert_eq!(bodies.len(), 2); assert_eq!(bodies[0], "player> I wake up.\n\nYou wake in a cold cell."); assert_eq!(bodies[1], "player> I sit up.\n\nThe bars are iron.");}