Something went wrong. Try again.
HEXWORK - A Lantern in the Deep: 3D ASCII terminal roguelike RPG in Rust.
Entirely designed and built by Muse Spark 1.3 to evaluate if I like the model.
Something went wrong. Try again.
34 kB · 948 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949//! Stage3c2 production-process gates. Historical flat-vault and actorless//! component replay tests were removed because they are not production evidence.
#[path = "phase12/feedback.rs"]pub mod feedback;#[path = "phase12/inventory.rs"]mod inventory;#[path = "phase12/keymap.rs"]mod keymap;#[path = "phase11/negative.rs"]mod negative;#[path = "phase12/presentation.rs"]mod presentation;#[path = "phase11/recovery.rs"]mod recovery;mod support;#[path = "phase12/v4_compat.rs"]mod v4_compat;
use hexwork_core::action::CommittedAction;use hexwork_core::character::{Ancestry, Calling};use hexwork_core::modes::Mode;use hexwork_core::runtime::ActorRole;use hexwork_save::artifact::ArtifactStore;use hexwork_save::{ decode_replay, decode_run, decode_vault, encode_replay, encode_vault, read_file_bounded, InitialLineage, ProductionReplay, RunId, RunIdentity, SessionEvent, MAX_COMPRESSED_BYTES, MAX_VAULT_BYTES,};use support::{ first_two_deep_arrivals, grate_mother_death_script, story_grate_mother_death_script_after_rollback, Harness,};
fn field<'a>(record: &'a serde_json::Value, name: &str) -> &'a serde_json::Value { record .get(name) .unwrap_or_else(|| panic!("missing {name}: {record}"))}
#[test]#[cfg(any(target_os = "linux", target_os = "macos"))]fn binary_reports_real_store_lock_contention_then_succeeds_after_release() { let harness = Harness::new("parent-store-lock-holder"); let script = harness.script("quit.commands", "quit\n"); let (parent_store_lock_holder, _) = ArtifactStore::open(&harness.data).unwrap();
let contended = Harness::run(harness.managed(&script)); assert_eq!( contended.status.code(), Some(6), "contention stderr: {}", String::from_utf8_lossy(&contended.stderr) );
drop(parent_store_lock_holder); let released = Harness::run(harness.managed(&script)); assert!( released.status.success(), "released stderr: {}", String::from_utf8_lossy(&released.stderr) );}
#[test]fn binary_presentation_controls_follow_player_without_mutating_simulation() { let harness = Harness::new("presentation"); let script = harness.script( "presentation.commands", "observe state\ninput west\nobserve state\ninput map\nobserve state\ninput slice-down\nobserve state\ninput map\ninput camera\nobserve state\nquit\n", ); let output = Harness::run(harness.managed(&script)); assert!( output.status.success(), "presentation stderr: {}", String::from_utf8_lossy(&output.stderr) ); let observations = Harness::records(&output) .into_iter() .filter(|record| field(record, "record_kind") == "observation") .collect::<Vec<_>>(); assert_eq!(observations.len(), 5); let detail = |index: usize| field(&observations[index], "detail").as_str().unwrap(); let hash = |index: usize| { field(&observations[index], "canonical_hash") .as_u64() .unwrap() };
assert!(detail(0).contains("view_mode=FirstPerson")); assert!(detail(0).contains("view_eye=21.500,31.500,9.600")); // One `input west` is a single paced movement sample: 3000 mm/s over one // 100 ms world tick, i.e. a 0.300 m step inside the cell, not a whole-cell // teleport. Observed: the eye shifts 21.500 -> 21.200 and costs one tick. assert!(detail(1).contains("view_mode=FirstPerson")); assert!(detail(1).contains("view_eye=21.200,31.500,9.600")); assert_ne!(hash(0), hash(1), "legal movement mutates the simulation");
assert!(detail(2).contains("view_mode=TopTactical")); assert!(detail(2).contains("view_eye=21.200,31.500,32.000")); assert!(detail(2).contains("tactical_z=8")); assert_eq!(hash(1), hash(2), "opening map is observational");
assert!(detail(3).contains("view_mode=TopTactical")); assert!(detail(3).contains("tactical_z=7")); assert_eq!(hash(2), hash(3), "tactical slice is observational");
assert!(detail(4).contains("view_mode=ChaseOrbit")); assert_eq!(hash(3), hash(4), "camera orbit is observational");}
#[test]fn binary_new_save_exit_load_preserves_hash_and_history() { let harness = Harness::new("resume"); let first_script = harness.script("first.commands", "input west\nobserve state\nsave\nquit\n"); let first = Harness::run(harness.managed(&first_script)); assert!( first.status.success(), "first stderr: {}", String::from_utf8_lossy(&first.stderr) ); let first_records = Harness::records(&first); let run_id = first_records .iter() .find(|record| field(record, "code") == "allocation-durable") .and_then(|record| field(record, "run_id").as_str()) .expect("allocation run ID") .to_owned(); let first_state = first_records .iter() .find(|record| field(record, "record_kind") == "observation") .expect("state observation"); let first_hash = field(first_state, "canonical_hash").as_u64().unwrap(); let first_events = field(first_state, "count").as_u64().unwrap(); assert_eq!(first_events, 1); assert!( field(first_state, "detail") .as_str() .unwrap() .contains("view_mode=FirstPerson;view_eye=21.200,31.500,9.600"), "move view: {}", field(first_state, "detail") );
let second_script = harness.script("second.commands", "observe state\nquit\n"); let mut load = harness.managed(&second_script); load.arg("--load").arg(&run_id); let second = Harness::run(load); assert!( second.status.success(), "load stderr: {}", String::from_utf8_lossy(&second.stderr) ); let second_records = Harness::records(&second); let second_state = second_records .iter() .find(|record| field(record, "record_kind") == "observation") .expect("loaded state observation"); assert_eq!( field(second_state, "canonical_hash").as_u64(), Some(first_hash) ); assert_eq!(field(second_state, "count").as_u64(), Some(first_events)); assert!( field(second_state, "detail") .as_str() .unwrap() .contains("view_mode=FirstPerson;view_eye=21.200,31.500,9.600"), "loaded view: {}", field(second_state, "detail") ); assert!(harness.data.join("lineage-vault.json").is_file()); assert!(harness .data .join("runs") .join(&run_id) .join("run.save") .is_file()); assert!(!harness.cwd.join("lineage-vault.json").exists());}
#[test]fn classic_real_death_commits_demo_vault_receipt_before_retirement() { let harness = Harness::new("classic-death"); let pack = hexwork_content::RuntimePack::load(&harness.content).unwrap(); let vault_path = harness.data.join("lineage-vault.json");
// The generated route ends in four normal reciprocal-melee confirms. Split at // its final command instead of assuming an opaque command index: the prefix // process saves the authoritative pack immediately before the known lethal // fourth exchange, then the loaded process supplies exactly that omitted input. let mut prefix = grate_mother_death_script(223); assert!( prefix.ends_with("input confirm\n"), "death route must end at the verified lethal confirm" ); prefix.truncate(prefix.len() - "input confirm\n".len()); prefix.push_str("save\nquit\n"); let prefix_script = harness.script("prelethal.commands", &prefix); let mut prefix_command = harness.managed(&prefix_script); prefix_command.arg("--seed").arg("223"); let prefix_output = Harness::run(prefix_command); assert!( prefix_output.status.success(), "prelethal stderr: {}", String::from_utf8_lossy(&prefix_output.stderr) ); let prefix_records = Harness::records(&prefix_output); let transition_states = prefix_records .iter() .filter(|record| field(record, "record_kind") == "observation") .collect::<Vec<_>>(); assert_eq!(transition_states.len(), 2); for (record, arrival) in transition_states.iter().zip(first_two_deep_arrivals(223)) { let expected = format!( "view_mode=FirstPerson;view_eye={:.3},{:.3},{:.3}", arrival[0] as f32 + 0.5, arrival[1] as f32 + 0.5, arrival[2] as f32 + 1.6 ); assert!( field(record, "detail") .as_str() .unwrap() .contains(&expected), "transition view: {} expected {expected}", field(record, "detail") ); } let run_id = prefix_records .iter() .find(|record| field(record, "code") == "allocation-durable") .and_then(|record| field(record, "run_id").as_str()) .unwrap() .to_owned(); let parsed_run_id = run_id.parse::<RunId>().unwrap(); let run_dir = harness.data.join("runs").join(&run_id); let prelethal_bytes = read_file_bounded(run_dir.join("run.save"), MAX_COMPRESSED_BYTES).unwrap(); let prelethal = decode_run(&prelethal_bytes, pack.hash()).unwrap(); assert_eq!(prelethal.identity.run_id, parsed_run_id); let player_id = prelethal .current .actors .iter() .find(|actor| { prelethal .current .identity(actor.id) .is_some_and(|identity| identity.role == ActorRole::Player) }) .expect("one prelethal Player") .id; let prelethal_pack = prelethal .current .packs .get(&player_id) .expect("prelethal authoritative Player pack") .clone(); assert!(!prelethal_pack.items.is_empty()); let prelethal_events = prelethal.events.len();
let lethal_script = harness.script("lethal.commands", "input confirm\n"); let mut lethal_command = harness.managed(&lethal_script); lethal_command.arg("--load").arg(&run_id); let output = Harness::run(lethal_command); assert!( output.status.success(), "death stderr: {}", String::from_utf8_lossy(&output.stderr) ); let records = Harness::records(&output); let codes = records .iter() .filter_map(|record| field(record, "code").as_str()) .collect::<Vec<_>>(); let positions = [ "terminal-snapshot-durable", "pending-death-durable", "replay-durable", "epitaph-durable", "vault-death-durable", "committed-death-durable", "run-retired-durable", "pending-retired-durable", ] .map(|code| { codes .iter() .position(|candidate| *candidate == code) .unwrap_or_else(|| panic!("missing {code}: {codes:?}")) }); assert!(positions.windows(2).all(|pair| pair[0] < pair[1])); assert!(!run_dir.join("run.save").exists()); assert!(!run_dir.join("death.pending").exists()); assert!(run_dir.join("death.committed").is_file()); let replay_path = run_dir.join("replay.hexdemo"); let committed_path = run_dir.join("death.committed"); let epitaph_path = run_dir.join("epitaph.txt"); assert!(replay_path.is_file()); assert!(epitaph_path.is_file()); let replay_bytes = read_file_bounded(&replay_path, MAX_COMPRESSED_BYTES).unwrap(); let replay = decode_replay(&replay_bytes, pack.hash()).unwrap(); assert_eq!(replay.identity.run_id, parsed_run_id); assert_eq!(replay.events.len(), prelethal_events + 1); assert_eq!( &replay.events[..prelethal_events], prelethal.events.as_slice(), "death archive must retain the complete prelethal history" );
let vault_bytes = read_file_bounded(&vault_path, MAX_VAULT_BYTES).unwrap(); let vault = decode_vault(&vault_bytes).unwrap(); assert!(vault.applied_deaths.contains_key(&parsed_run_id)); let prelethal_knowledge = prelethal.current.knowledge(); assert_eq!( vault.current.mapped_regions, prelethal_knowledge .mapped_regions .iter() .cloned() .collect::<Vec<_>>() ); assert_eq!( vault.current.recipes, prelethal_knowledge .recipes .iter() .cloned() .collect::<Vec<_>>() ); assert_eq!( vault.current.translated_hymns, prelethal_knowledge .translated_hymns .iter() .cloned() .collect::<Vec<_>>() ); assert_eq!( vault.current.identified_item_templates, prelethal .current .known_templates() .map(str::to_owned) .collect::<Vec<_>>() ); assert!( vault.current.recipes.is_empty(), "normal route learns no recipe" ); assert!( vault.current.translated_hymns.is_empty(), "normal route translates no hymn" ); assert!( vault.current.identified_item_templates.is_empty(), "normal route performs no identification action" );
// Core reciprocal melee mutates actors/causes and creates only the synthetic // corpse; it does not transform the Player's pre-existing Pack. Production // death extraction excludes template `corpse`, so every resulting lot must // exactly project one item from this actual prelethal managed save. let eligible = prelethal_pack .items .values() .filter(|item| item.template != "corpse") .collect::<Vec<_>>(); let death_lots = vault .current .auction_lots .iter() .filter(|lot| lot.origin_run_id == parsed_run_id) .collect::<Vec<_>>(); assert_eq!(death_lots.len(), eligible.len()); for (lot, item) in death_lots.iter().zip(&eligible) { assert_eq!(lot.lot_id.origin_run_id(), parsed_run_id); assert_eq!(lot.lot_id.origin_item_id(), item.id); assert_eq!(lot.origin_run_id, parsed_run_id); assert_eq!(lot.origin_item_id, item.id); assert_eq!(lot.template_id, item.template); assert_eq!(lot.material, item.material); assert_eq!(lot.identified, item.identified); assert_eq!(lot.cursed, item.cursed); assert_eq!(lot.blessed, item.blessed); assert_eq!(lot.condition, item.condition); assert_eq!(lot.silvered, item.silvered); assert_eq!(lot.illegal, item.illegal); assert_eq!(lot.tags, item.tags); assert_eq!(lot.fuel, item.fuel); assert_eq!(lot.wet_ticks, item.wet_ticks); let base = pack.item(&item.template).unwrap().base_price_cu; assert_eq!(lot.base_price_cu, base); assert_eq!( lot.buyback_price_cu, hexwork_core::services::auction_buyback(base) ); } assert!(death_lots.iter().all(|lot| lot.template_id != "corpse")); let lowest = eligible.first().expect("eligible lowest-ID item"); let ghost = vault .current .ghosts .iter() .find(|ghost| ghost.origin_run_id == parsed_run_id) .expect("death ghost"); assert_eq!(ghost.item_template_id, lowest.template); let epitaph = String::from_utf8(std::fs::read(&epitaph_path).unwrap()).unwrap(); let causal_warning = epitaph .lines() .rev() .find_map(|line| { let (_, cause) = line.split_once(' ')?; cause.starts_with("death:").then(|| cause.to_owned()) }) .or_else(|| { epitaph .lines() .rev() .find_map(|line| line.split_once(' ').map(|(_, cause)| cause.to_owned())) }) .expect("terminal epitaph cause"); assert_eq!(ghost.warning, causal_warning);
let committed_bytes = std::fs::read(&committed_path).unwrap(); let epitaph_bytes = std::fs::read(&epitaph_path).unwrap(); let stable_artifacts = || { ( std::fs::read(&vault_path).unwrap(), std::fs::read(&committed_path).unwrap(), std::fs::read(&replay_path).unwrap(), std::fs::read(&epitaph_path).unwrap(), ) }; let committed_snapshot = stable_artifacts();
let refused_script = harness.script("retired-load.commands", "observe state\n"); let mut refused_command = harness.managed(&refused_script); refused_command.arg("--load").arg(&run_id); let refused = Harness::run(refused_command); assert_eq!(refused.status.code(), Some(3)); assert!( Harness::records(&refused).iter().all(|record| { field(record, "record_kind") != "observation" && field(record, "record_kind") != "action" }), "retired load must not play or observe" ); assert!( String::from_utf8_lossy(&refused.stderr).contains(&format!( "recovery-conflict: {run_id}: death marker blocks living operation" )), "retired stderr: {}", String::from_utf8_lossy(&refused.stderr) ); assert_eq!(stable_artifacts(), committed_snapshot);
for attempt in 0..2 { let recovered = Harness::run({ let mut recover = harness.command(); recover .arg("--data-dir") .arg(&harness.data) .arg("--content-root") .arg(&harness.content) .arg("--recover"); recover }); assert!( recovered.status.success(), "recover {attempt} stderr: {}", String::from_utf8_lossy(&recovered.stderr) ); let recovery_records = Harness::records(&recovered); assert!(recovery_records .iter() .any(|record| field(record, "code") == "recovery-complete")); assert!(recovery_records.iter().all(|record| { !matches!( field(record, "code").as_str(), Some("vault-death-durable") | Some("committed-death-durable") | Some("replay-durable") | Some("epitaph-durable") ) })); assert_eq!(stable_artifacts(), committed_snapshot); let recovered_vault = decode_vault(&std::fs::read(&vault_path).unwrap()).unwrap(); assert_eq!(recovered_vault.current.ghosts, vault.current.ghosts); assert_eq!( recovered_vault.current.auction_lots, vault.current.auction_lots ); assert_eq!(recovered_vault.applied_deaths, vault.applied_deaths); }
// Standalone archive replay reconstructs from its recorded immutable lineage; // it neither consults nor writes the external vault. Exercise both a different // valid vault and no vault, restoring the committed bytes before managed calls. let mut changed_vault = vault.clone(); changed_vault.next_run_sequence += 1; std::fs::write(&vault_path, encode_vault(&changed_vault).unwrap()).unwrap(); let run_replay = || { Harness::run({ let mut replay_command = harness.command(); replay_command .arg("--content-root") .arg(&harness.content) .arg("--replay") .arg(&replay_path); replay_command }) }; let changed_replay = run_replay(); assert!( changed_replay.status.success(), "changed-vault replay stderr: {}", String::from_utf8_lossy(&changed_replay.stderr) ); assert!(String::from_utf8_lossy(&changed_replay.stdout).contains("verified event-text replay")); assert_eq!( std::fs::read(&vault_path).unwrap(), encode_vault(&changed_vault).unwrap(), "archive replay is read-only" ); std::fs::write(&vault_path, &vault_bytes).unwrap(); std::fs::remove_file(&vault_path).unwrap(); let removed_replay = run_replay(); assert!( removed_replay.status.success(), "removed-vault replay stderr: {}", String::from_utf8_lossy(&removed_replay.stderr) ); assert!(String::from_utf8_lossy(&removed_replay.stdout).contains("verified event-text replay")); assert!( !vault_path.exists(), "archive replay must not create a store" ); std::fs::write(&vault_path, &vault_bytes).unwrap(); assert_eq!(std::fs::read(&committed_path).unwrap(), committed_bytes); assert_eq!(std::fs::read(&epitaph_path).unwrap(), epitaph_bytes);
let descendant_script = harness.script( "descendant.commands", "observe knowledge\nobserve inventory\nobserve state\nsave\nquit\n", ); let mut descendant_command = harness.managed(&descendant_script); descendant_command .arg("--seed") .arg("224") .arg("--calling") .arg("1"); let descendant = Harness::run(descendant_command); assert!( descendant.status.success(), "descendant stderr: {}", String::from_utf8_lossy(&descendant.stderr) ); let descendant_records = Harness::records(&descendant); let descendant_run_id = descendant_records .iter() .find(|record| field(record, "code") == "allocation-durable") .and_then(|record| field(record, "run_id").as_str()) .unwrap(); assert_ne!(descendant_run_id, run_id); assert_eq!( descendant_run_id.parse::<RunId>().unwrap().sequence(), parsed_run_id.sequence() + 1, "descendant allocation is sequential" ); let knowledge = descendant_records .iter() .find(|record| { record .get("detail") .is_some_and(|value| value == "knowledge") }) .unwrap(); let knowledge_entries = field(knowledge, "entries").as_array().unwrap(); assert!(knowledge_entries.iter().any(|entry| entry == "map:deep:1")); assert!(knowledge_entries.iter().any(|entry| entry == "map:deep:2"));
let descendant_save = harness .data .join("runs") .join(descendant_run_id) .join("run.save"); let descendant_decoded = decode_run( &read_file_bounded(descendant_save, MAX_COMPRESSED_BYTES).unwrap(), pack.hash(), ) .unwrap(); let descendant_player = descendant_decoded .current .actors .iter() .find(|actor| { descendant_decoded .current .identity(actor.id) .is_some_and(|identity| identity.role == ActorRole::Player) }) .expect("one descendant Player") .id; let kit = hexwork_core::character::kit(Calling::Ratcatcher); let mut actual_templates = descendant_decoded.current.packs[&descendant_player] .items .values() .map(|item| item.template.clone()) .collect::<Vec<_>>(); actual_templates.sort(); let mut expected_templates = kit.items.into_iter().map(str::to_owned).collect::<Vec<_>>(); expected_templates.sort(); assert_eq!( actual_templates, expected_templates, "auction lots did not become live descendant gear" ); let wallet = descendant_decoded .current .wallet(descendant_player) .expect("descendant wallet"); assert_eq!(wallet.copper(), kit.stipend_cu); assert_eq!(wallet.scrip(), 0); assert!(descendant_decoded .current .chapel_debt(descendant_player) .is_none()); assert_eq!( descendant_decoded.identity.initial_lineage, vault.current, "descendant records the exact committed lineage snapshot" );}
#[test]fn story_real_death_rolls_back_saves_resumes_and_repeats() { let harness = Harness::new("story-death"); let mut body = grate_mother_death_script(223); body.push_str("observe state\nobserve knowledge\n"); body.push_str(&story_grate_mother_death_script_after_rollback(223)); body.push_str("observe state\nquit\n"); let script = harness.script("story.commands", &body); let mut command = harness.managed(&script); command.arg("--story").arg("--seed").arg("223"); let output = Harness::run(command); assert!( output.status.success(), "story stderr: {}", String::from_utf8_lossy(&output.stderr) ); let records = Harness::records(&output); let run_id = records .iter() .find(|record| field(record, "code") == "allocation-durable") .and_then(|record| field(record, "run_id").as_str()) .unwrap() .to_owned(); let states = records .iter() .filter(|record| { field(record, "record_kind") == "observation" && record .get("detail") .and_then(serde_json::Value::as_str) .is_some_and(|detail| detail.contains("location=Town(\"milthollow\")")) }) .collect::<Vec<_>>(); assert_eq!(states.len(), 2); assert!(field(states[0], "detail") .as_str() .unwrap() .contains("location=Town(\"milthollow\");chapel_debt=1")); assert!(field(states[1], "detail") .as_str() .unwrap() .contains("location=Town(\"milthollow\");chapel_debt=1")); let town = hexwork_worldgen::milthollow(223); let chapel = town .interactions .iter() .find(|interaction| { matches!( &interaction.kind, hexwork_worldgen::InteractionKind::Service { id } if id == "chapel" ) }) .unwrap(); let chapel_eye = format!( "view_mode=FirstPerson;view_eye={:.3},{:.3},{:.3}", chapel.access[0] as f32 + 0.5, chapel.access[1] as f32 + 0.5, chapel.access[2] as f32 + 1.6 ); assert!(field(states[0], "detail") .as_str() .unwrap() .contains(&chapel_eye)); assert!(field(states[1], "detail") .as_str() .unwrap() .contains(&chapel_eye)); assert!( field(states[1], "count").as_u64().unwrap() > field(states[0], "count").as_u64().unwrap() ); let run_dir = harness.data.join("runs").join(&run_id); assert!(run_dir.join("run.save").is_file()); for forbidden in [ "death.pending", "death.committed", "replay.hexdemo", "epitaph.txt", ] { assert!(!run_dir.join(forbidden).exists(), "Story wrote {forbidden}"); }
let resume_script = harness.script("resume-story.commands", "observe state\nquit\n"); let mut resume = harness.managed(&resume_script); resume.arg("--load").arg(&run_id).arg("--story"); let resumed = Harness::run(resume); assert!( resumed.status.success(), "resume stderr: {}", String::from_utf8_lossy(&resumed.stderr) ); let resumed_state = Harness::records(&resumed) .into_iter() .find(|record| field(record, "record_kind") == "observation") .unwrap(); assert_eq!( field(&resumed_state, "canonical_hash"), field(states[1], "canonical_hash") ); assert!(field(&resumed_state, "detail") .as_str() .unwrap() .contains("chapel_debt=1")); assert!(field(&resumed_state, "detail") .as_str() .unwrap() .contains(&chapel_eye));}
#[test]fn binary_replay_applies_actual_event_and_checks_terminal_hash() { let harness = Harness::new("replay"); let script = harness.script( "record.commands", "observe state\ninput wait\nobserve state\nquit\n", ); let output = Harness::run(harness.managed(&script)); assert!( output.status.success(), "record stderr: {}", String::from_utf8_lossy(&output.stderr) ); let records = Harness::records(&output); let run_id = records .iter() .find(|record| field(record, "code") == "allocation-durable") .and_then(|record| field(record, "run_id").as_str()) .unwrap() .parse::<RunId>() .unwrap(); let observations = records .iter() .filter(|record| field(record, "record_kind") == "observation") .collect::<Vec<_>>(); assert_eq!(observations.len(), 2); let initial_tick = field(observations[0], "tick").as_u64().unwrap(); let final_tick = field(observations[1], "tick").as_u64().unwrap(); let final_hash = field(observations[1], "canonical_hash").as_u64().unwrap(); assert_eq!(final_tick, initial_tick + 1); let vault = decode_vault( &read_file_bounded(harness.data.join("lineage-vault.json"), MAX_VAULT_BYTES).unwrap(), ) .unwrap(); let pack = hexwork_content::RuntimePack::load(&harness.content).unwrap(); let replay = ProductionReplay { identity: RunIdentity { seed: 140, mode: Mode::Classic, ancestry: Ancestry::Mudborn, calling: Calling::Lampbearer, display_name: "the delver".into(), content_hash: pack.hash().to_owned(), lineage_id: vault.lineage_id, run_id, initial_lineage: InitialLineage::default(), }, events: vec![SessionEvent::action( initial_tick, final_tick, CommittedAction::Wait, )], expected_final_tick: final_tick, expected_final_hash: final_hash, }; let replay_path = harness.root.join("actual.hexdemo"); std::fs::write(&replay_path, encode_replay(&replay).unwrap()).unwrap(); let replayed = Harness::run({ let mut command = harness.command(); command .arg("--content-root") .arg(&harness.content) .arg("--replay") .arg(&replay_path); command }); assert!( replayed.status.success(), "replay stderr: {}", String::from_utf8_lossy(&replayed.stderr) ); let text = String::from_utf8(replayed.stdout).unwrap(); assert!(text.contains("verified event-text replay")); assert!(text.contains(&format!("hash {final_hash:#x}")));
let mut corrupt = encode_replay(&replay).unwrap(); let last = corrupt.len() - 1; corrupt[last] ^= 1; std::fs::write(&replay_path, corrupt).unwrap(); let rejected = Harness::run({ let mut command = harness.command(); command .arg("--content-root") .arg(&harness.content) .arg("--replay") .arg(&replay_path); command }); assert_eq!(rejected.status.code(), Some(3)); assert!(rejected.stdout.is_empty(), "no raw replay fallback");}
#[test]fn binary_cli_and_content_errors_are_hard_and_classified() { let harness = Harness::new("errors"); let output = Harness::run({ let mut command = harness.command(); command.arg("--daily").arg("2023-02-29"); command }); assert_eq!(output.status.code(), Some(2));
let output = Harness::run({ let mut command = harness.command(); command .arg("--content-root") .arg(harness.root.join("missing")) .arg("--arena"); command }); assert_eq!( output.status.code(), Some(2), "arena rejects unrelated root option" );
let script = harness.script("quit.commands", "quit\n"); let output = Harness::run({ let mut command = harness.command(); command .arg("--data-dir") .arg(&harness.data) .arg("--content-root") .arg(harness.root.join("missing")) .arg("--headless-script") .arg(script); command }); assert_eq!(output.status.code(), Some(3)); assert!( !harness.data.exists(), "content failure precedes artifact writes" );}
#[test]fn binary_arena_seed_changes_hash_without_artifacts() { let harness = Harness::new("arena"); let first = Harness::run({ let mut command = harness.command(); command.arg("--arena").arg("--seed").arg("140"); command }); let second = Harness::run({ let mut command = harness.command(); command.arg("--arena").arg("--seed").arg("141"); command }); assert!(first.status.success() && second.status.success()); assert_ne!(first.stdout, second.stdout); assert!(!harness.data.exists());}
#[test]fn autosave_crosses_600_once_and_same_tick_does_not_repeat() { let harness = Harness::new("autosave"); let script = harness.script( "autosave.commands", "advance 599\nobserve state\nadvance 1\nobserve inventory\nobserve knowledge\nquit\n", ); let output = Harness::run(harness.managed(&script)); assert!( output.status.success(), "autosave stderr: {}", String::from_utf8_lossy(&output.stderr) ); let records = Harness::records(&output); let saves = records .iter() .filter(|record| field(record, "code") == "run-snapshot-durable") .collect::<Vec<_>>(); assert_eq!(saves.len(), 2, "initial publication plus one autosave"); assert_eq!(field(saves[1], "tick").as_u64(), Some(600)); assert_eq!( records .iter() .filter(|record| { field(record, "code") == "run-snapshot-durable" && field(record, "tick").as_u64() == Some(600) }) .count(), 1 );}
#[test]fn headless_syntax_and_work_limit_fail_before_offending_mutation() { let harness = Harness::new("limits"); let invalid = harness.script("invalid.commands", "set hp 999\n"); let output = Harness::run(harness.managed(&invalid)); assert_eq!(output.status.code(), Some(2)); assert!( !harness.data.exists(), "whole script parsed before store open" );
let limited = harness.script("limited.commands", "advance 2\nobserve state\n"); let mut command = harness.managed(&limited); command.arg("--trusted-work-limit").arg("1"); let output = Harness::run(command); assert_eq!(output.status.code(), Some(5)); let records = Harness::records(&output); assert!(records.iter().all(|record| { field(record, "record_kind") != "command_ack" || field(record, "code") != "advance" }));}