diff --git a/crates/hexwork-cli/tests/phase11_gate.rs b/crates/hexwork-cli/tests/phase11_gate.rs index bd9f5d0..b5225bc 100644 --- a/crates/hexwork-cli/tests/phase11_gate.rs +++ b/crates/hexwork-cli/tests/phase11_gate.rs @@ -14,6 +14,8 @@ 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}; diff --git a/crates/hexwork-cli/tests/phase12/v4_compat.rs b/crates/hexwork-cli/tests/phase12/v4_compat.rs new file mode 100644 index 0000000..b458e28 --- /dev/null +++ b/crates/hexwork-cli/tests/phase12/v4_compat.rs @@ -0,0 +1,219 @@ +//! Immutable pre-transfer v4 compatibility evidence captured from accepted +//! production commit 7834d52bcb00f2dec1922d1b995e928e2e991f90. + +#![cfg(any(target_os = "linux", target_os = "macos"))] + +use super::support::Harness; +use hexwork_core::action::CommittedAction; +use hexwork_core::runtime::RunLocation; +use hexwork_save::artifact::{ArtifactStore, RecoveryLimits}; +use hexwork_save::{ + decode_replay, decode_run, encode_replay, encode_run, encode_vault, SessionEventKind, VaultV2, +}; +use hexwork_worldgen::PlanDestination; +use std::fs; +use std::path::Path; + +const ACCEPTED_COMMIT: &str = "7834d52bcb00f2dec1922d1b995e928e2e991f90"; +const ACCEPTED_BINARY_SHA256: &str = + "4c9f41a170e68914a7ab6e9b71eb20274cbe5dcb3913696edb7159f99809a977"; +const SEED: u64 = 140; +const DEATH_SEED: u64 = 223; +const RUN_ID: &str = "run-00000000000000000001"; +const CLASSIC_RUN_SHA256: &str = "467e582827b6f09a05aabaee60ba5bd4e85c373e5345a6a7aeee56dbf5c340c3"; +const CLASSIC_REPLAY_SHA256: &str = + "af28de1eaebcd9b66ec0bb72596f8bca22e2c5726219066e9e8b8d43ba997115"; +const STORY_RUN_SHA256: &str = "1dc214719fecca479a4e868d0da1ea04b974a297dec1370505d1126263fd5af5"; +const CLASSIC_FINAL_HASH: u64 = 11_066_088_334_843_938_192; +const REPLAY_FINAL_HASH: u64 = 846_499_532_936_173_058; +const STORY_FINAL_HASH: u64 = 1_016_476_638_999_387_259; + +const CLASSIC_RUN: &[u8] = + include_bytes!("../../../hexwork-save/testfixtures/run-v4-7834d52-classic.hexrun"); +const CLASSIC_REPLAY: &[u8] = + include_bytes!("../../../hexwork-save/testfixtures/replay-v4-7834d52-classic.hexdemo"); +const STORY_RUN: &[u8] = + include_bytes!("../../../hexwork-save/testfixtures/run-v4-7834d52-story.hexrun"); +const PROVENANCE: &str = + include_str!("../../../hexwork-save/testfixtures/pretransfer-v4-7834d52-provenance.txt"); + +fn sha256(bytes: &[u8]) -> String { + hexwork_content::pack_hash(&[bytes]) +} + +fn decode_run_fixture(bytes: &[u8]) -> hexwork_save::DecodedRun { + let pack = + hexwork_content::RuntimePack::load(Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")) + .unwrap(); + decode_run(bytes, pack.hash()).unwrap() +} + +fn install_fixture(harness: &Harness, bytes: &[u8]) -> hexwork_save::DecodedRun { + let pack = hexwork_content::RuntimePack::load(&harness.content).unwrap(); + let decoded = decode_run(bytes, pack.hash()).unwrap(); + fs::create_dir_all(&harness.data).unwrap(); + let mut vault = VaultV2::fresh(decoded.identity.lineage_id); + vault.current = decoded.identity.initial_lineage.clone(); + fs::write( + harness.data.join("lineage-vault.json"), + encode_vault(&vault).unwrap(), + ) + .unwrap(); + let (mut store, _) = ArtifactStore::open(&harness.data).unwrap(); + store + .recover(&pack, RecoveryLimits::default(), None) + .unwrap(); + let (allocation, _) = store.allocate_run().unwrap(); + assert_eq!(allocation.run_id(), decoded.identity.run_id); + assert_eq!(allocation.lineage_id(), decoded.identity.lineage_id); + assert_eq!( + allocation.initial_lineage(), + &decoded.identity.initial_lineage + ); + store + .publish_initial_save(&allocation, bytes, &pack) + .unwrap(); + drop(store); + decoded +} + +#[test] +fn genuine_pretransfer_v4_fixtures_decode_and_reencode_identically() { + assert_eq!(sha256(CLASSIC_RUN), CLASSIC_RUN_SHA256); + assert_eq!(sha256(CLASSIC_REPLAY), CLASSIC_REPLAY_SHA256); + assert_eq!(sha256(STORY_RUN), STORY_RUN_SHA256); + assert!(PROVENANCE.contains(ACCEPTED_COMMIT)); + assert!(PROVENANCE.contains(ACCEPTED_BINARY_SHA256)); + + let classic = decode_run_fixture(CLASSIC_RUN); + assert_eq!(classic.identity.seed, SEED); + assert_eq!(classic.identity.run_id.to_string(), RUN_ID); + assert_eq!( + classic.current.try_hash_state().unwrap(), + CLASSIC_FINAL_HASH + ); + assert_eq!( + encode_run( + &classic.identity, + &classic.current, + classic.story_checkpoint.as_ref(), + &classic.events, + ) + .unwrap(), + CLASSIC_RUN + ); + + let pack = + hexwork_content::RuntimePack::load(Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")) + .unwrap(); + let replay = decode_replay(CLASSIC_REPLAY, pack.hash()).unwrap(); + assert_eq!(replay.identity.seed, DEATH_SEED); + assert_eq!(replay.identity.run_id.to_string(), RUN_ID); + assert_eq!(replay.expected_final_hash, REPLAY_FINAL_HASH); + assert_eq!(encode_replay(&replay).unwrap(), CLASSIC_REPLAY); + + let story = decode_run_fixture(STORY_RUN); + assert_eq!(story.identity.seed, SEED); + assert_eq!(story.identity.run_id.to_string(), RUN_ID); + assert_eq!(story.current.try_hash_state().unwrap(), STORY_FINAL_HASH); + assert!( + matches!(story.current.run_location(), RunLocation::Town(town) if town == "milthollow") + ); + let checkpoint = story.story_checkpoint.as_ref().expect("Story checkpoint"); + let prefix = usize::try_from(checkpoint.after_event_count).unwrap(); + assert_eq!(prefix, 28, "fixture must retain the genuine return prefix"); + assert_eq!( + prefix, + story.events.len(), + "save must retain the exact checkpoint prefix" + ); + assert_eq!(checkpoint.state.tick, story.current.tick); + assert_eq!(checkpoint.state.hash, STORY_FINAL_HASH); + let town = hexwork_worldgen::milthollow(SEED); + let down = town + .connectors + .iter() + .find(|connector| connector.destination == PlanDestination::Deep { depth: 1 }) + .expect("generated Town -> Deep(1) connector"); + assert!(matches!( + story.events[prefix - 2].kind, + SessionEventKind::Action(CommittedAction::UseTransition { transition_id }) + if transition_id == down.transition_id + )); + assert!(matches!( + story.events[prefix - 1].kind, + SessionEventKind::Action(CommittedAction::UseTransition { transition_id }) + if transition_id == down.paired_transition_id + )); + assert_eq!( + encode_run( + &story.identity, + &story.current, + story.story_checkpoint.as_ref(), + &story.events, + ) + .unwrap(), + STORY_RUN + ); +} + +#[test] +fn binary_loads_and_replays_pretransfer_v4_fixtures() { + for (label, bytes, story_mode, expected_hash) in [ + ("classic", CLASSIC_RUN, false, CLASSIC_FINAL_HASH), + ("story", STORY_RUN, true, STORY_FINAL_HASH), + ] { + let harness = Harness::new(&format!("pretransfer-v4-{label}-load")); + let decoded = install_fixture(&harness, bytes); + let script = harness.script("load.commands", "observe state\nquit\n"); + let output = Harness::run({ + let mut command = harness.managed(&script); + command.arg("--load").arg(RUN_ID); + if story_mode { + command.arg("--story"); + } + command + }); + assert!( + output.status.success(), + "{label} load stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let observation = Harness::records(&output) + .into_iter() + .find(|record| record["record_kind"] == "observation") + .expect("loaded state observation"); + assert_eq!(observation["canonical_hash"].as_u64(), Some(expected_hash)); + assert_eq!( + observation["count"].as_u64(), + Some(decoded.events.len() as u64) + ); + assert_eq!( + fs::read(harness.data.join("runs").join(RUN_ID).join("run.save")).unwrap(), + bytes, + "read-only load rewrote {label} fixture" + ); + } + + let harness = Harness::new("pretransfer-v4-replay"); + let replay_path = harness.root.join("classic-death.hexdemo"); + fs::write(&replay_path, CLASSIC_REPLAY).unwrap(); + let output = Harness::run({ + let mut command = harness.command(); + command + .arg("--content-root") + .arg(&harness.content) + .arg("--replay") + .arg(&replay_path); + command + }); + assert!( + output.status.success(), + "replay stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let text = String::from_utf8(output.stdout).unwrap(); + assert!(text.contains("verified event-text replay")); + assert!(text.contains(&format!("hash {REPLAY_FINAL_HASH:#x}"))); + assert_eq!(fs::read(replay_path).unwrap(), CLASSIC_REPLAY); +} diff --git a/crates/hexwork-save/testfixtures/pretransfer-v4-7834d52-provenance.txt b/crates/hexwork-save/testfixtures/pretransfer-v4-7834d52-provenance.txt new file mode 100644 index 0000000..a430e3c --- /dev/null +++ b/crates/hexwork-save/testfixtures/pretransfer-v4-7834d52-provenance.txt @@ -0,0 +1,158 @@ +HEXWORK genuine pre-transfer v4 compatibility fixture provenance + +Source and build +- Accepted source commit: 7834d52bcb00f2dec1922d1b995e928e2e991f90 +- Source tree at capture: accepted commit above; no runtime source edits; parent-owned docs/phase12-wiring-plan.md was modified but is not a build input. +- Build command: TMPDIR=/var/tmp cargo build --locked -p hexwork-cli --bin hexwork +- Production executable: target/debug/hexwork +- Executable SHA-256: 4c9f41a170e68914a7ab6e9b71eb20274cbe5dcb3913696edb7159f99809a977 +- Executable size: 83743816 bytes +- rustc: rustc 1.98.1 (48a229cea 2026-09-01) +- cargo: cargo 1.98.1 (797e8a9bc 2026-08-05) +- The one-off capture test checked the executable SHA before spawning it through the bounded Harness. That scratch-specific generator was removed after capture; the exact commands and immutable bytes remain here. + +Common character choices +- name: the delver +- ancestry: 1 (Mudborn) +- calling: 1 (Lampbearer) +- fresh independent managed lineage for each production process +- allocated run ID in each independent lineage: run-00000000000000000001 + +run-v4-7834d52-classic.hexrun +- Origin: live Classic production process, seed 140. +- Invocation options: --data-dir --content-root --headless-script