Something went wrong. Try again.
A 5e storytelling engine with an LLM DM
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152//! Tests for the clock section: the current campaign clock and the//! turns-since-last-`mark` nudge, stated ahead of the session brief in//! every turn's system prompt.
use super::tests::{dm_for, dm_with_campaign, fake_server, sent_messages, sse_response};use crate::bus::Speaker;
use serde_json::json;use tempfile::TempDir;
/// The system prompt of the next request the fake server captured.fn next_system_prompt( requests: &std::sync::mpsc::Receiver<super::tests::CapturedRequest>,) -> String { sent_messages(&requests.recv().unwrap())[0]["content"] .as_str() .unwrap() .to_string()}
/// An SSE response for one round that calls `mark` with `time`, and/// narrates nothing, so the turn goes another round.fn mark_call_response(time: &str) -> Vec<u8> { let arguments = json!({ "time": time, "event": "Time passes.", "visibility": "secret", }) .to_string(); let body = format!( "data: {{\"choices\":[{{\"delta\":{{\"tool_calls\":[{{\"index\":0,\"id\":\"call_1\",\"function\":{{\"name\":\"mark\",\"arguments\":{}}}}}]}},\"finish_reason\":null}}]}}\n\n\ data: [DONE]\n\n", json!(arguments) ); sse_response(&body)}
#[test]fn a_second_turns_prompt_carries_the_campaign_clock() { let world = TempDir::new().unwrap(); let (url, requests, server) = fake_server(vec![ sse_response("data: [DONE]\n\n"), sse_response("data: [DONE]\n\n"), ]); let mut dm = dm_with_campaign(url, &world);
dm.turn(Speaker::Player, "I look around.").unwrap(); dm.turn(Speaker::Player, "I look again.").unwrap();
server.join().unwrap(); requests.recv().unwrap(); let prompt = next_system_prompt(&requests);
assert!(prompt.contains("The campaign clock reads #d1-0000."));}
#[test]fn a_mid_turn_mark_moves_the_clock_line_on_the_next_turn() { let world = TempDir::new().unwrap(); let (url, requests, server) = fake_server(vec![ mark_call_response("#d2-0830"), sse_response( "data: {\"choices\":[{\"delta\":{\"content\":\"Morning comes.\"},\"finish_reason\":null}]}\n\n\ data: [DONE]\n\n", ), sse_response("data: [DONE]\n\n"), ]); let mut dm = dm_with_campaign(url, &world);
dm.turn(Speaker::Player, "I rest until dawn.").unwrap(); dm.turn(Speaker::Player, "I look around.").unwrap();
server.join().unwrap(); requests.recv().unwrap(); requests.recv().unwrap(); let prompt = next_system_prompt(&requests);
assert!(prompt.contains("The campaign clock reads #d2-0830."));}
#[test]fn a_dm_with_no_campaign_carries_no_clock_line() { let (url, requests, server) = fake_server(vec![ sse_response("data: [DONE]\n\n"), sse_response("data: [DONE]\n\n"), ]); let mut dm = dm_for(url);
dm.turn(Speaker::Player, "I look around.").unwrap(); dm.turn(Speaker::Player, "I look again.").unwrap();
server.join().unwrap(); requests.recv().unwrap(); let prompt = next_system_prompt(&requests);
assert!(!prompt.contains("The campaign clock reads"));}
#[test]fn turns_without_a_mark_nudge_the_dm_to_mark() { let world = TempDir::new().unwrap(); let (url, requests, server) = fake_server(vec![ sse_response("data: [DONE]\n\n"), sse_response("data: [DONE]\n\n"), sse_response("data: [DONE]\n\n"), ]); let mut dm = dm_with_campaign(url, &world);
dm.turn(Speaker::Player, "I look around.").unwrap(); dm.turn(Speaker::Player, "I look again.").unwrap(); dm.turn(Speaker::Player, "I keep looking.").unwrap();
server.join().unwrap(); requests.recv().unwrap(); requests.recv().unwrap(); let prompt = next_system_prompt(&requests);
assert!(prompt.contains("It's been 2 turns since you last marked time."));}
#[test]fn a_mark_resets_the_turns_since_mark_nudge() { let world = TempDir::new().unwrap(); let (url, requests, server) = fake_server(vec![ mark_call_response("#d2-0830"), sse_response( "data: {\"choices\":[{\"delta\":{\"content\":\"Morning comes.\"},\"finish_reason\":null}]}\n\n\ data: [DONE]\n\n", ), sse_response("data: [DONE]\n\n"), sse_response("data: [DONE]\n\n"), ]); let mut dm = dm_with_campaign(url, &world);
dm.turn(Speaker::Player, "I rest until dawn.").unwrap(); dm.turn(Speaker::Player, "I look around.").unwrap(); dm.turn(Speaker::Player, "I look again.").unwrap();
server.join().unwrap(); requests.recv().unwrap(); requests.recv().unwrap();
// The turn that follows the mark carries no nudge yet. let fresh = next_system_prompt(&requests); assert!(fresh.contains("The campaign clock reads #d2-0830.")); assert!(!fresh.contains("since you last marked time"));
// One completed turn without a mark starts the count again. let nudged = next_system_prompt(&requests); assert!(nudged.contains("It's been 1 turn since you last marked time."));}