test(cli): migrate dissolved-verb e2e tests into the project fixture (#218 prep)

The four dissolved verbs (sweep/generalize/walkforward/mc --real) are about to
gain a project gate that refuses outside a project (#218). 25 cli_run.rs tests
today run a well-formed verb to completion in a bare cwd and pin its grade /
store effect — i.e. they pin the pre-gate behaviour. Relocate them into the
built project fixture (fresh_project() over the demo-project cdylib, serialized,
store reset per test) so they still exercise the verb path once the gate lands.
Grades are byte-identical in-project (the fixture Aura.toml overrides no
instrument or data path — DEFAULT_DATA_PATH is absolute), verified against the
exact pins; local run 113 passed / 0 failed / 0 skipped with archive data.

Two new tests pin that the in-project run stamps manifest.project provenance
(the sweep chokepoint and generalize's run_blueprint_member path), a path no
cli_run.rs test previously exercised. The built_project/project_lock helpers are
duplicated into this binary to keep the change inside cli_run.rs; a shared
tests/common extraction is a separate hygiene item.

refs #218
This commit is contained in:
2026-07-09 21:17:45 +02:00
parent 08b0f9c783
commit 2162bd6d97
+218 -70
View File
@@ -1,7 +1,9 @@
//! Integration test: drive the built `aura` binary as a downstream user would,
//! asserting the `run` subcommand's stdout/exit contract and the bad-args path.
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Mutex, MutexGuard, OnceLock};
/// Path to the freshly-built `aura` binary (Cargo sets this env var for the test
/// crate; the binary is named `aura` in `Cargo.toml`).
@@ -17,6 +19,58 @@ fn temp_cwd(name: &str) -> std::path::PathBuf {
dir
}
/// The demo-project fixture (Aura.toml + a built cdylib present), built once —
/// mirrors research_docs.rs's helper (a separate test binary, so it needs its
/// own `OnceLock`); `cargo build` is idempotent, so a second binary building
/// the same fixture is a no-op.
fn built_project() -> &'static PathBuf {
static BUILT: OnceLock<PathBuf> = OnceLock::new();
BUILT.get_or_init(|| {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/demo-project");
let out = std::process::Command::new("cargo")
.arg("build")
.current_dir(&dir)
.output()
.expect("spawn cargo build for the fixture project");
assert!(
out.status.success(),
"fixture build failed:\n{}",
String::from_utf8_lossy(&out.stderr)
);
dir
})
}
/// Serializes every test that runs a verb inside the shared fixture (they reset
/// `<fixture>/runs`, so parallel threads would race). Poison-tolerant.
fn project_lock() -> MutexGuard<'static, ()> {
static LOCK: Mutex<()> = Mutex::new(());
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
/// Removes `<fixture>/runs` on drop (including mid-panic) so a migrated test never
/// leaks its content-addressed store into the git-tracked fixture tree.
struct RunsCleanup(PathBuf);
impl Drop for RunsCleanup {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.0).ok();
}
}
/// Enter the shared built project fixture with a FRESH `runs/` store, serialized.
/// Returns the fixture dir (has an Aura.toml, so `provenance()` is `Some`) and the
/// guards; bind the guards for the whole test body: `let (dir, _g) = fresh_project();`.
/// The guard tuple orders `RunsCleanup` before the `MutexGuard` deliberately: tuple
/// fields drop in index order, so the `runs/` removal (index 0) runs while the lock
/// (index 1) is still held — the serialization the lock exists to enforce.
fn fresh_project() -> (&'static PathBuf, (RunsCleanup, MutexGuard<'static, ()>)) {
let lock = project_lock();
let dir = built_project();
let runs = dir.join("runs");
std::fs::remove_dir_all(&runs).ok();
(dir, (RunsCleanup(runs), lock))
}
#[test]
fn run_prints_json_and_exits_zero() {
// `run` is blueprint-only now (#159 cut 4 retired the bare built-in default);
@@ -662,15 +716,13 @@ fn local_data_present() -> bool {
#[test]
fn sweep_real_no_geometry_symbol_refuses_with_exit_1() {
// not gated: the pip refusal happens before any data access.
let dir = temp_cwd("sweep_real_refuse");
let (dir, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(BIN)
.args(["sweep", &fixture, "--axis", "sma_signal.fast.length=2,4", "--real", "NONEXISTENT"])
.current_dir(&dir).output().unwrap();
.current_dir(dir).output().unwrap();
assert_eq!(out.status.code(), Some(1));
assert!(String::from_utf8_lossy(&out.stderr).contains("no recorded geometry"));
let _ = std::fs::remove_dir_all(&dir);
}
// GER40 Sept-2024 window (inclusive Unix-ms) — the same gated calendar month
@@ -693,7 +745,7 @@ fn sweep_real_blueprint_member_manifest_stamps_the_default_stop() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let dir = temp_cwd("sweep_real_blueprint_no_stop");
let (dir, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(BIN)
.args([
@@ -705,7 +757,7 @@ fn sweep_real_blueprint_member_manifest_stamps_the_default_stop() {
"--axis", "sma_signal.slow.length=8",
"--name", "brp_nostop",
])
.current_dir(&dir)
.current_dir(dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
@@ -726,6 +778,53 @@ fn sweep_real_blueprint_member_manifest_stamps_the_default_stop() {
}
}
/// Property (#218 migration follow-up): running a dissolved `sweep` inside a
/// project (`fresh_project()`, the fixture this migration introduced) stamps
/// `report.manifest.project` on every member with the project's audit-trail
/// provenance (C18/C16) — the `demo` namespace and a well-formed 64-hex-char
/// dylib sha256 — rather than the `None` a bare-cwd run (no `Aura.toml`)
/// produces. Before this migration no cli_run.rs sweep test ran inside a real
/// project, so this stamping path was never exercised end-to-end. Gated on the
/// local GER40 archive; skips cleanly when absent.
#[test]
fn sweep_real_blueprint_member_manifest_stamps_project_provenance() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let (dir, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8",
"--name", "brp_provenance",
])
.current_dir(dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2, "one member line per grid point: {stdout}");
for line in &lines {
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
let project = &v["report"]["manifest"]["project"];
assert_eq!(
project["namespace"].as_str(), Some("demo"),
"member manifest carries the fixture project's namespace: {line}"
);
let sha = project["dylib_sha256"].as_str().unwrap_or_else(|| panic!("dylib_sha256 present: {line}"));
assert_eq!(sha.len(), 64, "dylib_sha256 is a full sha256 hex digest: {line}");
assert!(sha.chars().all(|c| c.is_ascii_hexdigit()), "dylib_sha256 is hex: {line}");
}
}
/// Property: the real-data blueprint sweep path (`aura sweep <blueprint.json>
/// --real SYM --axis …`, dispatched as sugar over the one campaign executor)
/// prints one member line per grid point, IN AXIS ORDER (odometer: the first
@@ -746,7 +845,7 @@ fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let dir = temp_cwd("sweep_real_blueprint");
let (dir, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(BIN)
.args([
@@ -758,7 +857,7 @@ fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() {
"--axis", "sma_signal.slow.length=8,16",
"--name", "brp",
])
.current_dir(&dir)
.current_dir(dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
@@ -806,7 +905,7 @@ fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() {
// exactly one new Sweep family persisted, with the grid's four members.
let fams = std::process::Command::new(BIN)
.args(["runs", "families"])
.current_dir(&dir)
.current_dir(dir)
.output()
.unwrap();
assert!(fams.status.success(), "families exit: {:?}", fams.status);
@@ -909,7 +1008,7 @@ fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() {
"--axis", "sma_signal.slow.length=8,16",
"--name", "brp",
])
.current_dir(&dir)
.current_dir(dir)
.output()
.unwrap();
assert!(out2.status.success(), "second run stderr: {}", String::from_utf8_lossy(&out2.stderr));
@@ -939,8 +1038,6 @@ fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() {
}
assert_eq!(count("processes"), 1, "second identical run dedupes the process doc");
assert_eq!(count("campaigns"), 1, "second identical run dedupes the campaign doc");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (#210 c0110 fieldtest, "the wrapped-name refusal mangles the axis
@@ -1000,7 +1097,7 @@ fn aura_sweep_real_blueprint_subset_axes_refuses_without_store_litter() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let dir = temp_cwd("sweep_real_subset_refusal");
let (dir, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args([
@@ -1011,7 +1108,7 @@ fn aura_sweep_real_blueprint_subset_axes_refuses_without_store_litter() {
"--axis", "sma_signal.fast.length=2,4",
"--name", "c0110-subset",
])
.current_dir(&dir)
.current_dir(dir)
.output()
.expect("spawn aura sweep (subset axes)");
assert_ne!(
@@ -1030,7 +1127,6 @@ fn aura_sweep_real_blueprint_subset_axes_refuses_without_store_litter() {
!runs_log.exists() || std::fs::read_to_string(&runs_log).unwrap().is_empty(),
"no realization recorded for a refused sweep"
);
let _ = std::fs::remove_dir_all(&dir);
}
/// The synthetic seed-resweep `mc` is undefined over real bars (one realization ->
@@ -1363,7 +1459,7 @@ fn generalize_strategy_flag_is_removed_from_the_grammar() {
fn generalize_grades_a_candidate_across_two_instruments() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-two");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -1372,7 +1468,7 @@ fn generalize_grades_a_candidate_across_two_instruments() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -1409,7 +1505,7 @@ fn generalize_grades_a_candidate_across_two_instruments() {
fn generalize_real_e2e_pins_the_exact_current_grade() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-exact-grade");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -1418,7 +1514,7 @@ fn generalize_real_e2e_pins_the_exact_current_grade() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -1466,7 +1562,7 @@ fn generalize_real_e2e_pins_the_exact_current_grade() {
fn generalize_grades_a_non_r_sma_blueprint() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-non-r-sma");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -1476,7 +1572,7 @@ fn generalize_grades_a_non_r_sma_blueprint() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -1515,7 +1611,7 @@ fn generalize_grades_a_non_r_sma_blueprint() {
fn walkforward_real_e2e_pins_the_exact_current_grade() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("walkforward-exact-grade");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -1524,7 +1620,7 @@ fn walkforward_real_e2e_pins_the_exact_current_grade() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -1577,7 +1673,7 @@ fn walkforward_real_e2e_pins_the_exact_current_grade() {
fn walkforward_dissolves_a_non_r_sma_blueprint() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("walkforward-non-r-sma");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -1587,7 +1683,7 @@ fn walkforward_dissolves_a_non_r_sma_blueprint() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -1664,7 +1760,7 @@ fn walkforward_dissolved_refuses_select_plateau() {
/// cleanly on a data refusal.
#[test]
fn walkforward_dissolved_defaults_a_single_omitted_knob() {
let cwd = temp_cwd("walkforward-single-knob-default");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -1672,7 +1768,7 @@ fn walkforward_dissolved_defaults_a_single_omitted_knob() {
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "3",
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -1807,7 +1903,7 @@ fn walkforward_dissolved_refuses_name_and_trace_together() {
fn walkforward_dissolves_through_the_campaign_path() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("walkforward-dissolves");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -1816,7 +1912,7 @@ fn walkforward_dissolves_through_the_campaign_path() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -1862,7 +1958,7 @@ fn walkforward_dissolves_through_the_campaign_path() {
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn families");
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
@@ -1898,7 +1994,7 @@ fn walkforward_dissolves_through_the_campaign_path() {
fn walkforward_stopless_defaults_the_regime_byte_identically_to_explicit_3_2() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("walkforward-stopless-defaults");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
// Stop-less: the two knobs are omitted entirely. This is the headline — today it
@@ -1910,7 +2006,7 @@ fn walkforward_stopless_defaults_the_regime_byte_identically_to_explicit_3_2() {
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if stopless.status.code() == Some(1) {
@@ -1939,7 +2035,7 @@ fn walkforward_stopless_defaults_the_regime_byte_identically_to_explicit_3_2() {
"--stop-length", "3", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
assert_eq!(
@@ -1984,7 +2080,7 @@ fn walkforward_stopless_defaults_the_regime_byte_identically_to_explicit_3_2() {
fn generalize_dissolves_through_the_campaign_path() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-dissolves");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -1993,7 +2089,7 @@ fn generalize_dissolves_through_the_campaign_path() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -2044,7 +2140,7 @@ fn generalize_dissolves_through_the_campaign_path() {
// per-instrument Sweep family per cell (persisted by the executor).
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn families");
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
@@ -2075,7 +2171,7 @@ fn generalize_dissolves_through_the_campaign_path() {
fn generalize_repeated_identical_invocation_does_not_litter_the_store() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-repeat-idempotent");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let invoke = || {
std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
@@ -2085,7 +2181,7 @@ fn generalize_repeated_identical_invocation_does_not_litter_the_store() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura")
};
@@ -2130,7 +2226,7 @@ fn generalize_repeated_identical_invocation_does_not_litter_the_store() {
fn generalize_distinct_invocations_persist_distinct_campaign_documents() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-distinct-content");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let invoke = |fast: &str| {
let fast_axis = format!("sma_signal.fast.length={fast}");
@@ -2141,7 +2237,7 @@ fn generalize_distinct_invocations_persist_distinct_campaign_documents() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura")
};
@@ -2179,7 +2275,7 @@ fn generalize_distinct_invocations_persist_distinct_campaign_documents() {
/// unexercised. Gated on local GER40/USDJPY data.
#[test]
fn generalize_without_explicit_window_falls_back_to_the_first_symbols_full_window() {
let cwd = temp_cwd("generalize-default-window");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -2187,7 +2283,7 @@ fn generalize_without_explicit_window_falls_back_to_the_first_symbols_full_windo
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -2197,7 +2293,6 @@ fn generalize_without_explicit_window_falls_back_to_the_first_symbols_full_windo
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
let _ = std::fs::remove_dir_all(&cwd);
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
@@ -2205,7 +2300,6 @@ fn generalize_without_explicit_window_falls_back_to_the_first_symbols_full_windo
assert!(stdout.contains("\"generalize\":"), "aggregate object: {stdout}");
assert!(stdout.contains("\"n_instruments\":2"), "two instruments graded: {stdout}");
assert!(stdout.contains("\"family_id\":\"generalize-0\""), "family handle: {stdout}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: a single-instrument generalize is refused at parse time (exit 2),
@@ -2362,7 +2456,7 @@ fn generalize_refuses_an_unknown_axis_name() {
fn generalize_persists_a_discoverable_cross_instrument_family() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-family");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -2371,7 +2465,7 @@ fn generalize_persists_a_discoverable_cross_instrument_family() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -2381,7 +2475,6 @@ fn generalize_persists_a_discoverable_cross_instrument_family() {
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
let _ = std::fs::remove_dir_all(&cwd);
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
@@ -2389,7 +2482,7 @@ fn generalize_persists_a_discoverable_cross_instrument_family() {
// the run persisted the per-instrument members as a discoverable CrossInstrument family.
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn families");
assert!(fams.status.success(), "families exit: {:?}", fams.status);
@@ -2407,8 +2500,6 @@ fn generalize_persists_a_discoverable_cross_instrument_family() {
assert_eq!(cross.len(), 1, "exactly one CrossInstrument family: {fams_out:?}");
assert!(cross[0].contains("\"family_id\":\"generalize-0\""), "families: {fams_out:?}");
assert!(cross[0].contains("\"members\":2"), "one member per instrument: {fams_out:?}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#210 T3, dissolution): the persisted `CrossInstrument` family's
@@ -2427,7 +2518,7 @@ fn generalize_persists_a_discoverable_cross_instrument_family() {
fn generalize_family_members_are_attributed_to_the_right_instrument() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-member-attribution");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -2436,7 +2527,7 @@ fn generalize_family_members_are_attributed_to_the_right_instrument() {
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -2446,14 +2537,13 @@ fn generalize_family_members_are_attributed_to_the_right_instrument() {
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
let _ = std::fs::remove_dir_all(&cwd);
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let fam = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "family", "generalize-0"])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn runs family");
assert!(fam.status.success(), "runs family exit: {:?}", fam.status);
@@ -2481,8 +2571,66 @@ fn generalize_family_members_are_attributed_to_the_right_instrument() {
members[1]["metrics"]["r"]["expectancy_r"].as_f64(), Some(0.005795903617609842),
"USDJPY's own expectancy_r, matching the exact-grade anchor: {fam_out:?}"
);
}
let _ = std::fs::remove_dir_all(&cwd);
/// Property (#218 migration follow-up): `generalize`'s per-instrument members
/// route through the shared campaign path (`campaign_run.rs`'s
/// `run_blueprint_member` call, a code path distinct from sweep's in
/// `main.rs`), and that path ALSO stamps `manifest.project` with the running
/// project's provenance (C18/C16) for every persisted member — not just the
/// sweep chokepoint. Before the project-fixture migration this cli_run.rs
/// path only ever ran outside a project, so `project` was always `None` here.
/// Gated on the shared GER40/USDJPY Sept-2024 archive.
#[test]
fn generalize_family_members_stamp_project_provenance() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--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/USDJPY data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let fam = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "family", "generalize-0"])
.current_dir(cwd)
.output()
.expect("spawn runs family");
assert!(fam.status.success(), "runs family exit: {:?}", fam.status);
let fam_out = String::from_utf8(fam.stdout).expect("utf-8");
let members: Vec<serde_json::Value> = fam_out
.lines()
.filter(|l| l.starts_with('{'))
.map(|l| serde_json::from_str(l).expect("member line parses as JSON"))
.collect();
assert_eq!(members.len(), 2, "one member per instrument: {fam_out:?}");
for m in &members {
let project = &m["manifest"]["project"];
assert_eq!(
project["namespace"].as_str(), Some("demo"),
"campaign-path member manifest carries the fixture project's namespace: {fam_out:?}"
);
let sha = project["dylib_sha256"].as_str()
.unwrap_or_else(|| panic!("dylib_sha256 present: {fam_out:?}"));
assert_eq!(sha.len(), 64, "dylib_sha256 is a full sha256 hex digest: {fam_out:?}");
}
}
/// `aura sweep <blueprint.json> --axis <name>=<csv> …` (#166): sweeps a loaded OPEN
@@ -3373,7 +3521,7 @@ fn exit_codes_partition_usage_two_from_runtime_one() {
fn mc_r_bootstrap_real_e2e_pins_the_exact_current_grade() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("mc-r-bootstrap-exact-grade");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -3383,7 +3531,7 @@ fn mc_r_bootstrap_real_e2e_pins_the_exact_current_grade() {
"--block-len", "5", "--resamples", "1000", "--seed", "42",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -3430,7 +3578,7 @@ fn mc_r_bootstrap_real_e2e_pins_the_exact_current_grade() {
fn mc_dissolves_a_non_r_sma_blueprint() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("mc-non-r-sma");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -3441,7 +3589,7 @@ fn mc_dissolves_a_non_r_sma_blueprint() {
"--block-len", "5", "--resamples", "1000", "--seed", "42",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -3486,7 +3634,7 @@ fn mc_dissolves_a_non_r_sma_blueprint() {
fn mc_dissolves_through_the_campaign_path() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("mc-dissolves");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -3496,7 +3644,7 @@ fn mc_dissolves_through_the_campaign_path() {
"--block-len", "5", "--resamples", "1000", "--seed", "42",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -3550,7 +3698,7 @@ fn mc_dissolves_through_the_campaign_path() {
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn families");
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
@@ -3644,9 +3792,9 @@ fn mc_dissolved_refuses_an_unknown_axis_name() {
fn mc_dissolved_seed_changes_the_bootstrap_draw_not_the_pooled_series() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let run = |seed: &str| {
let cwd = temp_cwd(&format!("mc-seed-{seed}"));
std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"mc", &fixture, "--real", "GER40",
@@ -3655,7 +3803,7 @@ fn mc_dissolved_seed_changes_the_bootstrap_draw_not_the_pooled_series() {
"--block-len", "5", "--resamples", "1000", "--seed", seed,
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura")
};
@@ -3707,7 +3855,7 @@ fn walkforward_campaign_runs_an_arbitrary_blueprint() {
}
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("walkforward-arbitrary-blueprint");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -3717,7 +3865,7 @@ fn walkforward_campaign_runs_an_arbitrary_blueprint() {
"--stop-length", "3", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -3759,7 +3907,7 @@ fn mc_campaign_runs_an_arbitrary_blueprint() {
}
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("mc-arbitrary-blueprint");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -3770,7 +3918,7 @@ fn mc_campaign_runs_an_arbitrary_blueprint() {
"--block-len", "5", "--resamples", "100", "--seed", "7",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
@@ -3808,7 +3956,7 @@ fn generalize_campaign_runs_an_arbitrary_blueprint() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let cwd = temp_cwd("generalize-arbitrary-blueprint");
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_meanrev_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
@@ -3819,7 +3967,7 @@ fn generalize_campaign_runs_an_arbitrary_blueprint() {
"--stop-length", "3", "--stop-k", "2.0",
"--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_TO_MS,
])
.current_dir(&cwd)
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {