diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index ca2bbc6..45c6365 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -3572,6 +3572,257 @@ fn mc_dissolved_seed_changes_the_bootstrap_draw_not_the_pooled_series() { ); } +/// #220 weld-gone proof: the walkforward campaign path runs an arbitrary user +/// blueprint with axis names the CLI has never heard of — the r-sma weld +/// (hardcoded fast/slow) is structurally gone. Gated on the GER40 archive; +/// skips cleanly when absent. +#[test] +fn walkforward_campaign_runs_an_arbitrary_blueprint() { + if !local_data_present() { + eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH); + return; + } + const FROM_MS: &str = "1735689600000"; + const TO_MS: &str = "1767225599000"; + let cwd = temp_cwd("walkforward-arbitrary-blueprint"); + let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR")); + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args([ + "walkforward", &fixture, "--real", "GER40", + "--axis", "r_breakout_signal.channel_hi.length=20,40", + "--axis", "r_breakout_signal.channel_lo.length=10,20", + "--stop-length", "3", "--stop-k", "2.0", + "--from", FROM_MS, "--to", TO_MS, + ]) + .current_dir(&cwd) + .output() + .expect("spawn aura"); + if out.status.code() == Some(1) { + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("no local data") || stderr.contains("no recorded geometry"), + "exit 1 must be a data refusal, got: {stderr}" + ); + eprintln!("skip: no local GER40 data"); + return; + } + assert_eq!( + out.status.code(), + Some(0), + "exit: {:?} stderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout"); + let grade_line = stdout + .lines() + .find(|l| l.starts_with("{\"walkforward\":")) + .unwrap_or_else(|| panic!("the walkforward summary line: {stdout}")); + let v: serde_json::Value = serde_json::from_str(grade_line).expect("summary line parses as JSON"); + let wf = &v["walkforward"]; + assert!(wf["windows"].as_u64().is_some_and(|w| w >= 1), "at least one window: {grade_line}"); + // param_stability rows: channel_hi, channel_lo, stop_length, stop_k. + let ps = wf["param_stability"].as_array().expect("param_stability is an array"); + assert_eq!(ps.len(), 4, "two channel axes + the stop pair: {grade_line}"); +} + +/// #220 weld-gone proof for mc: the R-bootstrap pipeline runs the same +/// arbitrary breakout blueprint. Gated on the GER40 archive. +#[test] +fn mc_campaign_runs_an_arbitrary_blueprint() { + if !local_data_present() { + eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH); + return; + } + const FROM_MS: &str = "1735689600000"; + const TO_MS: &str = "1767225599000"; + let cwd = temp_cwd("mc-arbitrary-blueprint"); + let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR")); + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args([ + "mc", &fixture, "--real", "GER40", + "--axis", "r_breakout_signal.channel_hi.length=20,40", + "--axis", "r_breakout_signal.channel_lo.length=10,20", + "--stop-length", "3", "--stop-k", "2.0", + "--block-len", "5", "--resamples", "100", "--seed", "7", + "--from", FROM_MS, "--to", TO_MS, + ]) + .current_dir(&cwd) + .output() + .expect("spawn aura"); + if out.status.code() == Some(1) { + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("no local data") || stderr.contains("no recorded geometry"), + "exit 1 must be a data refusal, got: {stderr}" + ); + eprintln!("skip: no local GER40 data"); + return; + } + assert_eq!( + out.status.code(), + Some(0), + "exit: {:?} stderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout"); + let grade_line = stdout + .lines() + .find(|l| l.starts_with("{\"mc_r_bootstrap\":")) + .unwrap_or_else(|| panic!("the mc_r_bootstrap grade line: {stdout}")); + let v: serde_json::Value = serde_json::from_str(grade_line).expect("grade line parses as JSON"); + assert_eq!(v["mc_r_bootstrap"]["n_resamples"].as_u64(), Some(100), "resamples flow: {grade_line}"); + assert!(v["mc_r_bootstrap"]["e_r"]["mean"].is_number(), "bootstrap E[R] present: {grade_line}"); +} + +/// #220 weld-gone proof for generalize: one mean-reversion candidate graded +/// across two instruments — three axes with names the CLI has never heard of, +/// including an F64 axis. Gated on the GER40/USDJPY Sept-2024 archive. +#[test] +fn generalize_campaign_runs_an_arbitrary_blueprint() { + if !local_data_present() { + eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH); + return; + } + let cwd = temp_cwd("generalize-arbitrary-blueprint"); + let fixture = format!("{}/examples/r_meanrev_open.json", env!("CARGO_MANIFEST_DIR")); + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args([ + "generalize", &fixture, "--real", "GER40,USDJPY", + "--axis", "r_meanrev_signal.mean_window.length=20", + "--axis", "r_meanrev_signal.var_window.length=20", + "--axis", "r_meanrev_signal.band.factor=2.0", + "--stop-length", "3", "--stop-k", "2.0", + "--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS, + ]) + .current_dir(&cwd) + .output() + .expect("spawn aura"); + if out.status.code() == Some(1) { + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("no local data") || stderr.contains("no recorded geometry"), + "exit 1 must be a data refusal, got: {stderr}" + ); + eprintln!("skip: no local GER40/USDJPY data"); + return; + } + assert_eq!( + out.status.code(), + Some(0), + "exit: {:?} stderr: {}", + out.status, + String::from_utf8_lossy(&out.stderr) + ); + let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout"); + assert!(stdout.contains("\"generalize\":"), "aggregate object: {stdout}"); + assert!(stdout.contains("\"n_instruments\":2"), "two instruments graded: {stdout}"); + assert!(stdout.contains("\"worst_case\":"), "worst-case floor present: {stdout}"); +} + +/// #220: the walkforward campaign path requires >= 1 --axis (exit 2, data-free). +#[test] +fn walkforward_campaign_refuses_missing_axis() { + let cwd = temp_cwd("walkforward-campaign-no-axis"); + let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR")); + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args(["walkforward", &fixture, "--real", "GER40", "--stop-length", "14", "--stop-k", "2.0"]) + .current_dir(&cwd) + .output() + .expect("spawn aura"); + assert_eq!(out.status.code(), Some(2), "missing --axis is a usage refusal"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.contains("requires >= 1 --axis"), "axis-arity refusal: {stderr}"); +} + +/// #220: mc's two modes are disjoint — --seeds (synthetic seed family) with +/// --real (campaign pipeline) is a cross-mode usage error (exit 2, data-free). +#[test] +fn mc_campaign_refuses_seeds_with_real() { + let cwd = temp_cwd("mc-campaign-seeds-mix"); + let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR")); + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args([ + "mc", &fixture, "--real", "GER40", "--seeds", "3", + "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--stop-length", "14", "--stop-k", "2.0", + ]) + .current_dir(&cwd) + .output() + .expect("spawn aura"); + assert_eq!(out.status.code(), Some(2), "--seeds with --real is a cross-mode usage refusal"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.contains("Usage: aura mc"), "usage refusal: {stderr}"); +} + +/// #220: a raw-form axis name is refused before any data access on the migrated +/// verbs, exactly as sweep's shipped refusal — echoing what the user typed, with +/// no store litter (mirrors aura_sweep_real_blueprint_refuses_a_raw_form_axis_name_before_data_access). +#[test] +fn walkforward_campaign_refuses_a_raw_form_axis_name() { + let cwd = temp_cwd("walkforward-campaign-raw-axis"); + let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR")); + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args([ + "walkforward", &fixture, "--real", "GER40", + "--axis", "fast.length=3,5", "--axis", "sma_signal.slow.length=12,20", + "--stop-length", "14", "--stop-k", "2.0", + ]) + .current_dir(&cwd) + .output() + .expect("spawn aura"); + assert_eq!(out.status.code(), Some(2), "a raw-form axis name is refused before any data access"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.contains("axis \"fast.length\""), "echoes the raw name verbatim: {stderr}"); + assert!(!cwd.join("runs").exists(), "no store litter on the refusal path"); +} + +/// #220: an unknown axis name is refused at the dispatch boundary on the +/// migrated verbs (mirrors aura_sweep_rejects_an_unknown_axis). +#[test] +fn mc_campaign_refuses_an_unknown_axis() { + let cwd = temp_cwd("mc-campaign-unknown-axis"); + let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR")); + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args([ + "mc", &fixture, "--real", "GER40", + "--axis", "r_breakout_signal.nope.length=1", + "--stop-length", "14", "--stop-k", "2.0", + ]) + .current_dir(&cwd) + .output() + .expect("spawn aura"); + assert_eq!(out.status.code(), Some(2), "an unknown axis is refused at the dispatch boundary"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("axis \"r_breakout_signal.nope.length\""), + "names the offending axis: {stderr}" + ); +} + +/// #220: the mc campaign path also requires >= 1 --axis before any archive +/// access (exit 2, data-free) — the same arity gate walkforward enforces +/// (`walkforward_campaign_refuses_missing_axis`) and generalize enforces +/// (`generalize_refuses_no_axis`), but which Task 5 left untested for mc: the +/// `axes.is_empty()` check lives in `dispatch_mc`'s `--real` arm alone, so a +/// regression dropping it would otherwise surface only once real data hits the +/// R-bootstrap pipeline, not at argv-parse time. +#[test] +fn mc_campaign_refuses_missing_axis() { + let cwd = temp_cwd("mc-campaign-no-axis"); + let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR")); + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args(["mc", &fixture, "--real", "GER40", "--stop-length", "14", "--stop-k", "2.0"]) + .current_dir(&cwd) + .output() + .expect("spawn aura"); + assert_eq!(out.status.code(), Some(2), "missing --axis is a usage refusal"); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!(stderr.contains("Usage: aura mc"), "usage refusal: {stderr}"); + assert!(!cwd.join("runs").exists(), "no store litter on the refusal path"); +} + /// `aura run` on the shipped closed example reproduces the built-in r-sma grade /// (#159): the example is the proven data successor to `--harness r-sma`. Survives /// the Cut-1b builder deletion (depends only on the example + generic run path).