fix(cli): walkforward validates its --axis grid once, not once per window
`aura walkforward <bp> --axis <name>=<v>` emitted a rejected axis's diagnostic more than once (a racy 1-3x) when the grid did not resolve. `blueprint_walkforward_family` resolved the axes *inside* the per-window closure, and `walk_forward` fans that closure out across the windows in parallel — so several windows each `eprintln!` the same `BindError` and `exit(2)` before any one exit tears the process down. Axis resolution is window-independent, so it is hoisted to a single dispatch-boundary pre-flight (mirroring `aura sweep`, which validates its axes once before any member runs): the first in-sample window is resolved up front, surfacing the `BindError` exactly once; the per-window closure then resolves already-validated axes and cannot re-raise (`expect`). RED-first: cli_run.rs `aura_walkforward_emits_an_unknown_axis_rejection_once` drives the verb 20x and asserts the rejection is single every run (the emit count was racy, so a single invocation would be a flaky assertion). Surfaced by the World/C21 milestone fieldtest. closes #177 refs #170
This commit is contained in:
@@ -3306,9 +3306,25 @@ fn blueprint_walkforward_family(
|
||||
let space = blueprint_axis_probe(doc).param_space();
|
||||
let topo = topology_hash(&blueprint_from_json(doc, &|t| std_vocabulary(t))
|
||||
.expect("doc parse-validated at the dispatch boundary; reload is infallible"));
|
||||
// Validate the `--axis` grid ONCE at the dispatch boundary, mirroring `aura sweep`
|
||||
// (which resolves its axes a single time before any member runs). `walk_forward` fans
|
||||
// the per-window closure out across the windows in parallel, so a `BindError` raised
|
||||
// *inside* the closure would `eprintln!`+`exit(2)` from several windows before any one
|
||||
// exit lands — a racy, duplicated rejection (#177). Axis resolution is window-agnostic,
|
||||
// so pre-flighting the first IS window here surfaces the error exactly once (it fails in
|
||||
// `resolve_axes`, before any member runs); a per-window resolve then cannot re-raise.
|
||||
let first_is = WindowRoller::new(span, is_len, oos_len, step, RollMode::Rolling)
|
||||
.expect("roller config validated just above")
|
||||
.next()
|
||||
.expect("roller yields >= 1 window (validated above)")
|
||||
.is;
|
||||
if let Err(e) = blueprint_sweep_over(doc, axes, first_is.0, first_is.1, data) {
|
||||
eprintln!("aura: {e:?}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
walk_forward(roller, space.clone(), |w: WindowBounds| {
|
||||
let (is_family, lattice) = blueprint_sweep_over(doc, axes, w.is.0, w.is.1, data)
|
||||
.unwrap_or_else(|e| { eprintln!("aura: {e:?}"); std::process::exit(2); });
|
||||
.expect("axes validated in the dispatch-boundary pre-flight");
|
||||
let (best, selection) = match select_winner(&is_family, "sqn_normalized", select, Some(&lattice)) {
|
||||
Ok(v) => v,
|
||||
Err(msg) => { eprintln!("aura: {msg}"); std::process::exit(2); }
|
||||
|
||||
@@ -3638,6 +3638,41 @@ fn aura_walkforward_over_a_blueprint_rejects_an_unknown_axis() {
|
||||
assert!(!stderr.contains("panicked"), "must reject cleanly, never panic: {stderr}");
|
||||
}
|
||||
|
||||
/// E2E (#177): a rejected `--axis` on a loaded walk-forward blueprint is reported
|
||||
/// EXACTLY ONCE, however many IS/OOS windows the roll would have spanned — mirroring
|
||||
/// the sibling `aura sweep` / `aura mc` paths, which validate the axis ONCE up front
|
||||
/// and emit their rejection a single time. The property this protects: axis
|
||||
/// resolution is a single pre-flight, not a per-window re-raise.
|
||||
///
|
||||
/// The CLOSED fixture has an empty param_space, so ANY `--axis` name is an
|
||||
/// `UnknownKnob`. The pre-fix code validates the axis INSIDE the per-window closure
|
||||
/// that `walk_forward` fans out in parallel across the windows, so more than one
|
||||
/// window `eprintln!`s the same rejection before the racing `exit(2)` tears the
|
||||
/// process down — a race whose emit count is 1 or 2 per run. A single invocation
|
||||
/// would therefore be a flaky assertion (it emits once ~1/3 of the time by luck), so
|
||||
/// this drives the verb repeatedly: observing the double-emit at least once is then
|
||||
/// near-certain, and the assertion is that EVERY run emits the rejection exactly
|
||||
/// once. The fix (hoist axis resolution to one pre-flight before the windowed
|
||||
/// fan-out) makes every run deterministically single.
|
||||
#[test]
|
||||
fn aura_walkforward_emits_an_unknown_axis_rejection_once() {
|
||||
let bp = format!("{}/tests/fixtures/stage1_signal.json", env!("CARGO_MANIFEST_DIR"));
|
||||
for attempt in 1..=20 {
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["walkforward", &bp, "--axis", "graph.fast.length=2,3"])
|
||||
.output()
|
||||
.expect("spawn aura walkforward (unknown axis)");
|
||||
assert_eq!(out.status.code(), Some(2), "an unknown axis must fail clean, not panic");
|
||||
let stderr = String::from_utf8(out.stderr).expect("utf-8");
|
||||
assert!(!stderr.contains("panicked"), "must reject cleanly, never panic: {stderr}");
|
||||
let emits = stderr.matches("UnknownKnob").count();
|
||||
assert_eq!(
|
||||
emits, 1,
|
||||
"attempt {attempt}: the axis rejection must be emitted exactly once, got {emits}: {stderr:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// E2E (acc 2): `aura mc <open blueprint.json>` is rejected with a named error + exit 2
|
||||
/// (NOT a panic / exit 101) — MC binds no axis, so a free knob has no binder.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user