From 2f1baceec789c63427cf44cb3c97d64eedbe200b Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 26 Jul 2026 00:01:20 +0200 Subject: [PATCH] fix(aura-cli): exec --override refuses out-of-domain values and the wrapped form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RED-first fixes for the two field-test bugs on the new flag's blueprint leg, both formerly exit-101 panics. An out-of-domain override value now renders the node constructor's own message as a prose refusal, exit 1 — the single-run bootstrap rides the same catch mechanism the campaign path's #272 per-cell containment uses (a campaign-leg pin confirms the identical value was already contained there, exit 3, unchanged). The retired wrapped axis form refuses before the params zip with a did-you-mean naming the raw node.param namespace and the discovery verb, mirroring the campaign leg's existing prose family. refs #319 --- crates/aura-cli/src/main.rs | 42 +++++++++++++++- crates/aura-cli/tests/exec.rs | 92 +++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 505c71d..4a12411 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -1124,6 +1124,30 @@ fn exec_blueprint_leg(path: &str, overrides: &[String], tap: &[String], env: &au // unmatched path refuses with that same shared prose // (`member.rs:768-771`) rather than a second wording. let raw_space = blueprint_axis_probe(&doc, env).param_space(); + // Refuse the retired pre-#328 WRAPPED override form + // (`..`) BEFORE `override_paths`: that helper + // resolves an override path via `raw_matches_wrapped`, which accepts + // an exact wrapped name as well as the raw one (by design, for + // resolving an already-validated set) — so a wrapped CLI token + // "resolves" there, yet the ORIGINAL token string then misses the + // raw-keyed `ov_by_path` lookup below (panic: "override set derived + // from these exact overrides"). Mirrors the campaign leg's own + // did-you-mean refusal for the same retired form + // (`RefFault::AxisNotInParamSpace`, `research_docs.rs`). + let raw_bound: std::collections::HashSet = + signal.bound_param_space().into_iter().map(|b| b.name).collect(); + for (p, _) in &ov { + if let aura_runner::axes::AxisIntake::WrappedRetired(raw) = + aura_runner::axes::classify_axis_intake(p, &raw_space, &raw_bound) + { + eprintln!( + "aura: axis {p}: names the wrapped param-space form; overrides speak the \ + raw node.param namespace — did you mean \"{raw}\"? see \ + `aura graph introspect --params `" + ); + std::process::exit(1); + } + } let axes: Vec<(String, Vec)> = ov.iter().map(|(p, v)| (p.clone(), vec![*v])).collect(); let paths = override_paths(&axes, &raw_space, &signal).unwrap_or_else(|e| { eprintln!("aura: {e}"); @@ -1161,7 +1185,23 @@ fn exec_blueprint_leg(path: &str, overrides: &[String], tap: &[String], env: &au }) .collect(); let signal = reopen_all(signal, &paths); - let report = run_signal_r(signal, ¶ms, RunData::Synthetic, 0, env, tap_plan); + // An out-of-domain override value fails a node's own domain check at + // bootstrap (e.g. `Sma::new`'s `assert!`, or a negative length + // overflowing the ring-buffer allocation) — a constructor panic, not + // a `Result`. The campaign leg contains the identical value as a + // #272 per-cell fault; this single-run leg has no cell containment, + // so catch the same panic at this dispatch boundary via the shared + // `catch_member_panic` recipe (`aura_campaign::exec`'s own + // `SilencedPanic` + `catch_unwind`) and render it as a runtime-class + // refusal (exit 1, C14 partition) instead of an uncaught exit 101 — + // the input was well-formed argv; the value is what the node refuses. + let report = aura_campaign::catch_member_panic(|| { + run_signal_r(signal, ¶ms, RunData::Synthetic, 0, env, tap_plan) + }) + .unwrap_or_else(|msg| { + eprintln!("aura: {msg}"); + std::process::exit(1); + }); println!("{}", report.to_json()); } else if has_tap { if !overrides.is_empty() { diff --git a/crates/aura-cli/tests/exec.rs b/crates/aura-cli/tests/exec.rs index 3620add..77b396e 100644 --- a/crates/aura-cli/tests/exec.rs +++ b/crates/aura-cli/tests/exec.rs @@ -513,6 +513,61 @@ fn exec_override_duplicate_path_refuses_with_usage_not_a_panic() { assert!(out.contains("--override"), "stdout/stderr: {out}"); } +/// BUG B1 (fieldtest, #319 exec --override leg): an out-of-domain override +/// value on a bound param panics deep in the node's own constructor +/// (`Sma::new`'s `assert!`) instead of refusing — the campaign leg contains +/// the identical value as a #272 per-cell fault (exit 3, see +/// `aura_sweep_synthetic_member_panic_is_contained`); the single-run leg has +/// no cell containment, so the panic must be caught at the dispatch boundary +/// and rendered as a runtime-class refusal (exit 1, C14 partition) instead of +/// an uncaught exit-101 panic. +#[test] +fn exec_override_out_of_domain_value_refuses_not_panics() { + let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "fast.length=0"]); + assert_ne!(code, Some(101), "must not panic: {out}"); + assert_eq!(code, Some(1), "stdout/stderr: {out}"); + assert!(!out.contains("panicked at"), "no raw panic report reaches the consumer: {out}"); + assert!( + out.contains("SMA length must be >= 1"), + "the refusal must name the node's own domain rule: {out}" + ); +} + +/// BUG B1 twin: a negative override value overflows the SMA's ring-buffer +/// allocation (`length as usize` on a negative `i64` wraps to a huge +/// capacity) — same fault class, different panic message, same required +/// treatment (refuse, exit 1, never 101). +#[test] +fn exec_override_negative_value_refuses_not_panics() { + let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "fast.length=-3"]); + assert_ne!(code, Some(101), "must not panic: {out}"); + assert_eq!(code, Some(1), "stdout/stderr: {out}"); + assert!(!out.contains("panicked at"), "no raw panic report reaches the consumer: {out}"); + assert!(out.contains("capacity overflow"), "stdout/stderr: {out}"); +} + +/// BUG B2 (fieldtest, #319 exec --override leg): the retired pre-#328 +/// WRAPPED override form (`..`) panics at the +/// params-zip `.expect` in `exec_blueprint_leg` instead of refusing: +/// `override_paths` accepts an exact wrapped name too (by design, for +/// resolving an already-validated set) so the wrapped token "resolves" there, +/// but the zip below looks the ORIGINAL token up by its raw-translated key, +/// missing it. Refuse before the zip, mirroring the campaign leg's own +/// did-you-mean refusal for the same retired form +/// (`RefFault::AxisNotInParamSpace`). +#[test] +fn exec_override_wrapped_form_path_refuses_not_panics() { + let (out, code) = + run_code(&["exec", "examples/r_sma.json", "--override", "sma_signal.fast.length=1"]); + assert_ne!(code, Some(101), "must not panic: {out}"); + assert_eq!(code, Some(1), "stdout/stderr: {out}"); + assert!(!out.contains("panicked at"), "no raw panic report reaches the consumer: {out}"); + assert!( + out.contains("did you mean") && out.contains("fast.length"), + "the refusal must name the raw candidate: {out}" + ); +} + // --------------------------------------------------------------------------- // Task 4: `--override` on the campaign leg. // --------------------------------------------------------------------------- @@ -564,6 +619,43 @@ fn exec_campaign_override_reaches_the_member_manifests_raw() { } } +/// Cross-check (fieldtest B1, #319): the campaign leg's existing #272 +/// containment already covers an out-of-domain OVERRIDE value, not just a +/// document-declared axis value. `bias.scale` is bound-only (not one of +/// `campaign_doc_json_for`'s own axes), so `--override bias.scale=-1.0` +/// injects a single-value axis of a value the `Bias` node's own constructor +/// rejects (`Bias::new` asserts `scale > 0.0`) — must stay the deliberate +/// per-cell exit 3, never 101. Pinned here since no existing test drove an +/// out-of-domain VALUE through `--override` specifically (only through a +/// document's own axis values, e.g. `aura_sweep_synthetic_member_panic_is_contained`). +#[test] +fn exec_campaign_override_out_of_domain_value_is_contained() { + let (dir, _fixture) = fresh_project_with_data(); + let runs_dir = dir.join("runs"); + std::fs::remove_dir_all(&runs_dir).ok(); + let _cleanup = ScratchGuard(vec![ + ScratchPath::Dir(runs_dir.clone()), + ScratchPath::File(dir.join("ovpanic.process.json")), + ScratchPath::File(dir.join("ovpanic.campaign.json")), + ]); + let bp_id = seed_blueprint(&dir, "exec-override-panic-seed"); + let proc_id = register_process_doc(&dir, "ovpanic.process.json", SWEEP_ONLY_PROCESS_DOC); + let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999)); + write_doc(&dir, "ovpanic.campaign.json", &doc); + + let (out, code) = + run_code_in(&dir, &["exec", "ovpanic.campaign.json", "--override", "bias.scale=-1.0"]); + assert!( + out.lines().any(|l| l.contains("aura: warning: ") && l.contains("Bias scale must be > 0")), + "the member fault surfaces as one class-marked warning naming the fault: {out}" + ); + assert!(!out.contains("panicked at"), "no raw panic report reaches the consumer: {out}"); + assert_eq!( + code, Some(3), + "a contained member fault is the deliberate failed-cells exit 3, never 101: {out}" + ); +} + /// An override path colliding with a document-declared axis (`fast.length`, /// declared by `campaign_doc_json_for`) refuses: an override overrides a /// bound value, never an axis.