test(cli): RED -- synthetic member panics are contained like the real path

Milestone-fieldtest bug (source-blind run, 2026-07-24): a member fault
on the synthetic walkforward/sweep path escapes as an uncaught panic
(exit 101, no class marker, internal source path leaked) while the
real path contains the identical fault via the #272 boundary as
aura: warning: + exit 3. Cause: the family builders in
aura-runner/src/family.rs run members bare, without the catch_unwind
containment that lives only in aura-campaign/src/exec.rs. Two RED
e2es pin the derived contract (decision log on #278): no exit 101,
a warning-marked fault line, no panic text or crates/ path on stderr,
exit 3 per the C14 partition. refs #278
This commit is contained in:
2026-07-24 00:08:56 +02:00
parent 7e1dd25a76
commit bd6ddf9efe
+71
View File
@@ -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.