diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index 59b11eb..d9341f4 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -2181,6 +2181,84 @@ fn aura_sweep_real_blueprint_refuses_a_raw_form_axis_name_before_data_access() { let _ = std::fs::remove_dir_all(&dir); } +/// Property (#247): every sweep-verb boundary `BindError` renders as prose in +/// the same register as its unknown-axis sibling +/// (`aura: axis : names no param of this blueprint … — see `aura sweep +/// --list-axes``) — never a raw Rust Debug struct. A wrong-kind axis value +/// and an uncovered open knob are the two variants that today leak +/// `KindMismatch { knob: …, expected: …, got: … }` and +/// `MissingKnob("…")` verbatim to stderr; both must instead name the axis, carry +/// the specifics (expected vs supplied kind / the unbound knob), and point at +/// `--list-axes`, with the exit code staying 2. Data-free: both faults are +/// caught at the axis-resolution boundary before any member runs, so the test +/// needs no archive and no project (hostless synthetic sweep). +#[test] +fn sweep_boundary_bind_errors_render_as_prose_not_debug_structs() { + let dir = temp_cwd("sweep-bind-error-prose"); + + // (a) KindMismatch — an axis re-opens the bound I64 `fast.length` (#246) but + // supplies F64 values: the specifics are the expected vs supplied kind. + let closed = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); + let km = Command::new(BIN) + .args(["sweep", &closed, "--axis", "sma_signal.fast.length=2.5,3.5"]) + .current_dir(&dir) + .output() + .expect("spawn aura sweep (kind-mismatched axis)"); + assert_eq!( + km.status.code(), + Some(2), + "a kind-mismatched axis value stays a usage refusal: status={:?}", + km.status + ); + let km_err = String::from_utf8_lossy(&km.stderr).into_owned(); + assert!( + !km_err.contains("KindMismatch"), + "must not leak the raw Rust Debug struct name: {km_err}" + ); + assert!( + km_err.contains("sma_signal.fast.length"), + "the prose names the offending axis exactly as typed: {km_err}" + ); + assert!( + km_err.contains("I64") && km_err.contains("F64"), + "the prose carries the expected vs supplied kind specifics: {km_err}" + ); + assert!( + km_err.contains("--list-axes"), + "the prose points at the discovery surface, like the unknown-axis sibling: {km_err}" + ); + + // (b) MissingKnob — the open fixture leaves `slow.length` unbound when only + // `fast.length` is swept: the specific is the uncovered open knob. + let open = format!("{}/tests/fixtures/r_sma_open.json", env!("CARGO_MANIFEST_DIR")); + let mk = Command::new(BIN) + .args(["sweep", &open, "--axis", "sma_signal.fast.length=2,4"]) + .current_dir(&dir) + .output() + .expect("spawn aura sweep (uncovered open knob)"); + assert_eq!( + mk.status.code(), + Some(2), + "an uncovered open knob stays a usage refusal: status={:?}", + mk.status + ); + let mk_err = String::from_utf8_lossy(&mk.stderr).into_owned(); + assert!( + !mk_err.contains("MissingKnob"), + "must not leak the raw Rust Debug struct name: {mk_err}" + ); + assert!( + mk_err.contains("sma_signal.slow.length"), + "the prose names the unbound knob no axis supplies: {mk_err}" + ); + assert!( + mk_err.contains("--list-axes"), + "the prose points at the discovery surface, like the unknown-axis sibling: {mk_err}" + ); + + let _ = std::fs::remove_dir_all(&dir); +} + /// Property (#210 c0110 fieldtest, "refused dissolved sweeps still register /// their generated campaign document" — store litter, probe P3): binding only /// a SUBSET of the blueprint's open axes (here: `fast.length`, leaving