Something went wrong. Try again.
A 5e storytelling engine with an LLM DM
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251//! Tests for the heading-based transcript.
use super::*;use crate::campaign::day_file;use std::fs;
fn t() -> (Transcript, tempfile::TempDir) { let dir = tempfile::TempDir::new().unwrap(); let tr = Transcript::new(dir.path().join("x")); (tr, dir)}
#[test]fn append_writes_the_day_title_and_a_section_for_a_new_file() { let (tr, _dir) = t();
tr.append(GameTime::parse("#d1-0830").unwrap(), "player> I wake up.") .unwrap();
let text = fs::read_to_string(day_path(tr.dir(), 1)).unwrap(); assert_eq!(text, "# Day 1\n\n## #d1-0830\nplayer> I wake up.\n\n");}
#[test]fn append_to_the_same_section_does_not_repeat_the_heading() { let (tr, _dir) = t();
tr.append( GameTime::parse("#d1-0830").unwrap(), "player> I look around.", ) .unwrap(); tr.append( GameTime::parse("#d1-0830").unwrap(), "You find dusty cobwebs.", ) .unwrap();
let text = fs::read_to_string(day_path(tr.dir(), 1)).unwrap(); assert!( text.matches("## #d1-0830").count() == 1, "expected one heading, got: {text}" ); assert!(text.contains("player> I look around.")); assert!(text.contains("You find dusty cobwebs."));}
#[test]fn a_time_change_opens_a_new_section() { let (tr, _dir) = t();
tr.append(GameTime::parse("#d1-0830").unwrap(), "player> I wake up.") .unwrap(); tr.append(GameTime::parse("#d1-1200").unwrap(), "Noon arrives.") .unwrap();
let text = fs::read_to_string(day_path(tr.dir(), 1)).unwrap(); assert!(text.matches("## ").count() == 2); assert!(text.contains("## #d1-0830")); assert!(text.contains("## #d1-1200"));}
#[test]fn read_sections_returns_every_section_in_order() { let (tr, _dir) = t();
tr.append(GameTime::parse("#d1-0830").unwrap(), "player> I wake up.") .unwrap(); tr.append(GameTime::parse("#d1-1200").unwrap(), "Noon arrives.") .unwrap();
let sections = tr.read_sections().unwrap(); assert_eq!(sections.len(), 2); assert_eq!(sections[0].time.to_string(), "#d1-0830"); assert!(sections[0].body.contains("player> I wake up.")); assert_eq!(sections[1].time.to_string(), "#d1-1200"); assert!(sections[1].body.contains("Noon arrives."));}
#[test]fn a_markdown_heading_in_a_block_is_not_mistaken_for_a_new_section() { let (tr, _dir) = t();
tr.append( GameTime::parse("#d1-0830").unwrap(), "## The Cellar\nDamp stone stairs descend into the dark.", ) .unwrap();
let sections = tr.read_sections().unwrap(); assert_eq!(sections.len(), 1); assert_eq!(sections[0].time.to_string(), "#d1-0830"); assert!(sections[0].body.contains("## The Cellar")); assert!(sections[0].body.contains("Damp stone stairs"));}
#[test]fn a_markdown_heading_in_a_block_does_not_confuse_the_next_append() { let (tr, _dir) = t();
tr.append( GameTime::parse("#d1-0830").unwrap(), "## The Cellar\nDamp stairs.", ) .unwrap(); tr.append(GameTime::parse("#d1-0830").unwrap(), "You proceed further.") .unwrap();
let sections = tr.read_sections().unwrap(); assert_eq!(sections.len(), 1); assert!(sections[0].body.contains("## The Cellar")); assert!(sections[0].body.contains("You proceed further."));}
#[test]fn append_to_a_new_day_starts_a_new_day_file() { let (tr, _dir) = t();
tr.append(GameTime::parse("#d1-0830").unwrap(), "player> first day.") .unwrap(); tr.append(GameTime::parse("#d2-0100").unwrap(), "next day.") .unwrap();
assert!(day_path(tr.dir(), 2).exists()); let text = fs::read_to_string(day_path(tr.dir(), 2)).unwrap(); assert!(text.contains("# Day 2")); assert!(text.contains("## #d2-0100")); assert!(text.contains("next day."));}
#[test]fn a_dir_that_cannot_be_created_fails_append() { let dir = tempfile::TempDir::new().unwrap(); let parent = dir.path().join("blob"); std::fs::write(&parent, "x").unwrap(); let tr = Transcript::new(parent.join("x"));
let error = tr .append(GameTime::parse("#d1-0000").unwrap(), "x") .unwrap_err();
assert!(error.contains("cannot create"));}
#[test]fn read_sections_fails_when_a_day_file_is_a_directory() { let dir = tempfile::TempDir::new().unwrap(); let tr = Transcript::new(dir.path().join("x")); day_file::ensure_dir(tr.dir()).unwrap(); std::fs::create_dir(day_path(tr.dir(), 1)).unwrap();
let error = tr.read_sections().unwrap_err();
assert!(error.contains("cannot read"));}
#[test]fn append_fails_when_the_day_file_cannot_be_opened_for_writing() { use std::os::unix::fs::PermissionsExt; let dir = tempfile::TempDir::new().unwrap(); let tr = Transcript::new(dir.path().join("x")); // Write a valid file first so the read branch is taken. tr.append(GameTime::parse("#d1-0830").unwrap(), "first block.") .unwrap(); let path = day_path(tr.dir(), 1); assert!(path.exists()); // Chmod the file read-only; the next append reads it fine but then // fails to reopen it for the append itself. std::fs::set_permissions(&path, PermissionsExt::from_mode(0o400)).unwrap();
let error = tr .append(GameTime::parse("#d1-0830").unwrap(), "second block") .unwrap_err();
assert!(error.contains("cannot append"));}
#[test]fn append_normalizes_a_hand_removed_trailing_blank_line() { let (tr, _dir) = t(); let path = day_path(tr.dir(), 1); day_file::ensure_dir(tr.dir()).unwrap(); // A hand-edited file: the blank line the transcript always ends in // was trimmed away, leaving a single trailing newline. fs::write(&path, "# Day 1\n\n## #d1-0830\nplayer> I wake up.\n").unwrap();
tr.append( GameTime::parse("#d1-0830").unwrap(), "You find dusty cobwebs.", ) .unwrap();
let text = fs::read_to_string(&path).unwrap(); assert_eq!( text, "# Day 1\n\n## #d1-0830\nplayer> I wake up.\n\nYou find dusty cobwebs.\n\n" );}
#[test]fn append_normalizes_a_hand_edited_file_with_no_trailing_newline_at_all() { let (tr, _dir) = t(); let path = day_path(tr.dir(), 1); day_file::ensure_dir(tr.dir()).unwrap(); // A hand-edited file: both trailing newlines are gone, so the file // ends mid-line. fs::write(&path, "# Day 1\n\n## #d1-0830\nplayer> I wake up.").unwrap();
tr.append( GameTime::parse("#d1-0830").unwrap(), "You find dusty cobwebs.", ) .unwrap();
let text = fs::read_to_string(&path).unwrap(); assert_eq!( text, "# Day 1\n\n## #d1-0830\nplayer> I wake up.\n\nYou find dusty cobwebs.\n\n" );}
#[test]fn append_does_not_rewrite_bytes_already_on_disk() { let (tr, _dir) = t(); tr.append(GameTime::parse("#d1-0830").unwrap(), "first block.") .unwrap(); let path = day_path(tr.dir(), 1); let before = fs::read_to_string(&path).unwrap();
tr.append(GameTime::parse("#d1-1200").unwrap(), "second block.") .unwrap();
let after = fs::read_to_string(&path).unwrap(); assert!(after.starts_with(&before));}
#[test]fn append_fails_when_the_existing_file_cannot_be_read() { let dir = tempfile::TempDir::new().unwrap(); let tr = Transcript::new(dir.path().join("x")); day_file::ensure_dir(tr.dir()).unwrap(); let path = day_path(tr.dir(), 1); std::fs::create_dir(&path).unwrap(); // path.exists() is true (it's a directory), so append takes the read // branch and read_to_string fails. let error = tr .append(GameTime::parse("#d1-0830").unwrap(), "block") .unwrap_err(); assert!(error.contains("cannot read"));}