test(cli): weld-gone proof + new refusals for the generic verbs (#220 slice 3)
Task 5 of the verb-axis-generalization plan — the cycle's point, pinned: - walkforward_campaign_runs_an_arbitrary_blueprint and mc_campaign_runs_an_arbitrary_blueprint drive r_breakout_open.json (wrapped channel_hi/channel_lo axes) over the real archive; generalize_campaign_runs_an_arbitrary_blueprint grades r_meanrev_open.json across two instruments. All three invocations were structurally impossible before this cycle (exit 2). - New refusals: campaign mode without --axis (wf/mc), mc --real + --seeds mix, raw-form axis name before data access, unknown axis as a named Knob fault — each mirroring sweep's shipped contract. - Final acceptance gates ran green in the loop: full workspace suite, clippy -D warnings, cargo doc warning-free, zero too_many_arguments allows in verb_sugar.rs, no RGrid in the workspace, no r_sma_open embed at the three verb dispatch sites (the aura graph default embed survives by design). closes #220 closes #214
This commit is contained in:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user