//! cb-sim — scenario runner binary (GameKernel K17; precursor of `cb sim`). //! Parses scenario files, dispatches each to its game by the `/` //! prefix of its `scenario` field, and executes it. Exit codes: 0 all //! passed, 1 any failure — including a scenario whose game prefix is not //! registered, and a run in which nothing executed. There is deliberately //! no "tolerable" non-zero exit: a silent skip is the failure mode this //! binary exists to catch. use cb_game_runtime::{replay, scenario, RunOutcome, ScenarioFile}; use games_ground::GroundState; use std::path::{Path, PathBuf}; /// Where a failing run drops its `.cbreplay` bundle /// (MetricsAndScenarios §2). const REPLAY_DIR: &str = "replays"; fn main() { let argv: Vec = std::env::args().skip(1).collect(); if argv.is_empty() { eprintln!("usage: cb-sim [--runs ] ..."); eprintln!(" cb-sim --replay ..."); std::process::exit(64); } // K10: until CB-WP-0006 T06 this binary had no flag parsing at all, so // `--replay` had nowhere to go — every argument was treated as a // scenario path. if argv[0] == "--replay" { std::process::exit(replay_bundles(&argv[1..])); } // AM-8 (CB-WP-0015 T02): the replay count. Defaults to K8's two, so // `make sim` is unchanged; `make am8` passes 10 on one scenario. let mut runs = scenario::K8_RUNS; let mut args = argv; if args[0] == "--runs" { if args.len() < 3 { eprintln!("usage: cb-sim --runs ..."); std::process::exit(64); } runs = match args[1].parse::() { Ok(n) if n >= 2 => n, // A --runs that silently fell back to 2 would report an // N=10 result after doing an N=2 check — the harness-does- // nothing class this binary's header is about. _ => { eprintln!("--runs needs an integer >= 2, got {:?}", args[1]); std::process::exit(64); } }; args = args.split_off(2); } let mut failed = false; let mut passed = 0usize; let mut covered: Vec = Vec::new(); for path in &args { let text = match std::fs::read_to_string(path) { Ok(t) => t, Err(e) => { eprintln!("{path}: read error: {e}"); failed = true; continue; } }; let sc = match ScenarioFile::from_yaml(&text) { Err(e) => { eprintln!("{path}: parse error: {e}"); failed = true; continue; } Ok(sc) => sc, }; let outcome = match sc.scenario.split('/').next() { Some("ground") => scenario::run_n::(&sc, runs), _ => { // A renamed or typo'd prefix would otherwise skip every // scenario while the run still looked clean. eprintln!( "FAIL {} — no game registered for prefix {:?}", sc.scenario, sc.scenario.split('/').next().unwrap_or("") ); failed = true; continue; } }; match outcome { RunOutcome::Passed { covers } => { println!( "PASS {} covers={}{}", sc.scenario, covers.join(","), if sc.provisional { " provisional" } else { "" } ); covered.extend(covers); passed += 1; } RunOutcome::Failed { reason, evidence } => { println!("FAIL {} — {reason}", sc.scenario); // K10: a failure becomes work an agent can start, not just // information (MetricsAndScenarios §4). if let Some(ev) = evidence { match std::fs::create_dir_all(REPLAY_DIR) .map_err(|e| e.to_string()) .and_then(|()| { replay::write_bundle( Path::new(REPLAY_DIR), &sc, &ev.initial, &ev.initial_hash, &ev.end_state, &ev.end_state_hash, &reason, ) }) { Ok(path) => println!(" bundle {}", path.display()), Err(e) => eprintln!(" bundle NOT written: {e}"), } } failed = true; } } } covered.sort(); covered.dedup(); println!("{passed} passed, {} rules covered", covered.len()); // Positive control: a run that executed nothing must not pass. This // is the same class of error as a benchmark timing rejected work. if passed == 0 { eprintln!("FAIL — no scenario executed; refusing to report success"); failed = true; } if failed { std::process::exit(1); } } /// `cb-sim --replay ...` — re-execute recorded bundles. fn replay_bundles(paths: &[String]) -> i32 { if paths.is_empty() { eprintln!("usage: cb-sim --replay ..."); return 64; } let mut ok = 0usize; let mut bad = false; for p in paths { let bundle = PathBuf::from(p); match replay::replay::(&bundle) { Ok(r) => { println!( "REPLAY {} — {} commands, hash {} reproduced", r.scenario, r.commands, &r.hash[..12] ); ok += 1; } Err(e) => { println!("REPLAY FAIL {} — {e}", bundle.display()); bad = true; } } } // Same positive control as the scenario path: a run that replayed // nothing must not report success. if ok == 0 { eprintln!("FAIL — no bundle replayed; refusing to report success"); bad = true; } i32::from(bad) }