From 038e1702b36d8e1570bac52795cea901b2da2b49 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 13 Jul 2026 14:52:10 +0200 Subject: [PATCH 1/8] test(cli): bound three whale E2E windows instead of streaming full archives The two dissolved-walkforward flag tests assert window-independent properties (plateau acceptance, per-flag stop defaulting) and now use the ~135-day window the reproduce e2e already uses. The mc roller-fit test replaces its full-archive probe sweep with a fixed ~30-day window inside that same span: the property needs a data-carrying window far shorter than the 90+30-day roller, not a live-span derivation, and the archive only grows forward. Measured on the data-full host: 19.6s, 19.6s and 14.5s+ of serialized real-data compute drop to 1.1-1.9s each. refs #250 --- crates/aura-cli/tests/cli_run.rs | 82 +++++++++----------------------- 1 file changed, 22 insertions(+), 60 deletions(-) diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index 034fa02..cc4aaee 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -2992,8 +2992,11 @@ fn walkforward_dissolved_refuses_a_multi_value_stop() { /// #215: --select plateau is no longer refused on the dissolved path — it now /// threads through to the wf stage exactly like the built-in synthetic path. -/// Gated on the shared GER40 archive; skips cleanly on a data refusal (exit 1), -/// same tolerance as the sibling stop-knob-defaulting test. +/// The property is flag acceptance, not window semantics, so the window is +/// bounded (~135 days, one IS(90d)+OOS(30d) roll) rather than streaming the +/// full multi-year archive. Gated on the shared GER40 archive; skips cleanly +/// on a data refusal (exit 1), same tolerance as the sibling +/// stop-knob-defaulting test. #[test] fn walkforward_dissolved_accepts_select_plateau() { let (cwd, _g) = fresh_project(); @@ -3004,6 +3007,7 @@ fn walkforward_dissolved_accepts_select_plateau() { "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", "--stop-length", "14", "--stop-k", "2.0", "--select", "plateau:worst", + "--from", "1735689600000", "--to", "1747353600000", ]) .current_dir(cwd) .output() @@ -3090,8 +3094,10 @@ fn walkforward_real_e2e_pins_the_exact_current_plateau_grade() { /// explicit still succeeds, proving the defaulting is per-flag, not /// all-or-nothing (the sibling /// `walkforward_stopless_defaults_the_regime_byte_identically_to_explicit_3_2` -/// pins the both-omitted case). Gated on the shared GER40 archive; skips -/// cleanly on a data refusal. +/// pins the both-omitted case). The property is per-flag defaulting, not +/// window semantics, so the window is bounded (~135 days) rather than +/// streaming the full multi-year archive. Gated on the shared GER40 archive; +/// skips cleanly on a data refusal. #[test] fn walkforward_dissolved_defaults_a_single_omitted_knob() { let (cwd, _g) = fresh_project(); @@ -3101,6 +3107,7 @@ fn walkforward_dissolved_defaults_a_single_omitted_knob() { "walkforward", &fixture, "--real", "GER40", "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", "--stop-length", "3", + "--from", "1735689600000", "--to", "1747353600000", ]) .current_dir(cwd) .output() @@ -5268,11 +5275,12 @@ fn mc_dissolves_through_the_campaign_path() { /// to completion by fitting the injected walk-forward roller down to the campaign /// window, instead of refusing late at the executor with "walk_forward windows do not /// fit the campaign window" (exit 1, no remedy reachable from the mc surface). The -/// short window is DERIVED from the live archive — probe GER40's full span via a -/// no-window single-cell sweep, then take the last ~30 days — the same live-archive -/// derivation `generalize_without_explicit_window_resolves_the_intersection_of_all_symbols` -/// uses, so the window stays short as the archive grows and always holds recent bars. -/// The leading sweep stage runs first over the short window; the refusal today fires at +/// short window is a FIXED ~30 days (2025-04-16..2025-05-16, the tail of the +/// ~135-day span the walkforward e2e tests use): what the property needs is "a +/// data-carrying window far shorter than the roller", which a fixed recent window +/// satisfies without the full-archive probe sweep a live-span derivation costs — +/// the archive only grows forward, so the window never falls off its end. The +/// leading sweep stage runs first over the short window; the refusal today fires at /// stage 1 (the walk_forward roller), so this bites only when the window genuinely has /// data. Gated on the GER40 archive; skips cleanly on a data-less host. #[test] @@ -5284,56 +5292,10 @@ fn mc_real_fits_the_injected_roller_to_a_short_window() { let (cwd, _g) = fresh_project(); let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); - // Probe GER40's full archive window: a single-cell, no-window sweep records - // `campaign_window_ms(full_window)` into its generated campaign document, in the - // same Unix-ms currency `--from`/`--to` speak (the generalize no-window test's - // derivation). - let full_window = { - let name = "probe-ger40-span"; - let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) - .args([ - "sweep", &fixture, "--real", "GER40", - "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", - "--name", name, - ]) - .current_dir(cwd) - .output() - .expect("spawn aura sweep probe"); - assert_eq!( - out.status.code(), Some(0), - "the full-archive window-probe sweep must run to exit 0: {}", - String::from_utf8_lossy(&out.stderr) - ); - let dir = cwd.join("runs").join("campaigns"); - let mut resolved = None; - for entry in std::fs::read_dir(&dir).expect("campaigns dir exists after a run") { - let path = entry.expect("readable dir entry").path(); - let doc: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&path).expect("read campaign doc")) - .expect("campaign doc parses as JSON"); - if doc["name"].as_str() == Some(name) { - let w = &doc["data"]["windows"][0]; - resolved = Some(( - w["from_ms"].as_i64().expect("from_ms is an integer"), - w["to_ms"].as_i64().expect("to_ms is an integer"), - )); - break; - } - } - resolved.expect("the probe campaign document records GER40's full window") - }; - - // The last ~30 days of the archive: far shorter than the fixed 90+30-day roller, - // so the injected walk_forward cannot fit unless it scales down to the window. - const THIRTY_DAYS_MS: i64 = 30 * 86_400_000; - let sub_from = full_window.1 - THIRTY_DAYS_MS; - let sub_to = full_window.1; - assert!( - sub_from > full_window.0, - "the derived ~30-day sub-window must sit inside the archive span {full_window:?}" - ); - let sub_from_s = sub_from.to_string(); - let sub_to_s = sub_to.to_string(); + // ~30 days, far shorter than the fixed 90+30-day roller, so the injected + // walk_forward cannot fit unless it scales down to the window. + const SUB_FROM_MS: &str = "1744761600000"; + const SUB_TO_MS: &str = "1747353600000"; let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ @@ -5341,7 +5303,7 @@ fn mc_real_fits_the_injected_roller_to_a_short_window() { "--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20", "--stop-length", "14", "--stop-k", "2.0", "--block-len", "5", "--resamples", "100", "--seed", "42", - "--from", &sub_from_s, "--to", &sub_to_s, + "--from", SUB_FROM_MS, "--to", SUB_TO_MS, ]) .current_dir(cwd) .output() -- 2.52.0 From 00cc2fe927d7781efce021b53aa76f0e0049156b Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 13 Jul 2026 14:53:14 +0200 Subject: [PATCH 2/8] build: compile dependencies at opt-level 2 in the dev profile The E2E suite spawns the debug aura binary, whose wall-clock is dominated by dependency code (zip inflate of the tick archive, sha2 over the project dylib, serde). Workspace crates stay at opt-level 0 for fast rebuilds and debugging; every pinned float is computed by workspace code, so the byte-identity anchors are untouched. refs #250 --- Cargo.toml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 7797232..b606106 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,3 +36,12 @@ serde = { version = "1", features = ["derive"] } # parsed back. Exercised (with a 1-ULP canary value) by aura-cli's # `f64_blueprint_param_survives_store_round_trip_bit_identically`. serde_json = { version = "1", features = ["float_roundtrip"] } + +# Dependencies at opt-level 2, workspace crates at the dev default (0): the E2E +# suite spawns the debug `aura` binary whose wall-clock is dominated by dependency +# code (zip inflate, sha2 dylib hashing, serde) — optimizing deps is multiplicative +# there, while workspace crates stay fast-to-rebuild and debuggable. Float pins are +# unaffected: every pinned float is computed by workspace code (IEEE semantics are +# opt-level-independent in Rust regardless). +[profile.dev.package."*"] +opt-level = 2 -- 2.52.0 From 25b8354959b32a6b56eea9cd2e6264c537060abe Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 13 Jul 2026 15:11:21 +0200 Subject: [PATCH 3/8] =?UTF-8?q?test(cli):=20per-test=20project=20dirs=20vi?= =?UTF-8?q?a=20a=20[nodes]=20pointer=20=E2=80=94=20project=5Flock=20retire?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every E2E test that mutated the shared demo-project fixture store now mints its own tempdir project whose 2-line Aura.toml points at the once-built fixture crate by absolute [nodes] pointer; the runs/ store follows the project root, so no two tests contend and libtest's default thread parallelism applies. project_load keeps driving the fixture directly (the suite's only proof of the inline-crate routing arm); its single store-mutating test is that binary's only one, so no lock is needed there either. This also closes the documented cross-process race window (#223): there is no shared mutable store left to race on. Measured on the data-full host: cli_run 211s -> 57.2s (140 tests), research_docs 21.1s -> 2.1s (73 tests), project_load green, clippy clean. refs #250 --- crates/aura-cli/tests/cli_run.rs | 136 ++++----- crates/aura-cli/tests/common/mod.rs | 62 ++-- crates/aura-cli/tests/project_load.rs | 3 +- crates/aura-cli/tests/research_docs.rs | 377 +++++++++++-------------- 4 files changed, 275 insertions(+), 303 deletions(-) diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index cc4aaee..f12342e 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -1,12 +1,11 @@ //! 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::path::Path; use std::process::Command; -use std::sync::MutexGuard; mod common; -use common::{built_project, project_lock}; +use common::fresh_project; /// Path to the freshly-built `aura` binary (Cargo sets this env var for the test /// crate; the binary is named `aura` in `Cargo.toml`). @@ -22,29 +21,6 @@ fn temp_cwd(name: &str) -> std::path::PathBuf { dir } -/// 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)) -} - /// #218: a well-formed dissolved verb run outside a project refuses up front /// (exit 1, "needs a project") and leaves no store — parity with `campaign run`. /// The refusal names the INVOKED verb, not a copy-pasted generic string — a @@ -1215,7 +1191,7 @@ fn sweep_real_no_geometry_symbol_refuses_with_exit_1() { let fixture = format!("{}/examples/r_sma.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")); } @@ -1301,7 +1277,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)); @@ -1348,7 +1324,7 @@ fn sweep_real_blueprint_member_manifest_stamps_project_provenance() { "--axis", "sma_signal.slow.length=8", "--name", "brp_provenance", ]) - .current_dir(dir) + .current_dir(&dir) .output() .unwrap(); assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr)); @@ -1434,7 +1410,7 @@ fn sweep_real_ohlc_channel_members_run_and_reproduce() { let fixture = format!("{}/tests/fixtures/r_channel_open.json", env!("CARGO_MANIFEST_DIR")); let axes = std::process::Command::new(BIN) .args(["sweep", &fixture, "--list-axes"]) - .current_dir(dir) + .current_dir(&dir) .output() .unwrap(); assert_eq!( @@ -1455,7 +1431,7 @@ fn sweep_real_ohlc_channel_members_run_and_reproduce() { "--axis", "hl_channel.channel_length=3,5", "--name", "chan", ]) - .current_dir(dir) + .current_dir(&dir) .output() .unwrap(); assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr)); @@ -1476,7 +1452,7 @@ fn sweep_real_ohlc_channel_members_run_and_reproduce() { .to_string(); let rep = std::process::Command::new(BIN) .args(["reproduce", &family_id]) - .current_dir(dir) + .current_dir(&dir) .output() .unwrap(); assert!( @@ -1523,7 +1499,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)); @@ -1571,7 +1547,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); @@ -1674,7 +1650,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)); @@ -1761,7 +1737,7 @@ fn sweep_real_with_trace_writes_tap_series_to_disk() { "--axis", "sma_signal.slow.length=8", "--trace", "brp", ]) - .current_dir(dir) + .current_dir(&dir) .output() .unwrap(); // (1) the #168 refusal is lifted: the advertised flag runs instead of exit 2. @@ -1829,13 +1805,13 @@ fn reproduce_real_sweep_family_re_derives_bit_identically() { "--axis", "sma_signal.slow.length=8", "--name", "repro_sweep", ]) - .current_dir(dir) + .current_dir(&dir) .output() .unwrap(); assert!(mint.status.success(), "sweep mint stderr: {}", String::from_utf8_lossy(&mint.stderr)); - let id = family_id_of_kind(dir, "Sweep"); - let out = Command::new(BIN).args(["reproduce", &id]).current_dir(dir).output().unwrap(); + let id = family_id_of_kind(&dir, "Sweep"); + let out = Command::new(BIN).args(["reproduce", &id]).current_dir(&dir).output().unwrap(); let stdout = String::from_utf8_lossy(&out.stdout).into_owned(); assert!( out.status.success(), @@ -1984,13 +1960,13 @@ fn reproduce_real_walkforward_family_does_not_panic() { "--axis", "sma_signal.slow.length=8", "--name", "repro_wf", ]) - .current_dir(dir) + .current_dir(&dir) .output() .unwrap(); assert!(mint.status.success(), "walkforward mint stderr: {}", String::from_utf8_lossy(&mint.stderr)); - let id = family_id_of_kind(dir, "WalkForward"); - let out = Command::new(BIN).args(["reproduce", &id]).current_dir(dir).output().unwrap(); + let id = family_id_of_kind(&dir, "WalkForward"); + let out = Command::new(BIN).args(["reproduce", &id]).current_dir(&dir).output().unwrap(); let stdout = String::from_utf8_lossy(&out.stdout).into_owned(); let stderr = String::from_utf8_lossy(&out.stderr).into_owned(); assert_ne!( @@ -2147,7 +2123,7 @@ fn sweep_real_multi_column_blueprint_opens_high_and_low_columns() { "--axis", "hl_signal.fast.length=2,4", "--axis", "hl_signal.slow.length=8", ]) - .current_dir(dir) + .current_dir(&dir) .output() .unwrap(); assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr)); @@ -2232,7 +2208,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!( @@ -2288,7 +2264,7 @@ fn sweep_dissolved_accepts_an_axis_over_a_bound_param() { "--axis", "sma_signal.fast.length=2,3", "--name", "bound-override", ]) - .current_dir(dir) + .current_dir(&dir) .output() .expect("spawn aura sweep over a bound-param axis"); assert_eq!( @@ -2298,7 +2274,7 @@ fn sweep_dissolved_accepts_an_axis_over_a_bound_param() { path too, stderr: {}", String::from_utf8_lossy(&out.stderr) ); - let fams = Command::new(BIN).args(["runs", "families"]).current_dir(dir).output().unwrap(); + let fams = Command::new(BIN).args(["runs", "families"]).current_dir(&dir).output().unwrap(); assert!(fams.status.success(), "families exit: {:?}", fams.status); let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned(); assert_eq!(fams_out.lines().count(), 1, "exactly one family persisted: {fams_out}"); @@ -2731,7 +2707,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) { @@ -2777,7 +2753,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) { @@ -2834,7 +2810,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) { @@ -2882,7 +2858,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) { @@ -2944,7 +2920,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) { @@ -3009,7 +2985,7 @@ fn walkforward_dissolved_accepts_select_plateau() { "--select", "plateau:worst", "--from", "1735689600000", "--to", "1747353600000", ]) - .current_dir(cwd) + .current_dir(&cwd) .output() .expect("spawn aura"); if out.status.code() == Some(1) { @@ -3054,7 +3030,7 @@ fn walkforward_real_e2e_pins_the_exact_current_plateau_grade() { "--from", FROM_MS, "--to", TO_MS, "--select", "plateau:worst", ]) - .current_dir(cwd) + .current_dir(&cwd) .output() .expect("spawn aura"); if out.status.code() == Some(1) { @@ -3109,7 +3085,7 @@ fn walkforward_dissolved_defaults_a_single_omitted_knob() { "--stop-length", "3", "--from", "1735689600000", "--to", "1747353600000", ]) - .current_dir(cwd) + .current_dir(&cwd) .output() .expect("spawn aura"); if out.status.code() == Some(1) { @@ -3257,7 +3233,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) { @@ -3303,7 +3279,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(); @@ -3351,7 +3327,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) { @@ -3380,7 +3356,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!( @@ -3434,7 +3410,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) { @@ -3485,7 +3461,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(); @@ -3526,7 +3502,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") }; @@ -3582,7 +3558,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") }; @@ -3629,7 +3605,7 @@ fn generalize_without_explicit_window_resolves_a_shared_window_and_completes() { "--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) { @@ -3707,7 +3683,7 @@ fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols() "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", "--name", name, ]) - .current_dir(cwd) + .current_dir(&cwd) .output() .expect("spawn aura sweep"); assert_eq!( @@ -3739,7 +3715,7 @@ fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols() "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", "--name", "gen-both", ]) - .current_dir(cwd) + .current_dir(&cwd) .output() .expect("spawn aura generalize"); assert_eq!( @@ -3919,7 +3895,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) { @@ -3936,7 +3912,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); @@ -3981,7 +3957,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) { @@ -3997,7 +3973,7 @@ fn generalize_family_members_are_attributed_to_the_right_instrument() { 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); @@ -4048,7 +4024,7 @@ fn generalize_family_members_stamp_project_provenance() { "--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) { @@ -4064,7 +4040,7 @@ fn generalize_family_members_stamp_project_provenance() { 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); @@ -5083,7 +5059,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) { @@ -5140,7 +5116,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) { @@ -5195,7 +5171,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) { @@ -5249,7 +5225,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(); @@ -5305,7 +5281,7 @@ fn mc_real_fits_the_injected_roller_to_a_short_window() { "--block-len", "5", "--resamples", "100", "--seed", "42", "--from", SUB_FROM_MS, "--to", SUB_TO_MS, ]) - .current_dir(cwd) + .current_dir(&cwd) .output() .expect("spawn aura mc"); @@ -5414,7 +5390,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") }; @@ -5475,7 +5451,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) { @@ -5527,7 +5503,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) { @@ -5576,7 +5552,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) { diff --git a/crates/aura-cli/tests/common/mod.rs b/crates/aura-cli/tests/common/mod.rs index c3ba3f9..972eeec 100644 --- a/crates/aura-cli/tests/common/mod.rs +++ b/crates/aura-cli/tests/common/mod.rs @@ -3,14 +3,11 @@ //! and drive the `tests/fixtures/demo-project` fixture through the real //! `aura` binary (#223). //! -//! **Process-local serialization only.** [`project_lock`]'s `Mutex` is a -//! per-process `static`: it serializes test THREADS within one test-binary -//! process, but grants no cross-process exclusion. A process-parallel test -//! runner (e.g. `cargo-nextest`, which forks one process per test binary) — -//! or simply two concurrent `cargo test` invocations over the same working -//! tree — still race on the shared `tests/fixtures/demo-project/runs` store. -//! That boundary is a known, documented limitation (#223), not something -//! this module claims to close. +//! **Per-test project directories, no shared store.** [`fresh_project`] mints +//! a unique tempdir per test, wired to the shared, once-built fixture crate +//! via an absolute `[nodes]` pointer (`project.rs`'s pointer-arm routing), so +//! no two tests ever race on the same `runs/` store — libtest's default +//! thread parallelism applies without a serializing lock (#250). //! //! Each of the three test binaries compiles this file as its own `mod //! common`, so an item unused by one binary trips that binary's `-D @@ -18,7 +15,8 @@ //! rather than deleting a helper just because one binary doesn't need it. use std::path::{Path, PathBuf}; -use std::sync::{Mutex, MutexGuard, OnceLock}; +use std::sync::OnceLock; +use std::sync::atomic::{AtomicUsize, Ordering}; /// The demo-project fixture (`Aura.toml` present), built once per test-binary /// process. Each test binary has its own `OnceLock` (this module is compiled @@ -42,13 +40,45 @@ pub fn built_project() -> &'static PathBuf { }) } -/// Serializes every test that touches the shared demo-project fixture store -/// (they remove/re-seed `/runs`, so parallel test threads would race -/// on it). A poisoned lock is taken over: one failed test must not cascade -/// into unrelated lock panics. -pub fn project_lock() -> MutexGuard<'static, ()> { - static LOCK: Mutex<()> = Mutex::new(()); - LOCK.lock().unwrap_or_else(|e| e.into_inner()) +/// A per-test project directory, removed on drop (including mid-test panic). +#[allow(dead_code)] +pub struct FreshProject { + root: PathBuf, +} + +impl Drop for FreshProject { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.root); + } +} + +/// Mint a unique tempdir holding a 2-line data-level project (`Aura.toml` +/// with an absolute `[nodes]` pointer at the shared, once-built fixture +/// crate) plus a copy of `demo_signal.json`. Every test gets its own `runs/` +/// store under this root, so no two tests ever contend for the same store — +/// this is what lets `project_lock` disappear (#250). Bind the guard for the +/// whole test body: `let (dir, _g) = fresh_project();`. +#[allow(dead_code)] +pub fn fresh_project() -> (PathBuf, FreshProject) { + static COUNTER: AtomicUsize = AtomicUsize::new(0); + let root = std::env::temp_dir().join(format!( + "aura-e2e-{}-{}", + std::process::id(), + COUNTER.fetch_add(1, Ordering::Relaxed) + )); + std::fs::create_dir_all(&root).expect("create fresh project tempdir"); + let fixture = built_project(); + std::fs::write( + root.join("Aura.toml"), + format!( + "[paths]\nruns = \"runs\"\n\n[nodes]\ncrates = [\"{}\"]\n", + fixture.display() + ), + ) + .expect("write fresh project Aura.toml"); + std::fs::copy(fixture.join("demo_signal.json"), root.join("demo_signal.json")) + .expect("copy demo_signal.json into the fresh project"); + (root.clone(), FreshProject { root }) } /// A scratch filesystem entry a test writes under the git-tracked diff --git a/crates/aura-cli/tests/project_load.rs b/crates/aura-cli/tests/project_load.rs index b08013b..f979325 100644 --- a/crates/aura-cli/tests/project_load.rs +++ b/crates/aura-cli/tests/project_load.rs @@ -9,7 +9,7 @@ use std::process::{Command, Output}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; mod common; -use common::{built_project, project_lock}; +use common::built_project; fn fixture_dir() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/demo-project") @@ -98,7 +98,6 @@ fn outside_a_project_the_demo_blueprint_is_unknown() { /// rather than shell-relative (C17). #[test] fn project_registry_anchors_at_discovered_root_not_invocation_cwd() { - let _fixture = project_lock(); let dir = built_project(); let sub = dir.join("src"); let runs_dir = dir.join("runs"); diff --git a/crates/aura-cli/tests/research_docs.rs b/crates/aura-cli/tests/research_docs.rs index 5fd5c9a..4318fb4 100644 --- a/crates/aura-cli/tests/research_docs.rs +++ b/crates/aura-cli/tests/research_docs.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; mod common; -use common::{ScratchGuard, ScratchPath, built_project, project_lock}; +use common::{ScratchGuard, ScratchPath, fresh_project}; /// A fresh, unique working directory for a process test that persists /// content-addressed documents under `./runs/` (so `aura process register` @@ -304,8 +304,7 @@ fn process_register_stores_content_addressed_under_runs_root() { /// them together end to end. #[test] fn campaign_validate_in_project_reports_referential_tier_end_to_end() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -321,7 +320,7 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() { // test). let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let (sweep_out, sweep_code) = run_code_in( - dir, + &dir, &[ "sweep", &closed_bp, @@ -360,8 +359,8 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() { // overfit_probability (not per-member) flip to a per-member scalar. .replacen("\"overfit_probability\", \"cmp\": \"lt\", \"value\": 0.1", "\"win_rate\", \"cmp\": \"lt\", \"value\": 1.1", 1); assert_ne!(exec_process, PROCESS_DOC, "replacen must match the sweep stage"); - write_doc(dir, "seed.process.json", &exec_process); - let (proc_out, proc_code) = run_code_in(dir, &["process", "register", "seed.process.json"]); + write_doc(&dir, "seed.process.json", &exec_process); + let (proc_out, proc_code) = run_code_in(&dir, &["process", "register", "seed.process.json"]); assert_eq!(proc_code, Some(0), "seed process register failed: {proc_out}"); let proc_id = proc_out .lines() @@ -394,8 +393,8 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() { bp = bp_id, proc = proc_id ); - write_doc(dir, "ref-ok.campaign.json", &campaign); - let (out, code) = run_code_in(dir, &["campaign", "validate", "ref-ok.campaign.json"]); + write_doc(&dir, "ref-ok.campaign.json", &campaign); + let (out, code) = run_code_in(&dir, &["campaign", "validate", "ref-ok.campaign.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); assert!(out.contains( "campaign document valid (referential): all references resolve, axes are in the param space" @@ -404,8 +403,8 @@ fn campaign_validate_in_project_reports_referential_tier_end_to_end() { // An unresolved process reference: the fault block + `ref_fault_prose` // mapping, prose exit 1, at the same CLI seam. let bad = campaign.replace(&proc_id, "0000000000000000000000000000000000000000000000000000000000000000"); - write_doc(dir, "ref-bad.campaign.json", &bad); - let (out2, code2) = run_code_in(dir, &["campaign", "validate", "ref-bad.campaign.json"]); + write_doc(&dir, "ref-bad.campaign.json", &bad); + let (out2, code2) = run_code_in(&dir, &["campaign", "validate", "ref-bad.campaign.json"]); assert_eq!(code2, Some(1)); assert!(out2.contains("campaign references do not resolve:"), "stdout/stderr: {out2}"); assert!(out2.contains("not found in the project store"), "stdout/stderr: {out2}"); @@ -1129,9 +1128,8 @@ fn campaign_run_outside_project_refuses() { /// both readings (inside the project, past the project gate). #[test] fn campaign_run_bogus_target_refuses() { - let _fixture = project_lock(); - let dir = built_project(); - let (out, code) = run_code_in(dir, &["campaign", "run", "no-such-target"]); + let (dir, _fixture) = fresh_project(); + let (out, code) = run_code_in(&dir, &["campaign", "run", "no-such-target"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("'no-such-target' is neither a readable .json file nor a 64-hex content id"), @@ -1142,10 +1140,9 @@ fn campaign_run_bogus_target_refuses() { /// A well-formed but unknown content id refuses with the store-miss prose. #[test] fn campaign_run_unknown_id_refuses() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let id = "0".repeat(64); - let (out, code) = run_code_in(dir, &["campaign", "run", &id]); + let (out, code) = run_code_in(&dir, &["campaign", "run", &id]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains(&format!("no campaign {id} in the project store")), @@ -1169,8 +1166,7 @@ fn campaign_run_unknown_id_refuses() { /// bare-store-line contract exactly. #[test] fn campaign_runs_lists_and_dumps_the_bare_stored_record() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); std::fs::create_dir_all(&runs_dir).expect("create runs dir"); @@ -1185,7 +1181,7 @@ fn campaign_runs_lists_and_dumps_the_bare_stored_record() { // LIST mode: one scannable line per stored record, carrying the campaign // id (bare 64-hex or a prefix), exit 0. - let (list_out, list_code) = run_code_in(dir, &["campaign", "runs"]); + let (list_out, list_code) = run_code_in(&dir, &["campaign", "runs"]); assert_eq!(list_code, Some(0), "list exits 0: {list_out}"); assert!( list_out.contains(&campaign_id[..8]), @@ -1195,7 +1191,7 @@ fn campaign_runs_lists_and_dumps_the_bare_stored_record() { // DUMP mode: addressed by the campaign content id, prints the BARE stored // record line — the store form, NOT the `{{"campaign_run": …}}` run-emit // wrapper (F10) — byte-identical to the seeded line, exit 0. - let (dump_out, dump_code) = run_code_in(dir, &["campaign", "runs", &campaign_id]); + let (dump_out, dump_code) = run_code_in(&dir, &["campaign", "runs", &campaign_id]); assert_eq!(dump_code, Some(0), "dump exits 0: {dump_out}"); assert!( !dump_out.contains("\"campaign_run\""), @@ -1221,8 +1217,7 @@ fn campaign_runs_lists_and_dumps_the_bare_stored_record() { /// depends on. #[test] fn campaign_runs_dump_round_trips_a_populated_trace_name() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); std::fs::create_dir_all(&runs_dir).expect("create runs dir"); @@ -1235,7 +1230,7 @@ fn campaign_runs_dump_round_trips_a_populated_trace_name() { std::fs::write(runs_dir.join("campaign_runs.jsonl"), format!("{record_line}\n")) .expect("seed the campaign-run store"); - let (dump_out, dump_code) = run_code_in(dir, &["campaign", "runs", &campaign_id]); + let (dump_out, dump_code) = run_code_in(&dir, &["campaign", "runs", &campaign_id]); assert_eq!(dump_code, Some(0), "dump exits 0: {dump_out}"); assert_eq!( dump_out.trim(), @@ -1254,8 +1249,7 @@ fn campaign_runs_dump_round_trips_a_populated_trace_name() { /// runs, with Debug-free PipelineShape prose. #[test] fn campaign_run_refuses_mc_before_walk_forward() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -1263,10 +1257,10 @@ fn campaign_run_refuses_mc_before_walk_forward() { ScratchPath::File(dir.join("mcwf.process.json")), ScratchPath::File(dir.join("mcwf.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-mcwf-seed"); - let proc_id = register_process_doc(dir, "mcwf.process.json", MC_BEFORE_WF_PROCESS_DOC); - write_doc(dir, "mcwf.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); - let (out, code) = run_code_in(dir, &["campaign", "run", "mcwf.campaign.json"]); + let bp_id = seed_blueprint(&dir, "campaign-run-mcwf-seed"); + let proc_id = register_process_doc(&dir, "mcwf.process.json", MC_BEFORE_WF_PROCESS_DOC); + write_doc(&dir, "mcwf.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); + let (out, code) = run_code_in(&dir, &["campaign", "run", "mcwf.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!(out.contains("process pipeline is not executable:"), "stdout/stderr: {out}"); assert!( @@ -1304,8 +1298,7 @@ const SELECTION_FREE_SWEEP_THEN_GATE_PROCESS_DOC: &str = r#"{ /// nothing for a following stage to consume). #[test] fn campaign_run_refuses_a_non_terminal_selection_free_sweep() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -1313,11 +1306,11 @@ fn campaign_run_refuses_a_non_terminal_selection_free_sweep() { ScratchPath::File(dir.join("selfree.process.json")), ScratchPath::File(dir.join("selfree.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-selfree-seed"); + let bp_id = seed_blueprint(&dir, "campaign-run-selfree-seed"); let proc_id = - register_process_doc(dir, "selfree.process.json", SELECTION_FREE_SWEEP_THEN_GATE_PROCESS_DOC); - write_doc(dir, "selfree.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); - let (out, code) = run_code_in(dir, &["campaign", "run", "selfree.campaign.json"]); + register_process_doc(&dir, "selfree.process.json", SELECTION_FREE_SWEEP_THEN_GATE_PROCESS_DOC); + write_doc(&dir, "selfree.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); + let (out, code) = run_code_in(&dir, &["campaign", "run", "selfree.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains( @@ -1344,8 +1337,7 @@ const SELECTION_FREE_SWEEP_ONLY_PROCESS_DOC: &str = r#"{ #[test] fn campaign_validate_accepts_a_terminal_selection_free_sweep() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -1353,15 +1345,15 @@ fn campaign_validate_accepts_a_terminal_selection_free_sweep() { ScratchPath::File(dir.join("selfreeonly.process.json")), ScratchPath::File(dir.join("selfreeonly.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-validate-selfreeonly-seed"); + let bp_id = seed_blueprint(&dir, "campaign-validate-selfreeonly-seed"); let proc_id = - register_process_doc(dir, "selfreeonly.process.json", SELECTION_FREE_SWEEP_ONLY_PROCESS_DOC); + register_process_doc(&dir, "selfreeonly.process.json", SELECTION_FREE_SWEEP_ONLY_PROCESS_DOC); write_doc( - dir, + &dir, "selfreeonly.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""), ); - let (out, code) = run_code_in(dir, &["campaign", "validate", "selfreeonly.campaign.json"]); + let (out, code) = run_code_in(&dir, &["campaign", "validate", "selfreeonly.campaign.json"]); assert_eq!(code, Some(0), "a terminal selection-free sweep validates as executable: {out}"); assert!( out.contains("campaign document valid (executable): pipeline shape and static guards pass"), @@ -1378,8 +1370,7 @@ fn campaign_validate_accepts_a_terminal_selection_free_sweep() { /// single-instrument, and this refusal is data-free by construction). #[test] fn campaign_run_refuses_single_instrument_generalize() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -1387,10 +1378,10 @@ fn campaign_run_refuses_single_instrument_generalize() { ScratchPath::File(dir.join("gen1.process.json")), ScratchPath::File(dir.join("gen1.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-gen1-seed"); - let proc_id = register_process_doc(dir, "gen1.process.json", GENERALIZE_PROCESS_DOC); - write_doc(dir, "gen1.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); - let (out, code) = run_code_in(dir, &["campaign", "run", "gen1.campaign.json"]); + let bp_id = seed_blueprint(&dir, "campaign-run-gen1-seed"); + let proc_id = register_process_doc(&dir, "gen1.process.json", GENERALIZE_PROCESS_DOC); + write_doc(&dir, "gen1.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); + let (out, code) = run_code_in(&dir, &["campaign", "run", "gen1.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("std::generalize needs at least 2"), @@ -1423,8 +1414,7 @@ const GENERALIZE_NON_R_PROCESS_DOC: &str = r#"{ /// would silently accept a pip-denominated `std::generalize`. #[test] fn campaign_run_refuses_generalize_non_r_metric() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -1432,8 +1422,8 @@ fn campaign_run_refuses_generalize_non_r_metric() { ScratchPath::File(dir.join("genpip.process.json")), ScratchPath::File(dir.join("genpip.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-genpip-seed"); - let proc_id = register_process_doc(dir, "genpip.process.json", GENERALIZE_NON_R_PROCESS_DOC); + let bp_id = seed_blueprint(&dir, "campaign-run-genpip-seed"); + let proc_id = register_process_doc(&dir, "genpip.process.json", GENERALIZE_NON_R_PROCESS_DOC); // Two instruments (arity passes), so the metric guard is the one that // actually fires. The [1, 2] 1970 window never matters: this refusal is // data-free, before any member runs. @@ -1451,8 +1441,8 @@ fn campaign_run_refuses_generalize_non_r_metric() { "presentation": {{ "persist_taps": [], "emit": [] }} }}"# ); - write_doc(dir, "genpip.campaign.json", &campaign_doc); - let (out, code) = run_code_in(dir, &["campaign", "run", "genpip.campaign.json"]); + write_doc(&dir, "genpip.campaign.json", &campaign_doc); + let (out, code) = run_code_in(&dir, &["campaign", "run", "genpip.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); // #207 (fieldtest 0108 F8): the refusal names the REAL rule (the rankable // R-expectancy family), never the mislabel "pip metric" — max_r_drawdown is @@ -1505,8 +1495,7 @@ fn process_validate_permits_wf_after_mc_but_campaign_run_refuses_it() { ); // executor tier: campaign run's v2 preflight refuses the same shape. - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -1514,10 +1503,10 @@ fn process_validate_permits_wf_after_mc_but_campaign_run_refuses_it() { ScratchPath::File(dir.join("wfmc.process.json")), ScratchPath::File(dir.join("wfmc.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-wfmc-seed"); - let proc_id = register_process_doc(dir, "wfmc.process.json", WF_AFTER_MC_PROCESS_DOC); - write_doc(dir, "wfmc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); - let (out, code) = run_code_in(dir, &["campaign", "run", "wfmc.campaign.json"]); + let bp_id = seed_blueprint(&dir, "campaign-run-wfmc-seed"); + let proc_id = register_process_doc(&dir, "wfmc.process.json", WF_AFTER_MC_PROCESS_DOC); + write_doc(&dir, "wfmc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); + let (out, code) = run_code_in(&dir, &["campaign", "run", "wfmc.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("std::walk_forward cannot follow std::monte_carlo"), @@ -1545,8 +1534,7 @@ fn process_validate_permits_wf_after_mc_but_campaign_run_refuses_it() { /// shape validate clean (exit 0), the F6 friction. #[test] fn campaign_validate_runs_the_executor_preflight_as_a_third_tier() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -1556,15 +1544,15 @@ fn campaign_validate_runs_the_executor_preflight_as_a_third_tier() { ScratchPath::File(dir.join("wfmc.process.json")), ScratchPath::File(dir.join("wfmc.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-validate-3tier-seed"); + let bp_id = seed_blueprint(&dir, "campaign-validate-3tier-seed"); // Face 1 — the happy path: an executable pipeline shape (a bare // std::sweep). Intrinsic + referential pass today (exit 0), but the // executor tier is invisible; after #205 validate gains the third // "valid (executable)" line while staying exit 0. - let ok_proc = register_process_doc(dir, "runnable.process.json", SWEEP_ONLY_PROCESS_DOC); - write_doc(dir, "runnable.campaign.json", &campaign_doc_json(&bp_id, &ok_proc, (1, 2), "", "")); - let (ok_out, ok_code) = run_code_in(dir, &["campaign", "validate", "runnable.campaign.json"]); + let ok_proc = register_process_doc(&dir, "runnable.process.json", SWEEP_ONLY_PROCESS_DOC); + write_doc(&dir, "runnable.campaign.json", &campaign_doc_json(&bp_id, &ok_proc, (1, 2), "", "")); + let (ok_out, ok_code) = run_code_in(&dir, &["campaign", "validate", "runnable.campaign.json"]); assert_eq!(ok_code, Some(0), "an executable campaign validates clean: {ok_out}"); assert!( ok_out.contains("campaign document valid (referential): all references resolve"), @@ -1583,9 +1571,9 @@ fn campaign_validate_runs_the_executor_preflight_as_a_third_tier() { // reaches the third tier — which refuses the non-executable shape at // validate time (data-free: the [1, 2] 1970-epoch-ms window is never // reached), exit 1, with the executor fault header + the shape prose. - let bad_proc = register_process_doc(dir, "wfmc.process.json", WF_AFTER_MC_PROCESS_DOC); - write_doc(dir, "wfmc.campaign.json", &campaign_doc_json(&bp_id, &bad_proc, (1, 2), "", "")); - let (bad_out, bad_code) = run_code_in(dir, &["campaign", "validate", "wfmc.campaign.json"]); + let bad_proc = register_process_doc(&dir, "wfmc.process.json", WF_AFTER_MC_PROCESS_DOC); + write_doc(&dir, "wfmc.campaign.json", &campaign_doc_json(&bp_id, &bad_proc, (1, 2), "", "")); + let (bad_out, bad_code) = run_code_in(&dir, &["campaign", "validate", "wfmc.campaign.json"]); assert_eq!(bad_code, Some(1), "an executor-unrunnable shape refuses at validate: {bad_out}"); assert!( bad_out.contains("campaign is not executable:"), @@ -1639,8 +1627,7 @@ const WF_PLATEAU_GATED_PROCESS_DOC: &str = r#"{ /// exit 0 — either direction goes red here. #[test] fn campaign_validate_narrows_plateau_in_wf_refusal_to_gate_preceded_stages() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -1650,19 +1637,19 @@ fn campaign_validate_narrows_plateau_in_wf_refusal_to_gate_preceded_stages() { ScratchPath::File(dir.join("wf-plateau-gated.process.json")), ScratchPath::File(dir.join("wf-plateau-gated.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-validate-wf-plateau-seed"); + let bp_id = seed_blueprint(&dir, "campaign-validate-wf-plateau-seed"); // Face 1 — gate-free: sweep -> walk_forward(plateau:worst). The lattice // is intact, so the executor tier accepts it (exit 0). let free_id = - register_process_doc(dir, "wf-plateau-free.process.json", WF_PLATEAU_GATE_FREE_PROCESS_DOC); + register_process_doc(&dir, "wf-plateau-free.process.json", WF_PLATEAU_GATE_FREE_PROCESS_DOC); write_doc( - dir, + &dir, "wf-plateau-free.campaign.json", &campaign_doc_json(&bp_id, &free_id, (1, 2), "", ""), ); let (free_out, free_code) = - run_code_in(dir, &["campaign", "validate", "wf-plateau-free.campaign.json"]); + run_code_in(&dir, &["campaign", "validate", "wf-plateau-free.campaign.json"]); assert_eq!(free_code, Some(0), "gate-free plateau walk_forward validates clean: {free_out}"); assert!( free_out.contains( @@ -1675,14 +1662,14 @@ fn campaign_validate_narrows_plateau_in_wf_refusal_to_gate_preceded_stages() { // The gate can break the lattice, so the executor tier still refuses it // (exit 1), naming the offending stage. let gated_id = - register_process_doc(dir, "wf-plateau-gated.process.json", WF_PLATEAU_GATED_PROCESS_DOC); + register_process_doc(&dir, "wf-plateau-gated.process.json", WF_PLATEAU_GATED_PROCESS_DOC); write_doc( - dir, + &dir, "wf-plateau-gated.campaign.json", &campaign_doc_json(&bp_id, &gated_id, (1, 2), "", ""), ); let (gated_out, gated_code) = - run_code_in(dir, &["campaign", "validate", "wf-plateau-gated.campaign.json"]); + run_code_in(&dir, &["campaign", "validate", "wf-plateau-gated.campaign.json"]); assert_eq!(gated_code, Some(1), "gate-preceded plateau walk_forward still refuses: {gated_out}"); assert!( gated_out.contains("campaign is not executable:"), @@ -1717,8 +1704,7 @@ const ZERO_RESAMPLES_MC_PROCESS_DOC: &str = r#"{ /// would let this refusal disappear or drift to a different message. #[test] fn campaign_run_refuses_zero_resamples_monte_carlo_before_any_member_runs() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -1726,10 +1712,10 @@ fn campaign_run_refuses_zero_resamples_monte_carlo_before_any_member_runs() { ScratchPath::File(dir.join("zeromc.process.json")), ScratchPath::File(dir.join("zeromc.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-zeromc-seed"); - let proc_id = register_process_doc(dir, "zeromc.process.json", ZERO_RESAMPLES_MC_PROCESS_DOC); - write_doc(dir, "zeromc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); - let (out, code) = run_code_in(dir, &["campaign", "run", "zeromc.campaign.json"]); + let bp_id = seed_blueprint(&dir, "campaign-run-zeromc-seed"); + let proc_id = register_process_doc(&dir, "zeromc.process.json", ZERO_RESAMPLES_MC_PROCESS_DOC); + write_doc(&dir, "zeromc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); + let (out, code) = run_code_in(&dir, &["campaign", "run", "zeromc.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("process stage 1: monte_carlo resamples must be > 0"), @@ -1749,8 +1735,7 @@ fn campaign_run_refuses_zero_resamples_monte_carlo_before_any_member_runs() { /// pinned here. #[test] fn campaign_run_refuses_unknown_tap_at_validate() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -1758,14 +1743,14 @@ fn campaign_run_refuses_unknown_tap_at_validate() { ScratchPath::File(dir.join("badtap.process.json")), ScratchPath::File(dir.join("badtap.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-badtap-seed"); - let proc_id = register_process_doc(dir, "badtap.process.json", SWEEP_ONLY_PROCESS_DOC); + let bp_id = seed_blueprint(&dir, "campaign-run-badtap-seed"); + let proc_id = register_process_doc(&dir, "badtap.process.json", SWEEP_ONLY_PROCESS_DOC); write_doc( - dir, + &dir, "badtap.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"r_record\"", ""), ); - let (out, code) = run_code_in(dir, &["campaign", "run", "badtap.campaign.json"]); + let (out, code) = run_code_in(&dir, &["campaign", "run", "badtap.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!(out.contains("campaign document invalid:"), "stdout/stderr: {out}"); assert!( @@ -1794,8 +1779,7 @@ fn campaign_run_refuses_unknown_tap_at_validate() { /// tightens this test to pin its absence. #[test] fn campaign_run_valid_tap_reaches_the_member_data_seam() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -1803,14 +1787,14 @@ fn campaign_run_valid_tap_reaches_the_member_data_seam() { ScratchPath::File(dir.join("taps.process.json")), ScratchPath::File(dir.join("taps.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-taps-seed"); - let proc_id = register_process_doc(dir, "taps.process.json", SWEEP_ONLY_PROCESS_DOC); + let bp_id = seed_blueprint(&dir, "campaign-run-taps-seed"); + let proc_id = register_process_doc(&dir, "taps.process.json", SWEEP_ONLY_PROCESS_DOC); write_doc( - dir, + &dir, "taps.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"equity\"", ""), ); - let (out, code) = run_code_in(dir, &["campaign", "run", "taps.campaign.json"]); + let (out, code) = run_code_in(&dir, &["campaign", "run", "taps.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( !out.contains("unknown tap"), @@ -1849,8 +1833,7 @@ fn campaign_run_valid_tap_reaches_the_member_data_seam() { /// stays green on a data-less machine. #[test] fn campaign_run_real_e2e_sweep_gate_walkforward() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -1858,12 +1841,12 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() { ScratchPath::File(dir.join("wf.process.json")), ScratchPath::File(dir.join("wf.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-e2e-seed"); - let proc_id = register_process_doc(dir, "wf.process.json", WF_PROCESS_DOC); + let bp_id = seed_blueprint(&dir, "campaign-run-e2e-seed"); + let proc_id = register_process_doc(&dir, "wf.process.json", WF_PROCESS_DOC); // The GER40 Sept-2024 window (inclusive Unix-ms) — the same gated window // cli_run.rs drives; ~30 days, so the (14d, 7d, 7d) roller tiles it. write_doc( - dir, + &dir, "wf.campaign.json", &campaign_doc_json( &bp_id, @@ -1873,7 +1856,7 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() { "\"family_table\", \"selection_report\"", ), ); - let (out, code) = run_code_in(dir, &["campaign", "run", "wf.campaign.json"]); + let (out, code) = run_code_in(&dir, &["campaign", "run", "wf.campaign.json"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(1) @@ -1999,7 +1982,7 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() { // The shipped viewer reads the member back (the cli_run.rs chart // idiom): exit 0 and an HTML page carrying both persisted tap series. let (chart_out, chart_code) = - run_code_in(dir, &["chart", &format!("{trace_name}/{cell_key}")]); + run_code_in(&dir, &["chart", &format!("{trace_name}/{cell_key}")]); assert_eq!(chart_code, Some(0), "aura chart reads the persisted member: {chart_out}"); assert!( chart_out.contains("\"equity\"") && chart_out.contains("\"r_equity\""), @@ -2015,8 +1998,7 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() { /// Gated on the local GER40 archive; skips cleanly when absent. #[test] fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -2025,8 +2007,8 @@ fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() { ScratchPath::File(dir.join("close.campaign.json")), ScratchPath::File(dir.join("open.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "binding-override-seed"); - let proc_id = register_process_doc(dir, "sweep.process.json", SWEEP_ONLY_PROCESS_DOC); + let bp_id = seed_blueprint(&dir, "binding-override-seed"); + let proc_id = register_process_doc(&dir, "sweep.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, @@ -2039,10 +2021,10 @@ fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() { r#""data": { "bindings": { "price": "open" }, "instruments": ["GER40"],"#, ); assert_ne!(rebound, base, "the override splice must hit the data section"); - write_doc(dir, "close.campaign.json", &base); - write_doc(dir, "open.campaign.json", &rebound); + write_doc(&dir, "close.campaign.json", &base); + write_doc(&dir, "open.campaign.json", &rebound); - let (out_close, code_close) = run_code_in(dir, &["campaign", "run", "close.campaign.json"]); + let (out_close, code_close) = run_code_in(&dir, &["campaign", "run", "close.campaign.json"]); if code_close == Some(1) && (out_close.contains("no recorded geometry") || out_close.contains("no data for instrument")) { @@ -2050,7 +2032,7 @@ fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() { return; } assert_eq!(code_close, Some(0), "stdout/stderr: {out_close}"); - let (out_open, code_open) = run_code_in(dir, &["campaign", "run", "open.campaign.json"]); + let (out_open, code_open) = run_code_in(&dir, &["campaign", "run", "open.campaign.json"]); assert_eq!(code_open, Some(0), "stdout/stderr: {out_open}"); let first_member = |out: &str| -> serde_json::Value { @@ -2081,8 +2063,7 @@ fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() { /// above; skips on a data-less host. #[test] fn campaign_run_real_e2e_non_default_regime_stamps_its_own_stop() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -2090,8 +2071,8 @@ fn campaign_run_real_e2e_non_default_regime_stamps_its_own_stop() { ScratchPath::File(dir.join("regime.process.json")), ScratchPath::File(dir.join("regime.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-regime-seed"); - let proc_id = register_process_doc(dir, "regime.process.json", SWEEP_ONLY_PROCESS_DOC); + let bp_id = seed_blueprint(&dir, "campaign-run-regime-seed"); + let proc_id = register_process_doc(&dir, "regime.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, @@ -2105,8 +2086,8 @@ fn campaign_run_real_e2e_non_default_regime_stamps_its_own_stop() { 1, ); assert_ne!(with_regime, base, "replacen must actually match the fixture's seed field"); - write_doc(dir, "regime.campaign.json", &with_regime); - let (out, code) = run_code_in(dir, &["campaign", "run", "regime.campaign.json"]); + write_doc(&dir, "regime.campaign.json", &with_regime); + let (out, code) = run_code_in(&dir, &["campaign", "run", "regime.campaign.json"]); if code == Some(1) && (out.contains("no recorded geometry") || out.contains("no data for instrument")) @@ -2151,8 +2132,7 @@ fn campaign_run_real_e2e_non_default_regime_stamps_its_own_stop() { /// on the local GER40 archive like its siblings; skips on a data-less host. #[test] fn campaign_run_real_e2e_two_regimes_each_stamp_their_own_stop() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -2160,8 +2140,8 @@ fn campaign_run_real_e2e_two_regimes_each_stamp_their_own_stop() { ScratchPath::File(dir.join("tworegimes.process.json")), ScratchPath::File(dir.join("tworegimes.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-two-regimes-seed"); - let proc_id = register_process_doc(dir, "tworegimes.process.json", SWEEP_ONLY_PROCESS_DOC); + let bp_id = seed_blueprint(&dir, "campaign-run-two-regimes-seed"); + let proc_id = register_process_doc(&dir, "tworegimes.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, @@ -2176,8 +2156,8 @@ fn campaign_run_real_e2e_two_regimes_each_stamp_their_own_stop() { 1, ); assert_ne!(with_regimes, base, "replacen must actually match the fixture's seed field"); - write_doc(dir, "tworegimes.campaign.json", &with_regimes); - let (out, code) = run_code_in(dir, &["campaign", "run", "tworegimes.campaign.json"]); + write_doc(&dir, "tworegimes.campaign.json", &with_regimes); + let (out, code) = run_code_in(&dir, &["campaign", "run", "tworegimes.campaign.json"]); if code == Some(1) && (out.contains("no recorded geometry") || out.contains("no data for instrument")) @@ -2236,8 +2216,7 @@ fn campaign_run_real_e2e_two_regimes_each_stamp_their_own_stop() { /// archive like its siblings; skips on a data-less host. #[test] fn campaign_persist_non_default_regime_does_not_false_fail_c1() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -2245,9 +2224,9 @@ fn campaign_persist_non_default_regime_does_not_false_fail_c1() { ScratchPath::File(dir.join("regime-persist.process.json")), ScratchPath::File(dir.join("regime-persist.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-persist-regime-seed"); + let bp_id = seed_blueprint(&dir, "campaign-persist-regime-seed"); let proc_id = - register_process_doc(dir, "regime-persist.process.json", SWEEP_ONLY_PROCESS_DOC); + register_process_doc(&dir, "regime-persist.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, @@ -2264,8 +2243,8 @@ fn campaign_persist_non_default_regime_does_not_false_fail_c1() { 1, ); assert_ne!(with_regime, base, "replacen must actually match the fixture's seed field"); - write_doc(dir, "regime-persist.campaign.json", &with_regime); - let (out, code) = run_code_in(dir, &["campaign", "run", "regime-persist.campaign.json"]); + write_doc(&dir, "regime-persist.campaign.json", &with_regime); + let (out, code) = run_code_in(&dir, &["campaign", "run", "regime-persist.campaign.json"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(1) @@ -2321,8 +2300,7 @@ fn campaign_persist_non_default_regime_does_not_false_fail_c1() { /// the local GER40 archive; skips on a data-less host. #[test] fn campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -2330,8 +2308,8 @@ fn campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm() { ScratchPath::File(dir.join("cost-persist.process.json")), ScratchPath::File(dir.join("cost-persist.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-persist-cost-seed"); - let proc_id = register_process_doc(dir, "cost-persist.process.json", SWEEP_ONLY_PROCESS_DOC); + let bp_id = seed_blueprint(&dir, "campaign-persist-cost-seed"); + let proc_id = register_process_doc(&dir, "cost-persist.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, @@ -2345,8 +2323,8 @@ fn campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm() { 1, ); assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field"); - write_doc(dir, "cost-persist.campaign.json", &with_cost); - let (out, code) = run_code_in(dir, &["campaign", "run", "cost-persist.campaign.json"]); + write_doc(&dir, "cost-persist.campaign.json", &with_cost); + let (out, code) = run_code_in(&dir, &["campaign", "run", "cost-persist.campaign.json"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(1) @@ -2417,8 +2395,7 @@ fn campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm() { /// archive; skips on a data-less host. #[test] fn campaign_run_real_e2e_net_r_equity_tap_persists_the_cost_adjusted_curve() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -2426,8 +2403,8 @@ fn campaign_run_real_e2e_net_r_equity_tap_persists_the_cost_adjusted_curve() { ScratchPath::File(dir.join("net-persist.process.json")), ScratchPath::File(dir.join("net-persist.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-persist-net-seed"); - let proc_id = register_process_doc(dir, "net-persist.process.json", SWEEP_ONLY_PROCESS_DOC); + let bp_id = seed_blueprint(&dir, "campaign-persist-net-seed"); + let proc_id = register_process_doc(&dir, "net-persist.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, @@ -2441,8 +2418,8 @@ fn campaign_run_real_e2e_net_r_equity_tap_persists_the_cost_adjusted_curve() { 1, ); assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field"); - write_doc(dir, "net-persist.campaign.json", &with_cost); - let (out, code) = run_code_in(dir, &["campaign", "run", "net-persist.campaign.json"]); + write_doc(&dir, "net-persist.campaign.json", &with_cost); + let (out, code) = run_code_in(&dir, &["campaign", "run", "net-persist.campaign.json"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(1) @@ -2531,8 +2508,7 @@ fn campaign_run_real_e2e_net_r_equity_tap_persists_the_cost_adjusted_curve() { /// wired). Gated on the local GER40 archive; skips on a data-less host. #[test] fn campaign_run_real_e2e_vol_slippage_cost_persists_without_drift_alarm() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -2540,9 +2516,9 @@ fn campaign_run_real_e2e_vol_slippage_cost_persists_without_drift_alarm() { ScratchPath::File(dir.join("volcost-persist.process.json")), ScratchPath::File(dir.join("volcost-persist.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-persist-volcost-seed"); + let bp_id = seed_blueprint(&dir, "campaign-persist-volcost-seed"); let proc_id = - register_process_doc(dir, "volcost-persist.process.json", SWEEP_ONLY_PROCESS_DOC); + register_process_doc(&dir, "volcost-persist.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, @@ -2556,8 +2532,8 @@ fn campaign_run_real_e2e_vol_slippage_cost_persists_without_drift_alarm() { 1, ); assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field"); - write_doc(dir, "volcost-persist.campaign.json", &with_cost); - let (out, code) = run_code_in(dir, &["campaign", "run", "volcost-persist.campaign.json"]); + write_doc(&dir, "volcost-persist.campaign.json", &with_cost); + let (out, code) = run_code_in(&dir, &["campaign", "run", "volcost-persist.campaign.json"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(1) @@ -2617,8 +2593,7 @@ fn campaign_run_real_e2e_vol_slippage_cost_persists_without_drift_alarm() { /// into one dir. Gated on the local GER40 archive; skips on a data-less host. #[test] fn campaign_persist_two_regimes_land_in_distinct_trace_dirs() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -2626,9 +2601,9 @@ fn campaign_persist_two_regimes_land_in_distinct_trace_dirs() { ScratchPath::File(dir.join("tworegimes-persist.process.json")), ScratchPath::File(dir.join("tworegimes-persist.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-persist-two-regimes-seed"); + let bp_id = seed_blueprint(&dir, "campaign-persist-two-regimes-seed"); let proc_id = register_process_doc( - dir, + &dir, "tworegimes-persist.process.json", SWEEP_ONLY_PROCESS_DOC, ); @@ -2646,8 +2621,8 @@ fn campaign_persist_two_regimes_land_in_distinct_trace_dirs() { 1, ); assert_ne!(with_regimes, base, "replacen must actually match the fixture's seed field"); - write_doc(dir, "tworegimes-persist.campaign.json", &with_regimes); - let (out, code) = run_code_in(dir, &["campaign", "run", "tworegimes-persist.campaign.json"]); + write_doc(&dir, "tworegimes-persist.campaign.json", &with_regimes); + let (out, code) = run_code_in(&dir, &["campaign", "run", "tworegimes-persist.campaign.json"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(1) @@ -2704,8 +2679,7 @@ fn campaign_persist_two_regimes_land_in_distinct_trace_dirs() { /// host like its siblings. #[test] fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -2713,10 +2687,10 @@ fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() { ScratchPath::File(dir.join("mixedtap.process.json")), ScratchPath::File(dir.join("mixedtap.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-mixedtap-seed"); - let proc_id = register_process_doc(dir, "mixedtap.process.json", SWEEP_ONLY_PROCESS_DOC); + let bp_id = seed_blueprint(&dir, "campaign-run-mixedtap-seed"); + let proc_id = register_process_doc(&dir, "mixedtap.process.json", SWEEP_ONLY_PROCESS_DOC); write_doc( - dir, + &dir, "mixedtap.campaign.json", &campaign_doc_json( &bp_id, @@ -2726,7 +2700,7 @@ fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() { "", ), ); - let (out, code) = run_code_in(dir, &["campaign", "run", "mixedtap.campaign.json"]); + let (out, code) = run_code_in(&dir, &["campaign", "run", "mixedtap.campaign.json"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(1) @@ -2781,8 +2755,7 @@ fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() { /// manifests). Skips on a data-less host like its siblings. #[test] fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -2791,8 +2764,8 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() { ScratchPath::File(dir.join("costless.campaign.json")), ScratchPath::File(dir.join("costnet.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-costnet-seed"); - let proc_id = register_process_doc(dir, "costnet.process.json", SWEEP_ONLY_PROCESS_DOC); + let bp_id = seed_blueprint(&dir, "campaign-run-costnet-seed"); + let proc_id = register_process_doc(&dir, "costnet.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, @@ -2809,8 +2782,8 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() { 1, ); assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field"); - write_doc(dir, "costless.campaign.json", &base); - write_doc(dir, "costnet.campaign.json", &with_cost); + write_doc(&dir, "costless.campaign.json", &base); + write_doc(&dir, "costnet.campaign.json", &with_cost); // Per-member (axis-params key -> (expectancy_r, net_expectancy_r)), // parsed from the emitted family_table member lines. Cost stamps are @@ -2843,7 +2816,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() { // (a) the cost-less baseline: net == gross, and net_r_equity skips with // the remedy notice while equity still persists. - let (out_gross, code_gross) = run_code_in(dir, &["campaign", "run", "costless.campaign.json"]); + let (out_gross, code_gross) = run_code_in(&dir, &["campaign", "run", "costless.campaign.json"]); if code_gross == Some(1) && (out_gross.contains("no recorded geometry") || out_gross.contains("no data for instrument")) { @@ -2865,7 +2838,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() { // (b) the costed run: gross identical member-for-member, net strictly // below gross, no skip notice, net_r_equity persisted on disk. - let (out_net, code_net) = run_code_in(dir, &["campaign", "run", "costnet.campaign.json"]); + let (out_net, code_net) = run_code_in(&dir, &["campaign", "run", "costnet.campaign.json"]); assert_eq!(code_net, Some(0), "costed run: {out_net}"); assert!( !out_net.contains("skipped"), @@ -2916,7 +2889,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() { .to_string() }) .expect("a costed member line carries the family id"); - let (rep_out, rep_code) = run_code_in(dir, &["reproduce", &family_id]); + let (rep_out, rep_code) = run_code_in(&dir, &["reproduce", &family_id]); assert_eq!(rep_code, Some(0), "reproduce: {rep_out}"); assert!( rep_out.contains("reproduced 4/4 members bit-identically"), @@ -2939,8 +2912,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() { /// local GER40 archive; skips on a data-less host. #[test] fn campaign_run_real_e2e_carry_cost_persists_without_drift_alarm() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -2948,9 +2920,9 @@ fn campaign_run_real_e2e_carry_cost_persists_without_drift_alarm() { ScratchPath::File(dir.join("carrycost-persist.process.json")), ScratchPath::File(dir.join("carrycost-persist.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-persist-carrycost-seed"); + let bp_id = seed_blueprint(&dir, "campaign-persist-carrycost-seed"); let proc_id = - register_process_doc(dir, "carrycost-persist.process.json", SWEEP_ONLY_PROCESS_DOC); + register_process_doc(&dir, "carrycost-persist.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, @@ -2964,8 +2936,8 @@ fn campaign_run_real_e2e_carry_cost_persists_without_drift_alarm() { 1, ); assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field"); - write_doc(dir, "carrycost-persist.campaign.json", &with_cost); - let (out, code) = run_code_in(dir, &["campaign", "run", "carrycost-persist.campaign.json"]); + write_doc(&dir, "carrycost-persist.campaign.json", &with_cost); + let (out, code) = run_code_in(&dir, &["campaign", "run", "carrycost-persist.campaign.json"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(1) @@ -3021,8 +2993,7 @@ fn campaign_run_real_e2e_carry_cost_persists_without_drift_alarm() { /// data-free refusal fixture). #[test] fn campaign_run_real_e2e_sweep_monte_carlo_per_survivor() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -3030,15 +3001,15 @@ fn campaign_run_real_e2e_sweep_monte_carlo_per_survivor() { ScratchPath::File(dir.join("mc.process.json")), ScratchPath::File(dir.join("mc.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-mc-persurvivor-seed"); - let proc_id = register_process_doc(dir, "mc.process.json", MC_PROCESS_DOC); + let bp_id = seed_blueprint(&dir, "campaign-run-mc-persurvivor-seed"); + let proc_id = register_process_doc(&dir, "mc.process.json", MC_PROCESS_DOC); // Same GER40 Sept-2024 window as the gated wf e2e above. write_doc( - dir, + &dir, "mc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1725148800000, 1727740799999), "", ""), ); - let (out, code) = run_code_in(dir, &["campaign", "run", "mc.campaign.json"]); + let (out, code) = run_code_in(&dir, &["campaign", "run", "mc.campaign.json"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(1) @@ -3095,8 +3066,7 @@ fn campaign_run_real_e2e_sweep_monte_carlo_per_survivor() { /// prose either way (the message depends only on env/instrument/window). #[test] fn campaign_run_by_content_id_matches_file_sugar_refusal() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -3104,14 +3074,14 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() { ScratchPath::File(dir.join("mc2.process.json")), ScratchPath::File(dir.join("mc2.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-id-seed"); - let proc_id = register_process_doc(dir, "mc2.process.json", MC_PROCESS_DOC); - write_doc(dir, "mc2.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); + let bp_id = seed_blueprint(&dir, "campaign-run-id-seed"); + let proc_id = register_process_doc(&dir, "mc2.process.json", MC_PROCESS_DOC); + write_doc(&dir, "mc2.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); - let (file_out, file_code) = run_code_in(dir, &["campaign", "run", "mc2.campaign.json"]); + let (file_out, file_code) = run_code_in(&dir, &["campaign", "run", "mc2.campaign.json"]); assert_eq!(file_code, Some(1), "stdout/stderr: {file_out}"); - let (reg_out, reg_code) = run_code_in(dir, &["campaign", "register", "mc2.campaign.json"]); + let (reg_out, reg_code) = run_code_in(&dir, &["campaign", "register", "mc2.campaign.json"]); assert_eq!(reg_code, Some(0), "register failed: {reg_out}"); let id = reg_out .lines() @@ -3123,7 +3093,7 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() { .expect("id") .trim_start_matches("content:") .to_string(); - let (id_out, id_code) = run_code_in(dir, &["campaign", "run", &id]); + let (id_out, id_code) = run_code_in(&dir, &["campaign", "run", &id]); assert_eq!(id_code, Some(1), "stdout/stderr: {id_out}"); let file_line = file_out.lines().find(|l| l.starts_with("aura: ")).expect("file refusal line"); @@ -3146,8 +3116,7 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() { /// needed. #[test] fn campaign_run_tolerates_content_prefix_on_target() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -3155,11 +3124,11 @@ fn campaign_run_tolerates_content_prefix_on_target() { ScratchPath::File(dir.join("prefix.process.json")), ScratchPath::File(dir.join("prefix.campaign.json")), ]); - let bp_id = seed_blueprint(dir, "campaign-run-prefix-seed"); - let proc_id = register_process_doc(dir, "prefix.process.json", MC_PROCESS_DOC); - write_doc(dir, "prefix.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); + let bp_id = seed_blueprint(&dir, "campaign-run-prefix-seed"); + let proc_id = register_process_doc(&dir, "prefix.process.json", MC_PROCESS_DOC); + write_doc(&dir, "prefix.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); - let (reg_out, reg_code) = run_code_in(dir, &["campaign", "register", "prefix.campaign.json"]); + let (reg_out, reg_code) = run_code_in(&dir, &["campaign", "register", "prefix.campaign.json"]); assert_eq!(reg_code, Some(0), "register failed: {reg_out}"); let id = reg_out .lines() @@ -3172,8 +3141,8 @@ fn campaign_run_tolerates_content_prefix_on_target() { .trim_start_matches("content:") .to_string(); - let (bare_out, _) = run_code_in(dir, &["campaign", "run", &id]); - let (pfx_out, _) = run_code_in(dir, &["campaign", "run", &format!("content:{id}")]); + let (bare_out, _) = run_code_in(&dir, &["campaign", "run", &id]); + let (pfx_out, _) = run_code_in(&dir, &["campaign", "run", &format!("content:{id}")]); let pfx_line = pfx_out.lines().find(|l| l.starts_with("aura: ")).expect("prefixed refusal line"); assert!( @@ -3201,8 +3170,7 @@ fn campaign_run_tolerates_content_prefix_on_target() { /// data-free refusal) rather than failing earlier at referencing or binding. #[test] fn campaign_run_accepts_a_graph_register_seeded_strategy() { - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let _cleanup = ScratchGuard(vec![ @@ -3211,7 +3179,7 @@ fn campaign_run_accepts_a_graph_register_seeded_strategy() { ScratchPath::File(dir.join("onramp.campaign.json")), ]); let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); - let (reg_out, reg_code) = run_code_in(dir, &["graph", "register", &closed_bp]); + let (reg_out, reg_code) = run_code_in(&dir, &["graph", "register", &closed_bp]); assert_eq!(reg_code, Some(0), "graph register failed: {reg_out}"); let bp_id = reg_out .lines() @@ -3224,9 +3192,9 @@ fn campaign_run_accepts_a_graph_register_seeded_strategy() { .trim_start_matches("content:") .to_string(); - let proc_id = register_process_doc(dir, "onramp.process.json", SWEEP_ONLY_PROCESS_DOC); - write_doc(dir, "onramp.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); - let (out, code) = run_code_in(dir, &["campaign", "run", "onramp.campaign.json"]); + let proc_id = register_process_doc(&dir, "onramp.process.json", SWEEP_ONLY_PROCESS_DOC); + write_doc(&dir, "onramp.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); + let (out, code) = run_code_in(&dir, &["campaign", "run", "onramp.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("no recorded geometry") || out.contains("no data for instrument"), @@ -3248,15 +3216,14 @@ fn campaign_run_invalid_file_refuses_before_touching_store() { // Run inside the built project (campaign run needs one, per the project // gate); the document fails intrinsic validation before any store or // referential check is reached, so the project's own store is untouched. - let _fixture = project_lock(); - let dir = built_project(); + let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let bad = CAMPAIGN_DOC.replacen(r#""values": [8]"#, r#""values": []"#, 1); - let bad_path = write_doc(dir, "bad.campaign.json", &bad); + let bad_path = write_doc(&dir, "bad.campaign.json", &bad); let _cleanup = ScratchGuard(vec![ScratchPath::Dir(runs_dir.clone()), ScratchPath::File(bad_path)]); - let (out, code) = run_code_in(dir, &["campaign", "run", "bad.campaign.json"]); + let (out, code) = run_code_in(&dir, &["campaign", "run", "bad.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("aura: campaign document invalid:"), -- 2.52.0 From c31e946bb7ce0ffb7edb5b53279d3e48e11b2bd3 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 13 Jul 2026 15:24:00 +0200 Subject: [PATCH 4/8] =?UTF-8?q?test(cli):=20synthetic=20per-test=20M1=20ar?= =?UTF-8?q?chive=20=E2=80=94=20no-window=20generalize=20tests=20go=20hostl?= =?UTF-8?q?ess?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two generalize E2E tests whose property is no-window span resolution (shared-window fallback, intersection semantics) no longer stream the 6.6 GB host archive: fresh_project_with_data() generates a deterministic two-symbol M1 archive (SYMA 2024-01..08, SYMB 2024-03..06 strictly inside it, so the intersection differs from symbols[0]'s window on both bounds) in the exact zip/48-byte-record format data-server reads, plus geometry sidecars, under the per-test project's paths.data. Both tests drop their local_data_present gates — they now run on any host, data mount or not. ~35s and ~50s become 0.74s and 1.14s; the full cli_run binary lands at ~25s (211s at the branch base). Full workspace suite: 247s -> 45.7s. refs #250 --- Cargo.lock | 1 + crates/aura-cli/Cargo.toml | 7 + crates/aura-cli/tests/cli_run.rs | 59 ++++---- crates/aura-cli/tests/common/mod.rs | 211 +++++++++++++++++++++++++--- 4 files changed, 226 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 44d3cec..ab4ebe9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,6 +138,7 @@ dependencies = [ "serde_json", "sha2", "toml", + "zip", ] [[package]] diff --git a/crates/aura-cli/Cargo.toml b/crates/aura-cli/Cargo.toml index d6d4d61..e988e30 100644 --- a/crates/aura-cli/Cargo.toml +++ b/crates/aura-cli/Cargo.toml @@ -53,3 +53,10 @@ toml = "0.8" # long-option abbreviation. Research-side CLI only (invariant 8): a dev-loop # compile tax, never a frozen-artifact tax. clap = { version = "4", features = ["derive"] } + +[dev-dependencies] +# zip: hand-packs a tiny synthetic M1 archive (tests/common/mod.rs) in the +# exact on-disk format data-server reads — already transitive via the +# data-server dependency above; declared directly here so test code may +# `use zip::...` (#250, no new dependency actually enters the build graph). +zip = "2" diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index f12342e..1fb67de 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -5,7 +5,7 @@ use std::path::Path; use std::process::Command; mod common; -use common::fresh_project; +use common::{fresh_project, fresh_project_with_data}; /// Path to the freshly-built `aura` binary (Cargo sets this env var for the test /// crate; the binary is named `aura` in `Cargo.toml`). @@ -3594,30 +3594,24 @@ fn generalize_distinct_invocations_persist_distinct_campaign_documents() { /// independent full window). A wrong intersection (or a panic on the /// per-symbol probe) in that fallback would only surface when `--from`/`--to` /// are absent — every other generalize e2e pins the explicit-window path, -/// leaving this one unexercised. Gated on local GER40/USDJPY data. +/// leaving this one unexercised. The property is the no-window RESOLUTION +/// mechanics, not any particular archive's span, so it runs against +/// `fresh_project_with_data`'s tiny synthetic two-symbol archive (#250) — +/// hostless, no data-refusal skip arm needed. #[test] fn generalize_without_explicit_window_resolves_a_shared_window_and_completes() { - let (cwd, _g) = fresh_project(); + let (cwd, _g) = fresh_project_with_data(); let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ - "generalize", &fixture, "--real", "GER40,USDJPY", + "generalize", &fixture, "--real", "SYMA,SYMB", "--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"); - 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); + 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}"); @@ -3633,24 +3627,21 @@ fn generalize_without_explicit_window_resolves_a_shared_window_and_completes() { /// instrument conflates the instrument axis with the period axis, so the common /// window is the only one that isolates what the verb measures. Today's fallback /// picks `symbols[0]`'s full window alone (the superseded shape test above): -/// listing the WIDER symbol first (`AAPL.US`, archive from 2006) keeps AAPL's -/// early start, whereas the intersection with the later-starting `GER40` (archive -/// from 2014) must start at GER40's first bar and end at whichever archive ends -/// first — so on the current tree the resolved window differs from the +/// listing the WIDER synthetic symbol first (`SYMA`, spanning 2024-01..08) keeps +/// SYMA's early start, whereas the intersection with `SYMB` (spanning 2024-03..06, +/// strictly inside SYMA's span) must start at SYMB's first bar and end at SYMB's +/// last — so on the current tree the resolved window differs from the /// intersection on both bounds. Each symbol's full window is resolved /// independently here: a single-symbol `sweep` with no window records it into its /// generated campaign document, in the same Unix-ms currency and via the same /// `campaign_window_ms(full_window)` probe generalize itself resolves against — /// so the expected intersection is derived from the live archive, never a -/// hard-coded span that shifts as the archive grows. Archive-gated; skips -/// cleanly on a data-less host. +/// hard-coded span. The archive's SPAN SHAPE is authored (#250, +/// `fresh_project_with_data`), not its size, so this runs hostless in +/// milliseconds instead of streaming two full host archives. #[test] fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols() { - if !local_data_present() { - eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH); - return; - } - let (cwd, _g) = fresh_project(); + let (cwd, _g) = fresh_project_with_data(); let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); // The resolved shared window persists verbatim in the generated campaign @@ -3694,24 +3685,24 @@ fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols() window_of(name) }; - let aapl = probe("AAPL.US", "probe-aapl"); - let ger40 = probe("GER40", "probe-ger40"); + let syma = probe("SYMA", "probe-syma"); + let symb = probe("SYMB", "probe-symb"); // Precondition: the archives genuinely differ, so "the intersection" is a // distinct claim from "symbols[0]'s full window" (else the test is vacuous). assert_ne!( - aapl, ger40, - "AAPL.US {aapl:?} and GER40 {ger40:?} must span different windows for the test to bite" + syma, symb, + "SYMA {syma:?} and SYMB {symb:?} must span different windows for the test to bite" ); // The contract: the shared window is the intersection — latest start, earliest end. - let expected = (aapl.0.max(ger40.0), aapl.1.min(ger40.1)); + let expected = (syma.0.max(symb.0), syma.1.min(symb.1)); - // AAPL.US listed FIRST: today's `symbols[0]` fallback keeps AAPL's (wider) + // SYMA listed FIRST: today's `symbols[0]` fallback keeps SYMA's (wider) // window, which differs from the intersection on both bounds — so a pass here // can only mean the intersection semantics landed. let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) .args([ - "generalize", &fixture, "--real", "AAPL.US,GER40", + "generalize", &fixture, "--real", "SYMA,SYMB", "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", "--name", "gen-both", ]) @@ -3727,8 +3718,8 @@ fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols() assert_eq!( resolved, expected, - "no-window generalize must resolve the INTERSECTION {expected:?} of AAPL.US {aapl:?} \ - and GER40 {ger40:?} — not symbols[0]'s ({aapl:?}) full window (resolved {resolved:?})" + "no-window generalize must resolve the INTERSECTION {expected:?} of SYMA {syma:?} \ + and SYMB {symb:?} — not symbols[0]'s ({syma:?}) full window (resolved {resolved:?})" ); } diff --git a/crates/aura-cli/tests/common/mod.rs b/crates/aura-cli/tests/common/mod.rs index 972eeec..7a7211b 100644 --- a/crates/aura-cli/tests/common/mod.rs +++ b/crates/aura-cli/tests/common/mod.rs @@ -52,6 +52,40 @@ impl Drop for FreshProject { } } +/// Mints a unique, empty tempdir under `std::env::temp_dir()`, named +/// `aura-e2e--` so concurrent test threads (same process) and +/// concurrent `cargo test` processes never collide. +fn mint_tempdir() -> PathBuf { + static COUNTER: AtomicUsize = AtomicUsize::new(0); + let root = std::env::temp_dir().join(format!( + "aura-e2e-{}-{}", + std::process::id(), + COUNTER.fetch_add(1, Ordering::Relaxed) + )); + std::fs::create_dir_all(&root).expect("create fresh project tempdir"); + root +} + +/// Writes the `Aura.toml` + `demo_signal.json` common to every fresh project: +/// an absolute `[nodes]` pointer at the shared, once-built fixture crate, and +/// — when `data_dir` is `Some` — a `[paths] data` override pointing at it +/// (relative, resolved against the invocation cwd, which every test that uses +/// this always sets to `root` itself via `.current_dir`). +fn write_project_files(root: &Path, data_dir: Option<&str>) { + let fixture = built_project(); + let data_line = data_dir.map(|d| format!("data = \"{d}\"\n")).unwrap_or_default(); + std::fs::write( + root.join("Aura.toml"), + format!( + "[paths]\nruns = \"runs\"\n{data_line}\n[nodes]\ncrates = [\"{}\"]\n", + fixture.display() + ), + ) + .expect("write fresh project Aura.toml"); + std::fs::copy(fixture.join("demo_signal.json"), root.join("demo_signal.json")) + .expect("copy demo_signal.json into the fresh project"); +} + /// Mint a unique tempdir holding a 2-line data-level project (`Aura.toml` /// with an absolute `[nodes]` pointer at the shared, once-built fixture /// crate) plus a copy of `demo_signal.json`. Every test gets its own `runs/` @@ -60,24 +94,26 @@ impl Drop for FreshProject { /// whole test body: `let (dir, _g) = fresh_project();`. #[allow(dead_code)] pub fn fresh_project() -> (PathBuf, FreshProject) { - static COUNTER: AtomicUsize = AtomicUsize::new(0); - let root = std::env::temp_dir().join(format!( - "aura-e2e-{}-{}", - std::process::id(), - COUNTER.fetch_add(1, Ordering::Relaxed) - )); - std::fs::create_dir_all(&root).expect("create fresh project tempdir"); - let fixture = built_project(); - std::fs::write( - root.join("Aura.toml"), - format!( - "[paths]\nruns = \"runs\"\n\n[nodes]\ncrates = [\"{}\"]\n", - fixture.display() - ), - ) - .expect("write fresh project Aura.toml"); - std::fs::copy(fixture.join("demo_signal.json"), root.join("demo_signal.json")) - .expect("copy demo_signal.json into the fresh project"); + let root = mint_tempdir(); + write_project_files(&root, None); + (root.clone(), FreshProject { root }) +} + +/// Like [`fresh_project`], but additionally generates a tiny synthetic M1 +/// archive under `/data/` (two symbols, `SYMA` spanning 2024-01..08 and +/// `SYMB` spanning 2024-03..06 — `SYMB` lies strictly inside `SYMA`) and +/// points `[paths] data` at it. For tests whose property is archive-window +/// (span) semantics, not archive size — no host tick-data mount needed, and +/// no `--real`-symbol data refusal is ever reachable, so gates on data +/// presence are dead code once a test switches to this helper (#250). +#[allow(dead_code)] +pub fn fresh_project_with_data() -> (PathBuf, FreshProject) { + let root = mint_tempdir(); + write_project_files(&root, Some("data")); + let data_dir = root.join("data"); + std::fs::create_dir_all(&data_dir).expect("create synthetic archive dir"); + synthetic_data::write_symbol_archive(&data_dir, "SYMA", (2024, 1), (2024, 8)); + synthetic_data::write_symbol_archive(&data_dir, "SYMB", (2024, 3), (2024, 6)); (root.clone(), FreshProject { root }) } @@ -108,3 +144,142 @@ impl Drop for ScratchGuard { } } } + +/// A tiny, deterministic synthetic M1 archive generator: hand-packs the exact +/// on-disk format `data-server` reads (its `loader.rs` and `records.rs`) — +/// one zip per `SYMBOL_YYYY_MM.m1` holding a single `.bin` entry of packed +/// 48-byte `RawM1Record`s, plus a `SYMBOL.meta.json` geometry sidecar — so +/// the two no-window `generalize` E2E tests get a real, tiny, hostless +/// archive instead of streaming the 6.6 GB host archive (#250). +#[allow(dead_code)] +mod synthetic_data { + use std::io::Write; + use std::path::Path; + + /// Days since the Unix epoch for a proleptic-Gregorian civil date. + /// Howard Hinnant's `days_from_civil` — dependency-free calendar math so + /// this module needs no `chrono` (data-server's own dependency, not + /// aura-cli's). + fn days_from_civil(y: i32, m: u32, d: u32) -> i64 { + let y = i64::from(if m <= 2 { y - 1 } else { y }); + let era = if y >= 0 { y } else { y - 399 } / 400; + let yoe = y - era * 400; // [0, 399] + let mp = (i64::from(m) + 9) % 12; // [0, 11], Mar=0 .. Feb=11 + let doy = (153 * mp + 2) / 5 + i64::from(d) - 1; // [0, 365] + let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096] + era * 146_097 + doe - 719_468 + } + + /// Unix milliseconds at `y-m-d hh:mm:00` UTC. + fn unix_ms(y: i32, m: u32, d: u32, hh: u32, mm: u32) -> i64 { + days_from_civil(y, m, d) * 86_400_000 + (i64::from(hh) * 3600 + i64::from(mm) * 60) * 1000 + } + + /// 0 = Sunday .. 6 = Saturday (1970-01-01 was a Thursday, day 0). + fn weekday(y: i32, m: u32, d: u32) -> i64 { + (days_from_civil(y, m, d) % 7 + 4 + 7) % 7 + } + + fn days_in_month(y: i32, m: u32) -> u32 { + match m { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 => 29, + 2 => 28, + _ => unreachable!("month out of range: {m}"), + } + } + + /// A deterministic price at continuous minute offset `t` (minutes since + /// 2024-01-01T00:00 UTC, shared across both symbols so their overlapping + /// span carries the same underlying curve): a gentle upward trend plus a + /// 180-minute sine — short enough against a day's 480 tradeable minutes to + /// produce several SMA(3)/SMA(12) crossovers per day. + fn price_at(t_minutes: f64) -> f64 { + let hours = t_minutes / 60.0; + 100.0 + 0.01 * hours + 5.0 * (2.0 * std::f64::consts::PI * t_minutes / 180.0).sin() + } + + /// Packs one `RawM1Record`-shaped 48-byte little-endian record: `time` + /// (Delphi `TDateTime`, days since 1899-12-30 — the inverse of + /// `data_server::records::delphi_to_unix_ms`), `open/high/low/close`, + /// `spread` (f32), `volume` (i32). + fn pack_record(unix_ms_time: i64, open: f64, high: f64, low: f64, close: f64) -> [u8; 48] { + const DELPHI_EPOCH_OFFSET_DAYS: f64 = 25569.0; + const MS_PER_DAY: f64 = 86_400_000.0; + let dt = unix_ms_time as f64 / MS_PER_DAY + DELPHI_EPOCH_OFFSET_DAYS; + let mut rec = [0u8; 48]; + rec[0..8].copy_from_slice(&dt.to_le_bytes()); + rec[8..16].copy_from_slice(&open.to_le_bytes()); + rec[16..24].copy_from_slice(&high.to_le_bytes()); + rec[24..32].copy_from_slice(&low.to_le_bytes()); + rec[32..40].copy_from_slice(&close.to_le_bytes()); + rec[40..44].copy_from_slice(&0.5_f32.to_le_bytes()); + rec[44..48].copy_from_slice(&100_i32.to_le_bytes()); + rec + } + + /// One month's worth of weekday, 08:00-16:00 UTC, 1-minute bars (~10k). + fn month_records(year: i32, month: u32) -> Vec<[u8; 48]> { + let epoch_ms = unix_ms(2024, 1, 1, 0, 0); + let mut records = Vec::new(); + for day in 1..=days_in_month(year, month) { + if !(1..=5).contains(&weekday(year, month, day)) { + continue; // weekend + } + for hh in 8..16 { + for mm in 0..60 { + let ms = unix_ms(year, month, day, hh, mm); + let t = (ms - epoch_ms) as f64 / 60_000.0; + let (open, close) = (price_at(t), price_at(t + 1.0)); + let high = open.max(close) + 0.2; + let low = open.min(close) - 0.2; + records.push(pack_record(ms, open, high, low, close)); + } + } + } + records + } + + /// Writes one `SYMBOL_YYYY_MM.m1` zip (a single `.bin` entry of + /// packed records — mirrors `data-server`'s own `create_test_m1_zip` test + /// helper) under `data_dir`. + fn write_month_zip(data_dir: &Path, symbol: &str, year: i32, month: u32) { + let path = data_dir.join(format!("{symbol}_{year:04}_{month:02}.m1")); + let file = std::fs::File::create(&path).expect("create synthetic m1 zip"); + let mut zip = zip::ZipWriter::new(file); + zip.start_file::(format!("{symbol}.bin"), Default::default()) + .expect("start synthetic m1 zip entry"); + for rec in month_records(year, month) { + zip.write_all(&rec).expect("write synthetic m1 record"); + } + zip.finish().expect("finish synthetic m1 zip"); + } + + /// Writes every month in `[from, to]` (inclusive, `(year, month)` pairs) + /// for `symbol`, plus its `.meta.json` geometry sidecar — + /// GER40-shaped (digits 1, pip/tick/lot sized like an index CFD) so the + /// R/stop machinery never hits "no recorded geometry". + pub fn write_symbol_archive(data_dir: &Path, symbol: &str, from: (i32, u32), to: (i32, u32)) { + let (mut y, mut m) = from; + loop { + write_month_zip(data_dir, symbol, y, m); + if (y, m) == to { + break; + } + m += 1; + if m > 12 { + m = 1; + y += 1; + } + } + std::fs::write( + data_dir.join(format!("{symbol}.meta.json")), + format!( + "{{\"schemaVersion\":2,\"symbol\":\"{symbol}\",\"digits\":1,\"pipSize\":1,\ + \"tickSize\":0.1,\"lotSize\":1,\"baseAsset\":\"{symbol}\",\"quoteAsset\":\"EUR\"}}" + ), + ) + .expect("write synthetic geometry sidecar"); + } +} -- 2.52.0 From a04fda3a4cbcd4cad23a10ee6b890cbdcfd23328 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 13 Jul 2026 15:34:30 +0200 Subject: [PATCH 5/8] fix(cli): resolve a relative paths.data against the project root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Env::data_path() handed paths.data to the data layer verbatim, so a relative value resolved against the invoking process's cwd — aura run from a project subdirectory silently read a different archive location than from the root, while runs/ stayed anchored. Relative values now join onto the project root, matching runs_root() and the [nodes] pointer resolution; absolute values and the no-project default pass through unchanged. RED-first unit test pins the root-anchored resolution. closes #254 --- crates/aura-cli/src/project.rs | 32 ++++++++++++++++++++++++++--- crates/aura-cli/tests/common/mod.rs | 5 +++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/crates/aura-cli/src/project.rs b/crates/aura-cli/src/project.rs index 625b4f3..d4c026d 100644 --- a/crates/aura-cli/src/project.rs +++ b/crates/aura-cli/src/project.rs @@ -182,12 +182,23 @@ impl Env { TraceStore::open(self.runs_root()) } - /// The data archive root: `paths.data` inside a project when set, - /// the data-server default otherwise. + /// The data archive root: `paths.data` inside a project when set + /// (relative values resolved against the project root, same as + /// `runs_root()` and the `[nodes]` pointers), the data-server default + /// otherwise. pub fn data_path(&self) -> String { self.project .as_ref() - .and_then(|p| p.toml.paths.data.clone()) + .and_then(|p| { + p.toml.paths.data.as_ref().map(|data| { + let path = Path::new(data); + if path.is_absolute() { + data.clone() + } else { + p.root.join(path).to_string_lossy().to_string() + } + }) + }) .unwrap_or_else(|| data_server::DEFAULT_DATA_PATH.to_string()) } @@ -615,6 +626,21 @@ mod tests { std::fs::remove_dir_all(&tmp).ok(); } + /// #254: a relative `paths.data` must resolve against the project root + /// (`p.root`), matching `runs_root()`'s and the `[nodes]` pointers' + /// resolution — not against the invoking process's cwd, which silently + /// changes the data location depending on the caller's working directory. + #[test] + fn data_path_resolves_a_relative_paths_data_against_the_project_root() { + let tmp = std::env::temp_dir().join(format!("aura-datarel-{}", std::process::id())); + std::fs::create_dir_all(&tmp).unwrap(); + std::fs::write(tmp.join("Aura.toml"), "[paths]\ndata = \"data\"\n").unwrap(); + let p = load(&tmp, false).expect("data-only load"); + let env = Env::with_project(p); + assert_eq!(env.data_path(), tmp.join("data").to_string_lossy().to_string()); + std::fs::remove_dir_all(&tmp).ok(); + } + #[test] fn two_nodes_pointers_refuse_with_multi_crate() { let tmp = std::env::temp_dir().join(format!("aura-multi-{}", std::process::id())); diff --git a/crates/aura-cli/tests/common/mod.rs b/crates/aura-cli/tests/common/mod.rs index 7a7211b..7ac6fff 100644 --- a/crates/aura-cli/tests/common/mod.rs +++ b/crates/aura-cli/tests/common/mod.rs @@ -69,8 +69,9 @@ fn mint_tempdir() -> PathBuf { /// Writes the `Aura.toml` + `demo_signal.json` common to every fresh project: /// an absolute `[nodes]` pointer at the shared, once-built fixture crate, and /// — when `data_dir` is `Some` — a `[paths] data` override pointing at it -/// (relative, resolved against the invocation cwd, which every test that uses -/// this always sets to `root` itself via `.current_dir`). +/// (relative, resolved against the project root — `root` itself — since +/// `Env::data_path` joins a relative `paths.data` onto the project root, +/// same as `runs_root()` and the `[nodes]` pointers, #254). fn write_project_files(root: &Path, data_dir: Option<&str>) { let fixture = built_project(); let data_line = data_dir.map(|d| format!("data = \"{d}\"\n")).unwrap_or_default(); -- 2.52.0 From 479c6620e25a060e8f6cec23dd0932dd90e1a0b2 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 13 Jul 2026 15:50:35 +0200 Subject: [PATCH 6/8] perf(cli): synthetic walkforward pre-flight validates axes without running members MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit blueprint_walkforward_family's pre-flight executed the full first-IS- window sweep via blueprint_sweep_over and discarded the result, purely to surface axis errors once before the parallel window fan-out (#177). validate_axis_grid now drives the sweep terminal's own resolve/arity/ kind checks with a constant placeholder report — same single-sourced rejection wording, no member run, no roller derivation needed. Scope correction against the issue text: this path serves only the synthetic (non---real) walkforward. The --real trunk routes through verb_sugar into aura-campaign's [Sweep, WalkForward] process, whose leading full-window sweep is a separate, larger duplication — re-filed with corrected attribution in the issue's closing comment. closes #253 --- crates/aura-cli/src/main.rs | 59 +++++++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 9 deletions(-) diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 78085d7..9ff0f8f 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -2098,6 +2098,52 @@ fn run_oos_blueprint( (Vec::new(), report) } +/// A constant, zero-compute `RunReport` for [`validate_axis_grid`]'s sweep-terminal +/// probe. `SweepBinder::sweep_with_lattice`'s own `resolve_axes`/arity/kind checks all +/// run BEFORE this closure is invoked per grid point, so its body never influences the +/// validation outcome — only its signature (`Fn(&[Cell]) -> RunReport`) needs to +/// satisfy the terminal, at O(1) cost per point instead of a full member run. +fn axis_grid_probe_report() -> RunReport { + RunReport { + manifest: RunManifest { + commit: String::new(), + params: Vec::new(), + defaults: Vec::new(), + window: (Timestamp(0), Timestamp(0)), + seed: 0, + broker: "wf-axis-preflight-placeholder".to_string(), + selection: None, + instrument: None, + topology_hash: None, + project: None, + }, + metrics: RunMetrics { total_pips: 0.0, max_drawdown: 0.0, bias_sign_flips: 0, r: None }, + } +} + +/// Validate the `--axis` grid against `doc`'s wrapped param space WITHOUT running any +/// member (#253). Reuses the SAME strict, erroring axis-name check +/// `blueprint_sweep_over` performs (`override_paths` — single-sourced, so the two +/// paths cannot drift to differently-worded rejections) to derive the #246 override +/// set, then drives the sweep terminal's own `resolve_axes`/arity/kind checks +/// (`SweepBinder::sweep_with_lattice`) with [`axis_grid_probe_report`] standing in for +/// the run closure — no data access, no sim engine tick. Axis resolution is +/// window-agnostic, so the caller derives or passes no window at all. +fn validate_axis_grid( + doc: &str, axes: &[(String, Vec)], raw_space: &[ParamSpec], probe_signal: &Composite, + env: &project::Env, +) -> Result<(), BindError> { + let overrides = override_paths(axes, raw_space, probe_signal).map_err(BindError::UnknownKnob)?; + let probe = blueprint_axis_probe_reopened(doc, env, &overrides); + let mut iter = axes.iter(); + let (first_name, first_vals) = iter.next().expect("a blueprint walk-forward declares >= 1 axis"); + let mut binder = probe.axis(first_name, first_vals.clone()); + for (n, vals) in iter { + binder = binder.axis(n, vals.clone()); + } + binder.sweep_with_lattice(|_| axis_grid_probe_report()).map(|_| ()) +} + /// The loaded-blueprint IS-refit walk-forward: per IS window, re-optimize the /// blueprint over the user `--axis` grid, select by `sqn_normalized`, run the /// winner OOS, reusing the generic `walk_forward` driver + `select_winner`; @@ -2147,15 +2193,10 @@ fn blueprint_walkforward_family( // (which resolves its axes a single time before any member runs). `walk_forward` fans // the per-window closure out across the windows in parallel, so a `BindError` raised // *inside* the closure would `eprintln!`+`exit(2)` from several windows before any one - // exit lands — a racy, duplicated rejection (#177). Axis resolution is window-agnostic, - // so pre-flighting the first IS window here surfaces the error exactly once (it fails in - // `resolve_axes`, before any member runs); a per-window resolve then cannot re-raise. - let first_is = WindowRoller::new(span, is_len, oos_len, step, RollMode::Rolling) - .expect("roller config validated just above") - .next() - .expect("roller yields >= 1 window (validated above)") - .is; - if let Err(e) = blueprint_sweep_over(doc, axes, first_is.0, first_is.1, data, env, &binding) { + // exit lands — a racy, duplicated rejection (#177). Axis resolution is window-agnostic + // (#253): `validate_axis_grid` resolves the SAME grid the sweep terminal would, without + // running a single member — no IS window (or a second roller) is needed here at all. + if let Err(e) = validate_axis_grid(doc, axes, &raw_space, &probe_signal, env) { eprintln!("aura: {e:?}"); std::process::exit(2); } -- 2.52.0 From a895891ab143a95d8cf6ee12f781325afb896228 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 13 Jul 2026 16:33:18 +0200 Subject: [PATCH 7/8] =?UTF-8?q?perf(ingest,cli):=20derive=20archive=20boun?= =?UTF-8?q?ds=20from=20the=20monthly=20file=20index=20=E2=80=94=20probe=5F?= =?UTF-8?q?window=20drain=20retired?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit probe_window drained a source's entire close column just to learn its first and last bar timestamp; both live callers (open_real_source and campaign_window_ms on the campaign trunk) now resolve through aura-ingest's new archive_extent(): list the symbol's SYM_YYYY_MM.m1 files, load only the boundary months via the existing loader seam, and walk across gap months forward/backward — O(2 file loads) typical instead of O(archive). Empty-window refusal semantics are unchanged (pinned). Two characterization tests captured the resolved windows against the OLD implementation over the synthetic per-test archive and stay green unchanged after the swap. Measured: the probe itself drops 366ms -> 13ms (~28x) on the full 2014-2026 GER40 archive; end-to-end command wall-clock moves only ~2-3% because the sim loop dominates — the fix removes an O(archive-size) term, not the dominant cost. closes #252 --- Cargo.lock | 1 + crates/aura-cli/src/main.rs | 11 +- crates/aura-cli/tests/cli_run.rs | 135 +++++++++++++ crates/aura-ingest/Cargo.toml | 7 + crates/aura-ingest/src/lib.rs | 312 +++++++++++++++++++++++++++++++ 5 files changed, 456 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab4ebe9..fc93401 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,6 +182,7 @@ dependencies = [ "chrono", "chrono-tz", "data-server", + "zip", ] [[package]] diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 9ff0f8f..6a5b17e 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -639,17 +639,8 @@ fn probe_window( to_ms: Option, env: &project::Env, ) -> (Timestamp, Timestamp) { - let mut probe = aura_ingest::open_columns(server, symbol, from_ms, to_ms, &[aura_ingest::M1Field::Close]) + aura_ingest::archive_extent(server, std::path::Path::new(&env.data_path()), symbol, from_ms, to_ms) .unwrap_or_else(|| no_data_in_window(symbol, from_ms, to_ms, env)) - .pop() - .expect("open_columns yields one source per requested field"); - let first = - aura_engine::Source::peek(probe.as_ref()).unwrap_or_else(|| no_data_in_window(symbol, from_ms, to_ms, env)); - let mut last = first; - while let Some((t, _)) = aura_engine::Source::next(&mut *probe) { - last = t; - } - (first, last) } /// Open the real M1 sources for a recorded symbol over an optional window — one diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index 1fb67de..0b34354 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -5694,6 +5694,141 @@ fn shipped_r_sma_example_reproduces_the_builtin_grade() { assert!(stdout.contains("\"n_trades\":3"), "{stdout}"); } +/// Characterization pin (#252, byte-identity anchor before `probe_window`'s +/// internals swap from a full-window drain to `aura_ingest::archive_extent`'s +/// boundary-month derivation). Captured against the PRE-#252 drain-based code +/// over `fresh_project_with_data`'s hermetic two-symbol synthetic archive +/// (SYMA spans 2024-01..08, SYMB 2024-03..06, weekday 08:00-16:00 UTC bars): +/// the resolved `campaign_window_ms(full_window)` span persists verbatim into +/// the generated campaign document's `data.windows[0]`, exactly like the +/// sibling `generalize_without_explicit_window_resolves_the_intersection_of_all_symbols` +/// probe. Three cases: SYMA's and SYMB's full (no-window) spans, and an +/// explicit `--from`/`--to` sub-window straddling three month files +/// (2024-03-15..2024-05-15, so `filter_files` alone would load 3 files under +/// the old drain). The first bar of each captured span carries a known -1ms +/// float round-trip artifact (the synthetic fixture's Delphi-`TDateTime` +/// pack/unpack in `data-server`'s own record format, not a `probe_window` bug) +/// — this test does not explain it, only pins it exactly, since the +/// post-swap code must reproduce the identical bytes (same underlying decoder, +/// only fewer files touched to reach them). +#[test] +fn probe_window_resolves_the_pinned_span_before_the_252_swap() { + let (cwd, _g) = fresh_project_with_data(); + let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); + let window_of = |name: &str| -> (i64, i64) { + let dir = cwd.join("runs").join("campaigns"); + let path = std::fs::read_dir(&dir) + .expect("campaigns dir exists after a run") + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .find(|p| { + let doc: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(p).expect("read campaign doc")) + .expect("campaign doc parses as JSON"); + doc["name"].as_str() == Some(name) + }) + .unwrap_or_else(|| panic!("no campaign document named {name:?} in {}", dir.display())); + let doc: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&path).expect("read campaign doc")) + .expect("campaign doc parses as JSON"); + let w = &doc["data"]["windows"][0]; + ( + w["from_ms"].as_i64().expect("from_ms is an integer"), + w["to_ms"].as_i64().expect("to_ms is an integer"), + ) + }; + let sweep = |symbol: &str, name: &str, from: Option<&str>, to: Option<&str>| { + let mut args = vec![ + "sweep", fixture.as_str(), "--real", symbol, + "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--name", name, + ]; + if let Some(f) = from { + args.push("--from"); + args.push(f); + } + if let Some(t) = to { + args.push("--to"); + args.push(t); + } + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args(&args) + .current_dir(&cwd) + .output() + .expect("spawn aura sweep"); + assert_eq!( + out.status.code(), Some(0), + "window-probe sweep {name:?} must run to exit 0: {}", + String::from_utf8_lossy(&out.stderr) + ); + }; + + sweep("SYMA", "probe-252-no-window-syma", None, None); + assert_eq!( + window_of("probe-252-no-window-syma"), + (1_704_095_999_999, 1_725_033_540_000), + "SYMA's full (no-window) resolved span" + ); + + sweep("SYMB", "probe-252-no-window-symb", None, None); + assert_eq!( + window_of("probe-252-no-window-symb"), + (1_709_279_999_999, 1_719_590_340_000), + "SYMB's full (no-window) resolved span" + ); + + // 2024-03-15 00:00 UTC .. 2024-05-15 00:00 UTC: straddles the March, April, + // and May files (three boundary months under the old file-range drain). + sweep("SYMA", "probe-252-straddle", Some("1710460800000"), Some("1715731200000")); + assert_eq!( + window_of("probe-252-straddle"), + (1_710_489_599_999, 1_715_702_340_000), + "explicit sub-window straddling month boundaries, clipped to the archive's actual bars" + ); +} + +/// Characterization pin (#252, hostless zero-bar case): a window that falls +/// entirely on a weekend inside a COVERED month (2024-03-02/03, both inside +/// SYMA's Jan-Aug 2024 span, weekday-only bars) still refuses with the SAME +/// exit-1 "no data ... in the requested window" message `probe_window`'s +/// drain-based emptiness check raises today — `archive_extent` must reach the +/// identical conclusion by finding zero matching months in range, not by +/// draining a stream, and the refusal wording is part of what must not move +/// (mirrors `run_real_empty_window_refusal_names_the_window_not_the_symbol`, +/// hostless here since the property (an empty resolved sub-window still +/// refuses) needs no full archive, only a covered-but-empty range). +#[test] +fn probe_window_still_refuses_a_hostless_zero_bar_window() { + let (cwd, _g) = fresh_project_with_data(); + let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args([ + "sweep", &fixture, "--real", "SYMA", + "--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12", + "--name", "probe-252-zero-bars", + "--from", "1709337600000", // 2024-03-02 00:00 UTC (Saturday) + "--to", "1709424000000", // 2024-03-03 00:00 UTC (Sunday) + ]) + .current_dir(&cwd) + .output() + .expect("spawn aura sweep"); + assert_eq!( + out.status.code(), Some(1), + "a zero-bar window inside covered archive months still refuses with exit 1: stderr={}", + String::from_utf8_lossy(&out.stderr) + ); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("no data") && stderr.contains("window"), + "must name the empty window, not a missing symbol: {stderr}" + ); + assert!( + !cwd.join("runs").join("campaigns").exists() + || std::fs::read_dir(cwd.join("runs").join("campaigns")).unwrap().count() == 0, + "a refused zero-bar window must not register a generated campaign document" + ); +} + /// `aura run` on the shipped closed r-meanrev example (#159 cut 3) actually loads /// and grades through the real, separately-linked `aura` binary — not merely /// in-process, as `r_meanrev_example_loaded_runs_identically_to_the_carved_signal` diff --git a/crates/aura-ingest/Cargo.toml b/crates/aura-ingest/Cargo.toml index 40eb7f9..fdd3790 100644 --- a/crates/aura-ingest/Cargo.toml +++ b/crates/aura-ingest/Cargo.toml @@ -16,6 +16,13 @@ data-server = { git = "http://192.168.178.103:3000/Brummel/data-server.git", bra aura-engine = { path = "../aura-engine" } [dev-dependencies] +# zip: hand-packs a tiny synthetic M1 archive (lib.rs's own unit tests, #252's +# archive_extent gap-walk coverage) in the exact on-disk format data-server +# reads — already transitive via the data-server dependency above (a normal, +# non-dev dependency there); declared directly here so test code may `use +# zip::...` (mirrors aura-cli's tests/common/mod.rs, no new dependency +# actually enters the build graph). +zip = "2" # the integration tests bootstrap a sample harness + fold a RunReport aura-std = { path = "../aura-std" } # chrono / chrono-tz build the Berlin-local-wall-clock window bounds + the diff --git a/crates/aura-ingest/src/lib.rs b/crates/aura-ingest/src/lib.rs index 1746dfc..ceb3dcf 100644 --- a/crates/aura-ingest/src/lib.rs +++ b/crates/aura-ingest/src/lib.rs @@ -23,6 +23,7 @@ use aura_core::{Scalar, Timestamp}; use data_server::records::M1Parsed; use data_server::SymbolChunkIter; +use std::path::Path; /// Re-export of the data-server archive entry points (#81): a real-data source /// builds from `aura-ingest` alone — a consumer never names the external @@ -411,10 +412,168 @@ pub fn instrument_geometry(server: &Arc, symbol: &str) -> Option i64 { + let y = if m <= 2 { y - 1 } else { y }; + let era = if y >= 0 { y } else { y - 399 } / 400; + let yoe = (y - era * 400) as u64; // [0, 399] + let mp = (u64::from(m) + 9) % 12; // [0, 11]: Mar=0 .. Feb=11 + let doy = (153 * mp + 2) / 5 + u64::from(d) - 1; // [0, 365] + let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096] + era * 146_097 + doe as i64 - 719_468 +} + +/// UTC epoch-ms of `(year, month)`'s first instant — matches +/// `data_server::records::unix_ms_to_year_month`'s own UTC calendar (chrono's +/// `DateTime`), the reference [`archive_extent`] derives a +/// single-candidate-month probe window against. +fn month_start_ms(year: u16, month: u8) -> i64 { + days_from_civil(i64::from(year), u32::from(month), 1) * 86_400_000 +} + +/// UTC epoch-ms of the instant immediately after `(year, month)`'s last +/// millisecond (the exclusive upper bound [`archive_extent`] clamps a +/// forward-search probe's `to_ms` against, so a single query loads at most +/// one month's file). +fn month_end_ms_exclusive(year: u16, month: u8) -> i64 { + let (ny, nm) = if month == 12 { (year + 1, 1) } else { (year, month + 1) }; + month_start_ms(ny, nm) +} + +/// The sorted `(year, month)` pairs that exist as `_YYYY_MM.m1` files +/// directly under `data_path` — mirrors data-server's own file-naming +/// convention (`SymbolIndex::scan`, already load-bearing in its loader) via a +/// plain directory listing scoped to the one already-known `symbol`, so this +/// needs no general symbol-boundary regex and reads no private index (#252: +/// no new data-server surface). Empty when `data_path` doesn't exist or holds +/// no matching file for `symbol` — the same "no archive for this symbol" +/// condition an absent `DataServer` symbol represents. +fn list_m1_months(data_path: &Path, symbol: &str) -> Vec<(u16, u8)> { + let prefix = format!("{symbol}_"); + let mut months = Vec::new(); + let Ok(entries) = std::fs::read_dir(data_path) else { + return months; + }; + for entry in entries.flatten() { + let file_name = entry.file_name(); + let Some(name) = file_name.to_str() else { continue }; + let Some(rest) = name.strip_prefix(prefix.as_str()) else { continue }; + let Some(year_month) = rest.strip_suffix(".m1") else { continue }; + let Some((year_str, month_str)) = year_month.split_once('_') else { continue }; + if year_str.len() != 4 + || month_str.len() != 2 + || !year_str.bytes().all(|b| b.is_ascii_digit()) + || !month_str.bytes().all(|b| b.is_ascii_digit()) + { + continue; + } + let (Ok(year), Ok(month)) = (year_str.parse::(), month_str.parse::()) else { + continue; + }; + if (1..=12).contains(&month) { + months.push((year, month)); + } + } + months.sort_unstable(); + months +} + +/// Derive the archive's `(first, last)` bar timestamp inside `[from_ms, to_ms]` +/// (inclusive Unix-ms, `None` = open-ended) at O(1) file loads per boundary +/// instead of draining every bar in the window (#252). Lists the symbol's +/// monthly files directly off `data_path` ([`list_m1_months`]), then loads +/// ONLY the boundary month through the existing `stream_m1_windowed`/ +/// [`M1FieldSource`] seam — walking forward (for the first bar) or backward +/// (for the last) across gap months whose candidate file yields nothing in +/// range, exactly like the cross-file skip-ahead a full-window drain already +/// did internally, just narrowed to one file per candidate instead of every +/// file in the window. `server` is the caller's own `Arc` (shares +/// its file cache — the two boundary files loaded here are exactly the ones a +/// subsequent real run reads first/last, so this is not wasted work, only +/// reordered). Returns `None` exactly when no month in `[from_ms, to_ms]` has +/// a bar — no archive for the symbol, or a window with zero matching bars — +/// the same refusal condition a drain-based emptiness check detects. +pub fn archive_extent( + server: &Arc, + data_path: &Path, + symbol: &str, + from_ms: Option, + to_ms: Option, +) -> Option<(Timestamp, Timestamp)> { + let months = list_m1_months(data_path, symbol); + if months.is_empty() { + return None; + } + let from_ym = from_ms.map(data_server::records::unix_ms_to_year_month); + let to_ym = to_ms.map(data_server::records::unix_ms_to_year_month); + let in_range = |&(y, m): &(u16, u8)| { + from_ym.is_none_or(|fym| (y, m) >= fym) && to_ym.is_none_or(|tym| (y, m) <= tym) + }; + + // Forward walk: the first candidate month (ascending) whose file, probed + // over just that month's span, yields a bar — a gap or an empty boundary + // file simply advances to the next one. + let first = months.iter().filter(|ym| in_range(ym)).find_map(|&(y, m)| { + let month_cap = month_end_ms_exclusive(y, m) - 1; + let probe_to = to_ms.map_or(month_cap, |t| t.min(month_cap)); + let src = M1FieldSource::open(server, symbol, from_ms, Some(probe_to), M1Field::Close)?; + aura_engine::Source::peek(&src) + })?; + + // Backward walk: the last candidate month (descending) whose file, + // probed over just that month's span, yields a bar — fully drained (a + // single file) to find its own last matching record. + let last = months.iter().rev().filter(|ym| in_range(ym)).find_map(|&(y, m)| { + let month_floor = month_start_ms(y, m); + let probe_from = from_ms.map_or(month_floor, |f| f.max(month_floor)); + let mut src = M1FieldSource::open(server, symbol, Some(probe_from), to_ms, M1Field::Close)?; + let mut last_ts = aura_engine::Source::peek(&src)?; + while let Some((t, _)) = aura_engine::Source::next(&mut src) { + last_ts = t; + } + Some(last_ts) + })?; + + Some((first, last)) +} + #[cfg(test)] mod tests { use super::*; + /// A unique scratch directory under the OS temp root, removed on drop — + /// mirrors `aura-cli`'s own `tests/common::mint_tempdir` pattern (unique + /// name via pid + an atomic counter) so this crate's tests need no + /// `tempfile` dev-dependency for a handful of throwaway fixture dirs. + struct ScratchDir(std::path::PathBuf); + + impl ScratchDir { + fn new(tag: &str) -> Self { + static COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let dir = std::env::temp_dir() + .join(format!("aura-ingest-test-{tag}-{}-{n}", std::process::id())); + std::fs::create_dir_all(&dir).expect("create scratch dir"); + Self(dir) + } + + fn path(&self) -> &std::path::Path { + &self.0 + } + } + + impl Drop for ScratchDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + #[test] fn instrument_geometry_none_for_unknown_symbol() { // A symbol with no sidecar yields None — the accessor surfaces the reader's @@ -447,6 +606,159 @@ mod tests { } } + /// [`month_start_ms`] anchored against the same 2017-03-01 UTC reference + /// [`unix_ms_to_epoch_ns_scales_ms_to_ns`] pins, plus the Feb/Dec month-end + /// edges [`month_end_ms_exclusive`] must get right (leap-year length, and + /// the December -> next-January year rollover) — the two properties + /// [`archive_extent`]'s per-candidate-month probe bound depends on. + #[test] + fn month_start_and_end_ms_bracket_known_months() { + assert_eq!(month_start_ms(2017, 3), 1_488_326_400_000); + // 2024 is a leap year: February runs through the 29th, so March 1 + // 00:00 UTC is exactly 29 days after February 1 00:00 UTC. + assert_eq!( + month_end_ms_exclusive(2024, 2) - month_start_ms(2024, 2), + 29 * 86_400_000 + ); + // 2023 is not a leap year: 28 days. + assert_eq!( + month_end_ms_exclusive(2023, 2) - month_start_ms(2023, 2), + 28 * 86_400_000 + ); + // December rolls over into the next year's January. + assert_eq!(month_end_ms_exclusive(2024, 12), month_start_ms(2025, 1)); + // A month's end is exclusive: the next month starts immediately after. + assert_eq!(month_end_ms_exclusive(2024, 3), month_start_ms(2024, 4)); + } + + /// [`list_m1_months`] scans `data_path` for `_YYYY_MM.m1` files — + /// mirroring data-server's own `SymbolIndex::scan` regex (`test_symbol_index_scan` + /// in its `lib.rs`) but scoped to one already-known symbol, so it needs no + /// general-purpose parser. Sorted ascending, and never confused by another + /// symbol's file or a non-M1 extension sharing the same directory. + #[test] + fn list_m1_months_lists_and_sorts_the_one_symbols_files() { + let dir = ScratchDir::new("list-months"); + for name in [ + "EURUSD_2020_06.m1", + "EURUSD_2019_12.m1", + "EURUSD_2020_01.m1", + "EURUSD_2020_01.tick", // different format, same symbol+month: ignored + "GBPUSD_2020_01.m1", // different symbol: ignored + "EURUSD_2020_1.m1", // malformed month (one digit): ignored + "not_a_data_file.txt", + ] { + std::fs::write(dir.path().join(name), b"").expect("write stub file"); + } + assert_eq!( + list_m1_months(dir.path(), "EURUSD"), + vec![(2019, 12), (2020, 1), (2020, 6)] + ); + } + + #[test] + fn list_m1_months_empty_for_unknown_symbol_or_missing_dir() { + let dir = ScratchDir::new("list-months-empty"); + std::fs::write(dir.path().join("EURUSD_2020_01.m1"), b"").expect("write stub file"); + assert!(list_m1_months(dir.path(), "NOSYM").is_empty()); + assert!(list_m1_months(Path::new("/nonexistent-archive-path"), "EURUSD").is_empty()); + } + + /// Writes a minimal one-record `_YYYY_MM.m1` zip at `unix_ms_time` + /// (mirrors `data-server`'s own `write_m1_file` test helper — duplicated + /// here, not imported, since that helper lives in its private `#[cfg(test)]` + /// module). Deliberately zip/binary-shaped, not a stub, since + /// [`archive_extent`] must actually load this file through the real + /// `stream_m1_windowed`/`M1FieldSource` seam, not just see its filename. + fn write_one_bar_month(dir: &Path, symbol: &str, year: u16, month: u8, unix_ms_time: i64) { + use std::io::Write; + const DELPHI_EPOCH_OFFSET_DAYS: f64 = 25569.0; + const MS_PER_DAY: f64 = 86_400_000.0; + let dt = unix_ms_time as f64 / MS_PER_DAY + DELPHI_EPOCH_OFFSET_DAYS; + let mut rec = [0u8; 48]; + rec[0..8].copy_from_slice(&dt.to_le_bytes()); + rec[8..16].copy_from_slice(&1.0_f64.to_le_bytes()); // open + rec[16..24].copy_from_slice(&1.0_f64.to_le_bytes()); // high + rec[24..32].copy_from_slice(&1.0_f64.to_le_bytes()); // low + rec[32..40].copy_from_slice(&1.0_f64.to_le_bytes()); // close + rec[40..44].copy_from_slice(&0.0_f32.to_le_bytes()); // spread + rec[44..48].copy_from_slice(&0_i32.to_le_bytes()); // volume + let path = dir.join(format!("{symbol}_{year:04}_{month:02}.m1")); + let file = std::fs::File::create(&path).expect("create synthetic m1 zip"); + let mut zip = zip::ZipWriter::new(file); + zip.start_file::(format!("{symbol}.bin"), Default::default()) + .expect("start synthetic m1 zip entry"); + zip.write_all(&rec).expect("write synthetic m1 record"); + zip.finish().expect("finish synthetic m1 zip"); + } + + /// Property (#252): a requested window whose starting month has NO archive + /// file (a gap) still resolves the first bar, by walking forward to the + /// next available month that actually has one — not refusing outright. + /// Three files (Jan, MISSING Feb, Mar 2021), each holding one bar; a + /// window opening in the missing February must land on March's bar. + #[test] + fn archive_extent_walks_forward_across_a_gap_month() { + let dir = ScratchDir::new("extent-forward-gap"); + let jan_ms = month_start_ms(2021, 1) + 3_600_000; // 2021-01-01 01:00 UTC + let mar_ms = month_start_ms(2021, 3) + 3_600_000; // 2021-03-01 01:00 UTC + write_one_bar_month(dir.path(), "GAPSYM", 2021, 1, jan_ms); + write_one_bar_month(dir.path(), "GAPSYM", 2021, 3, mar_ms); + let server = Arc::new(DataServer::new(dir.path())); + + // From = the start of the missing February; no upper bound. + let from_ms = month_start_ms(2021, 2); + let (first, last) = archive_extent(&server, dir.path(), "GAPSYM", Some(from_ms), None) + .expect("March's bar is reachable by walking past the empty February"); + assert_eq!(first, unix_ms_to_epoch_ns(mar_ms)); + assert_eq!(last, unix_ms_to_epoch_ns(mar_ms)); + } + + /// Property (#252): symmetric to the forward walk, an upper bound landing + /// in a gap month walks BACKWARD to find the last bar of the nearest + /// earlier available month. + #[test] + fn archive_extent_walks_backward_across_a_gap_month() { + let dir = ScratchDir::new("extent-backward-gap"); + let jan_ms = month_start_ms(2021, 1) + 3_600_000; + let mar_ms = month_start_ms(2021, 3) + 3_600_000; + write_one_bar_month(dir.path(), "GAPSYM", 2021, 1, jan_ms); + write_one_bar_month(dir.path(), "GAPSYM", 2021, 3, mar_ms); + let server = Arc::new(DataServer::new(dir.path())); + + // To = the end of the missing February; no lower bound. + let to_ms = month_end_ms_exclusive(2021, 2) - 1; + let (first, last) = archive_extent(&server, dir.path(), "GAPSYM", None, Some(to_ms)) + .expect("January's bar is reachable by walking back past the empty February"); + assert_eq!(first, unix_ms_to_epoch_ns(jan_ms)); + assert_eq!(last, unix_ms_to_epoch_ns(jan_ms)); + } + + /// Property (#252): an unknown symbol (no monthly files at all) yields + /// `None` — the same file-level-absence contract [`open_columns`] and + /// [`instrument_geometry`] already share. + #[test] + fn archive_extent_none_for_unknown_symbol() { + let dir = ScratchDir::new("extent-unknown-symbol"); + let server = Arc::new(DataServer::new(dir.path())); + assert!(archive_extent(&server, dir.path(), "NOSYM", None, None).is_none()); + } + + /// Property (#252): a window entirely before the archive's first month (or + /// entirely after its last) yields `None` rather than snapping to the + /// nearest available month — [`archive_extent`]'s `in_range` bound must + /// reject every candidate, not just prefer the closest one. + #[test] + fn archive_extent_none_when_window_misses_every_month() { + let dir = ScratchDir::new("extent-misses-every-month"); + write_one_bar_month(dir.path(), "GAPSYM", 2021, 3, month_start_ms(2021, 3) + 3_600_000); + let server = Arc::new(DataServer::new(dir.path())); + // Entirely before the one archived month. + assert!(archive_extent(&server, dir.path(), "GAPSYM", None, Some(month_start_ms(2021, 1))).is_none()); + // Entirely after it. + assert!(archive_extent(&server, dir.path(), "GAPSYM", Some(month_start_ms(2021, 5)), None).is_none()); + } + #[test] fn open_columns_propagates_file_level_absence_as_none() { // The open_ohlc contract, generalized: file-level absence (unknown -- 2.52.0 From 84e10751767feb500c7c941b0df30334dbfb14ee Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 13 Jul 2026 16:33:19 +0200 Subject: [PATCH 8/8] fix(test): anchor the knob-lab scaffold under CARGO_TARGET_TMPDIR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit built_scale_project() scaffolded ~66 MB (built node crate) into a pid-suffixed tempdir, so its wipe-before-scaffold never hit a previous run's copy: one leaked directory per test-binary run. 159 of those (~10 GB) filled the host's 16 GB /tmp tmpfs to 100%, breaking every std::env::temp_dir() consumer. A fixed name under CARGO_TARGET_TMPDIR (real disk, reclaimed by cargo clean) makes the existing wipe reclaim the previous run — steady state is exactly one directory. Trade-off: two concurrent cargo-test invocations over the same checkout collide on the fixed name, the documented pre-existing caveat class for this suite (#223). closes #257 --- crates/aura-cli/tests/project_sweep_campaign.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/aura-cli/tests/project_sweep_campaign.rs b/crates/aura-cli/tests/project_sweep_campaign.rs index 1ac77d8..35d848d 100644 --- a/crates/aura-cli/tests/project_sweep_campaign.rs +++ b/crates/aura-cli/tests/project_sweep_campaign.rs @@ -63,11 +63,15 @@ fn aura_in(dir: &Path, args: &[&str]) -> Output { /// `blueprints/scaled_open.json`. Returns the project dir. `OnceLock` so the /// expensive scaffold+build happens once per test binary; the node crate's /// path-dep resolves into this checkout (the baked engine root), like -/// `project_new.rs`. +/// `project_new.rs`. The base is a FIXED name under `CARGO_TARGET_TMPDIR` +/// (real disk, reclaimed by `cargo clean`), not a pid-suffixed tempdir: the +/// wipe below must hit the PREVIOUS run's copy, else every test-binary run +/// leaks the ~66 MB built scaffold — 159 of those filled the 16 GB /tmp +/// tmpfs to 100 % (#257). fn built_scale_project() -> &'static PathBuf { static BUILT: OnceLock = OnceLock::new(); BUILT.get_or_init(|| { - let base = std::env::temp_dir().join(format!("aura-235-{}", std::process::id())); + let base = Path::new(env!("CARGO_TARGET_TMPDIR")).join("aura-235-knob-lab"); let _ = std::fs::remove_dir_all(&base); std::fs::create_dir_all(&base).expect("create scaffold base"); -- 2.52.0