//! Phase 12 read-only inventory browser process gates. use super::support::Harness; use hexwork_core::action::{CommittedAction, Direction}; use hexwork_core::runtime::ActorRole; use hexwork_save::{decode_run, read_file_bounded, SessionEventKind, MAX_COMPRESSED_BYTES}; use std::fs; use std::process::Command; fn only_run_id(harness: &Harness) -> String { let mut runs = fs::read_dir(harness.data.join("runs")) .unwrap() .map(|entry| entry.unwrap()) .filter(|entry| entry.file_type().unwrap().is_dir()) .map(|entry| entry.file_name().into_string().unwrap()) .collect::>(); runs.sort(); assert_eq!(runs.len(), 1, "expected one managed run: {runs:?}"); runs.pop().unwrap() } fn decode_managed_run(harness: &Harness, run_id: &str) -> hexwork_save::DecodedRun { let pack = hexwork_content::RuntimePack::load(&harness.content).unwrap(); decode_run( &read_file_bounded( harness.data.join("runs").join(run_id).join("run.save"), MAX_COMPRESSED_BYTES, ) .unwrap(), pack.hash(), ) .unwrap() } fn player(run: &hexwork_save::DecodedRun) -> &hexwork_core::actor::Actor { run.current .actors .iter() .find(|actor| { run.current .identity(actor.id) .is_some_and(|identity| identity.role == ActorRole::Player) }) .expect("one saved Player") } #[test] fn binary_headless_inventory_modal_navigation_is_observational() { let harness = Harness::new("inventory-headless"); let script = harness.script( "inventory.commands", "input inventory\ninput pick2\ninput confirm\ninput south\ninput look\ninput inventory\ninput west\nobserve state\nquit\n", ); let output = Harness::run(harness.managed(&script)); assert!( output.status.success(), "stdout={} stderr={}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); let records = Harness::records(&output); assert_eq!( records .iter() .filter(|record| record["record_kind"] == "command_ack") .count(), 8 ); let observation = records .iter() .find(|record| record["record_kind"] == "observation") .expect("state observation after browser and movement"); assert_eq!(observation["count"], 1); let run_id = only_run_id(&harness); let saved = decode_managed_run(&harness, &run_id); assert_eq!(saved.events.len(), 1); assert!(matches!( &saved.events[0].kind, SessionEventKind::Action(CommittedAction::Move(Direction::West)) )); // Browser navigation is observational: only the single paced west step is // journaled, and it advances 300 mm (one 3000 mm/s tick) inside the spawn // cell `[21,31,8]` rather than crossing into `[20,31,8]`. let feet = player(&saved).position.millimetres(); assert_eq!(player(&saved).position.cell(), [21, 31, 8]); assert_eq!(feet[0], 21_500 - 300, "one paced west step"); assert_eq!([feet[1], feet[2]], [31_500, 8_000]); } #[test] #[cfg(any(target_os = "linux", target_os = "macos"))] fn binary_pty_inventory_select_look_close_then_move() { let harness = Harness::new("inventory-pty"); let scenario = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/pty/inventory_scenario.py"); let output = Harness::run({ let mut command = Command::new("python3"); command .current_dir(&harness.cwd) .arg(scenario) .arg(env!("CARGO_BIN_EXE_hexwork")) .arg(&harness.data) .arg(&harness.content); command }); assert!( output.status.success(), "inventory PTY failed: stdout={} stderr={}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); let summary: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(summary["exit"], 0); assert_eq!(summary["list_visible"], true); assert_eq!(summary["different_item_selected"], true); assert_eq!(summary["detail_visible"], true); assert_eq!(summary["closed_without_exit"], true); assert_eq!(summary["session_over"], true); let run_id = summary["run_id"].as_str().unwrap(); let saved = decode_managed_run(&harness, run_id); assert_eq!(saved.identity.display_name, "the delver"); assert_eq!(saved.events.len(), 1); assert!(matches!( &saved.events[0].kind, SessionEventKind::Action(CommittedAction::Move(Direction::West)) )); // One paced west step (3000 mm/s over one 100 ms world tick) stays inside the // spawn cell `[21,31,8]`; it does not cross into `[20,31,8]`. let feet = player(&saved).position.millimetres(); assert_eq!(player(&saved).position.cell(), [21, 31, 8]); assert_eq!(feet[0], 21_500 - 300, "one paced west step"); assert_eq!([feet[1], feet[2]], [31_500, 8_000]); }