diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index 7d5b084..b1986bc 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -4910,6 +4910,68 @@ fn aura_sweep_synthetic_member_panic_is_contained() { let _ = std::fs::remove_dir_all(&cwd); } +/// E2E (#278 decision, mc twin): the synthetic `aura mc` shares the same +/// bare-member seam as its sweep/walkforward siblings (`run_blueprint_member` +/// inside `blueprint_mc_family`'s per-seed draw closure, no #272 fault +/// boundary), so the same member-compile fault class must be contained the +/// same way — one `aura: warning: ` line naming the fault, no raw panic text +/// or internal `crates/` path on stderr, deliberate exit 3 (C14), never 101. +/// mc takes only CLOSED blueprints, so the poison is BOUND (`length=0` on the +/// SMA add-op) rather than a poison axis: the test builds its minimal 4-op +/// poison blueprint inline via `aura graph build`. Two seeds so the parallel +/// draws join before the lowest-seed fault resolves (the shipped sweep +/// contract's thread-order-independent shape, C1). +#[test] +fn aura_mc_synthetic_member_panic_is_contained() { + let cwd = temp_cwd("blueprint-mc-member-panic-contained"); + // Minimal poison: each op is load-bearing (source: the close role; add+bind: + // the length=0 that trips `Sma::new`'s "SMA length must be >= 1" assert at + // member compile; feed: wiring so build validation passes; expose: the bias + // contract). Bound length keeps the blueprint CLOSED, as `aura mc` requires. + let ops = r#"[ + {"op":"source","role":"close","kind":"F64"}, + {"op":"add","type":"SMA","name":"sma","bind":{"length":{"I64":0}}}, + {"op":"feed","role":"close","into":["sma.series"]}, + {"op":"expose","from":"sma.value","as":"bias"} + ]"#; + let ops_path = cwd.join("poison.ops.json"); + std::fs::write(&ops_path, ops).expect("write poison op-list"); + let bp_path = cwd.join("poison.bp.json"); + let build = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args(["graph", "build"]) + .stdin(std::fs::File::open(&ops_path).expect("open poison op-list")) + .current_dir(&cwd) + .output() + .expect("spawn aura graph build (poison blueprint)"); + assert_eq!( + build.status.code(), + Some(0), + "poison blueprint builds (the fault is a member-COMPILE fault, not a build refusal): {}", + String::from_utf8_lossy(&build.stderr) + ); + std::fs::write(&bp_path, &build.stdout).expect("write poison blueprint"); + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args(["mc", bp_path.to_str().expect("utf-8 path"), "--seeds", "2"]) + .current_dir(&cwd) + .output() + .expect("spawn aura mc (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.