//! T15 production-process gates for atomic Contracts opening. // Public within this auto-discovered target so the shared bounded harness keeps its API. #[path = "support/mod.rs"] pub mod support; use hexwork_core::action::{CommittedAction, Direction}; use hexwork_save::{decode_run, read_file_bounded, SessionEventKind, MAX_COMPRESSED_BYTES}; use std::fs; use std::process::Command; use support::Harness; const SEED: u64 = 140; const NAME: &str = "T15 contracts delver"; 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 explicit_new_command(harness: &Harness, script: &std::path::Path) -> Command { let mut command = harness.managed(script); command .arg("--story") .arg("--seed") .arg(SEED.to_string()) .arg("--name") .arg(NAME) .arg("--ancestry") .arg("1") .arg("--calling") .arg("2"); command } fn actions(run: &hexwork_save::DecodedRun) -> Vec<&CommittedAction> { run.events .iter() .map(|event| match &event.kind { SessionEventKind::Action(action) => action, other => panic!("unexpected lifecycle event in living Story run: {other:?}"), }) .collect() } #[test] fn binary_headless_out_of_range_contracts_stops_later_commands() { let harness = Harness::new("contracts-open-headless"); let script = harness.script( "contracts.commands", "observe state\ninput contracts\ninput south\nsave\nquit\n", ); let output = Harness::run(explicit_new_command(&harness, &script)); assert_eq!( output.status.code(), Some(3), "stdout={} stderr={}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); assert!(String::from_utf8_lossy(&output.stderr) .contains("interaction-out-of-range: no matching interaction at [21, 31, 8]")); let records = Harness::records(&output); let baseline = records .iter() .find(|record| record["record_kind"] == "observation" && record["command_index"] == 0) .expect("baseline state observation before rejection"); let baseline_hash = baseline["canonical_hash"].as_u64().unwrap(); assert_eq!(baseline["count"], 0); assert!(records.iter().all(|record| { match record["command_index"].as_u64() { Some(index) => index < 1, None => true, } })); assert!(!records.iter().any(|record| { record["record_kind"] == "command_ack" && record["command_index"] .as_u64() .is_some_and(|index| index >= 1) })); let run_id = only_run_id(&harness); let saved = decode_managed_run(&harness, &run_id); assert_eq!(saved.current.try_hash_state().unwrap(), baseline_hash); assert!( saved.events.is_empty(), "durable initial history remains empty" ); } #[test] #[cfg(any(target_os = "linux", target_os = "macos"))] fn binary_pty_out_of_range_contracts_keeps_play_alive() { for mode in ["play", "map"] { let harness = Harness::new(&format!("contracts-open-pty-{mode}")); let scenario = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/pty/contracts_open.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) .arg(mode) .arg(SEED.to_string()); command }); assert!( output.status.success(), "{mode} PTY driver: 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["window_80x24"], true); assert_eq!(summary["rejection_visible_before_followup"], 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!( actions(&saved), vec![&CommittedAction::Move(Direction::South)], "failed Contracts inspection is absent and legal follow-up commits" ); } }