diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 18214c0..010c3a4 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -4256,6 +4256,18 @@ fn main() { eprintln!("aura: {path}: {e:?}"); std::process::exit(2); }); + // Refuse an open (free-knob) blueprint at the dispatch boundary, mirroring + // `blueprint_mc_family`'s closed-guard: `run` bootstraps over the EMPTY point, so a + // free knob would panic in `compile_with_params` — reject it clean instead (#176). + let free = blueprint_axis_probe(&doc).param_space(); + if !free.is_empty() { + eprintln!( + "aura: run requires a closed blueprint (no free parameters); {} free knob(s) — \ + bind them or use `aura sweep --axis`", + free.len() + ); + std::process::exit(2); + } let BlueprintRunArgs { params, data, seed } = parse_blueprint_run_args(&rest[1..]) .unwrap_or_else(|msg| { eprintln!("aura: {msg}"); diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index f6dc88a..cf31213 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -3655,6 +3655,37 @@ fn aura_mc_rejects_an_open_blueprint() { let _ = std::fs::remove_dir_all(&cwd); } +/// E2E (#176): `aura run ` refuses an open blueprint the same way the +/// sibling `aura mc` does — a clean named error + exit 2, NEVER a `compile_with_params` +/// arity panic (exit 101). The `run` dispatch bootstraps over the EMPTY point (a +/// closed-blueprint assumption), so a free knob must be rejected at the dispatch boundary +/// before that bootstrap, mirroring `blueprint_mc_family`'s closed-guard. +#[test] +fn aura_run_rejects_an_open_blueprint_without_panicking() { + let cwd = temp_cwd("run-blueprint-open-reject"); + let fixture = format!("{}/tests/fixtures/stage1_signal_open.json", env!("CARGO_MANIFEST_DIR")); + let out = Command::new(BIN) + .args(["run", &fixture]) + .current_dir(&cwd) + .output() + .expect("spawn aura run open-blueprint"); + let stderr = String::from_utf8_lossy(&out.stderr).into_owned(); + assert_eq!( + out.status.code(), + Some(2), + "an open blueprint fails clean (exit 2, not a panic/exit 101): stderr={stderr}" + ); + assert!( + stderr.contains("closed blueprint"), + "names the closed-blueprint requirement (mirroring `aura mc`): {stderr}" + ); + assert!( + !stderr.contains("panicked"), + "must refuse at the dispatch boundary, never panic: {stderr}" + ); + let _ = std::fs::remove_dir_all(&cwd); +} + /// E2E (negative): `aura reproduce ` is a hard error (exit non-zero + named /// cause on stderr), distinct from `runs family `'s treat-as-empty exit 0. #[test]