//! 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, ) -> 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 { 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.")); }