From 2162bd6d97aa1de9f46924c27d9abb9e24217bb3 Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 9 Jul 2026 21:17:45 +0200 Subject: [PATCH] test(cli): migrate dissolved-verb e2e tests into the project fixture (#218 prep) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/aura-cli/tests/cli_run.rs | 288 +++++++++++++++++++++++-------- 1 file changed, 218 insertions(+), 70 deletions(-) diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index 50456d9..afc1e5d 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -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 = 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 +/// `/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 `/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 /// --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 = 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 --axis = …` (#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) {