diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index f90d925..7d5b084 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -4839,6 +4839,77 @@ fn aura_walkforward_with_trades_emits_no_zero_trade_note() { let _ = std::fs::remove_dir_all(&cwd); } +/// E2E (#278 decision: same fault class, same verb, same surface => same +/// treatment): a member fault on the SYNTHETIC walk-forward path is CONTAINED +/// exactly like the real/campaign path contains it (#272) — it surfaces as an +/// `aura: warning: ` class-marked stderr line naming the member fault and the +/// process takes the deliberate failed-cells exit 3 (C14 partition, +/// `exit_on_campaign_result` precedent). It never escapes as an uncaught Rust +/// panic: no exit 101, no `panicked at`, no internal `crates/` source path on +/// the consumer's stderr. +#[test] +fn aura_walkforward_synthetic_member_panic_is_contained() { + let cwd = temp_cwd("blueprint-walkforward-member-panic-contained"); + let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); + // length=0 poisons the member: `Sma::new` asserts "SMA length must be >= 1" + // during member compile — the same fault class the real path records as + // "a member panicked: …" and survives. + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args(["walkforward", &bp, "--axis", "sma_signal.fast.length=0", "--name", "wfpanic"]) + .current_dir(&cwd) + .output() + .expect("spawn aura walkforward (poison member)"); + let stderr = String::from_utf8_lossy(&out.stderr).to_string(); + assert!( + stderr.lines().any(|l| l.contains("aura: warning: ") && l.contains("SMA length must be >= 1")), + "the member fault surfaces as one class-marked warning naming the fault: {stderr}" + ); + assert!( + !stderr.contains("panicked at"), + "no raw panic report reaches the consumer: {stderr}" + ); + assert!( + !stderr.contains("crates/"), + "no internal source path leaks to the consumer: {stderr}" + ); + assert_eq!( + out.status.code(), + Some(3), + "a contained member fault is the deliberate failed-cells exit 3, never 101: {stderr}" + ); + let _ = std::fs::remove_dir_all(&cwd); +} + +/// E2E (#278 decision, sweep twin): the synthetic `aura sweep` shares the +/// same bare-member seam (`run_blueprint_member` inside the family builders' +/// sweep closures, no #272 fault boundary), so the same poison must be +/// contained the same way — warning + exit 3, no panic escape. +#[test] +fn aura_sweep_synthetic_member_panic_is_contained() { + let cwd = temp_cwd("blueprint-sweep-member-panic-contained"); + let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args(["sweep", &bp, "--axis", "sma_signal.fast.length=0", "--name", "swpanic"]) + .current_dir(&cwd) + .output() + .expect("spawn aura sweep (poison member)"); + let stderr = String::from_utf8_lossy(&out.stderr).to_string(); + assert!( + stderr.lines().any(|l| l.contains("aura: warning: ") && l.contains("SMA length must be >= 1")), + "the member fault surfaces as one class-marked warning naming the fault: {stderr}" + ); + assert!( + !stderr.contains("panicked at") && !stderr.contains("crates/"), + "no raw panic report or internal source path leaks: {stderr}" + ); + assert_eq!( + out.status.code(), + Some(3), + "a contained member fault is the deliberate failed-cells exit 3, never 101: {stderr}" + ); + let _ = std::fs::remove_dir_all(&cwd); +} + /// E2E (#313, Path 1 — the real/sugar walk-forward): the degenerate /// equal-length config over the shared GER40 archive emits the same /// zero-trade note. Gated on the archive; skips cleanly on a data refusal.