Files
Aura/crates/aura-registry/tests/campaign_run_store_e2e.rs
claude a56ab7859d refactor: cut the engine's backtest-metrics edge via RunReport<M>
C28 phase 2 (Stratification); realizes item 1 of the deferred #147. The
engine's production surface no longer names a backtest-metric type:

- RunReport becomes generic over its metric payload M; sweep/mc/walkforward/
  blueprint thread the parameter (SweepPoint<M>, SweepFamily<M>, WindowRun<M>,
  WalkForwardResult<M>). RunManifest stays concrete and engine-owned (its
  selection: Option<FamilySelection> embeds the foundation-grade analysis type).
- summarize and the MC assembly (McDraw/McFamily/McAggregate/RBootstrap/
  r_bootstrap/monte_carlo) move to aura-backtest - McAggregate::from_draws reads
  RunMetrics fields by name, so generifying it is the phase-6 metric-vocabulary
  abstraction (#147 item 2), still deferred; wholesale relocation is the honest
  cut. The concrete instantiation lives in aura-backtest as
  `type RunReport = aura_engine::RunReport<RunMetrics>` + sibling aliases.
- the statistics kernel (MetricStats/quantile/resample_block/SplitMix64) moves
  to the aura-analysis foundation; the engine re-imports it (inner->foundation,
  legal) and re-exports it so existing consumers stay source-compatible.

Dependency inversion in one commit: aura-engine drops aura-backtest from
[dependencies] (back to dev-deps for its SimBroker/RunMetrics test fixtures);
aura-backtest gains aura-engine. Cycle-free for lib targets - the cycle closes
only through the engine's dev-dep edge, the pattern aura-vocabulary already
uses. aura-backtest reaches the kernel transitively through the engine
re-export, so no aura-backtest -> aura-analysis edge exists (the C28 ladder
permits backtest -> {core, engine} only). run_indexed / SplitMix64::next_f64
widened pub(crate) -> pub for cross-crate use.

Consumers (registry/campaign/cli/composites/ingest/bench) rewired by import
path only, no call-site logic changed. The c28_layering structural test extends
to the full ladder: aura-analysis (no aura-* deps), aura-engine ⊆ {core,
analysis}, aura-backtest ⊆ {core, engine}.

Behaviour-preserving: 1448/0 tests, clippy -D warnings clean, serde shapes
byte-identical (C18 - RunReport<M> keeps field order manifest,metrics; the
CLI pre-serialized-splice contract unchanged), moved code traceable via git
rename detection. Cycle-introduced broken intra-doc links fixed.

closes #292
2026-07-20 13:25:07 +02:00

140 lines
5.8 KiB
Rust

//! End-to-end coverage for the campaign-run store (cycle 0107 task 3): the
//! `campaign_runs.jsonl` sibling store's persistence and isolation properties,
//! driven through the PUBLIC `Registry` surface only (`append_family` /
//! `append_campaign_run` / `load_family_members` / `load_campaign_runs`) — the
//! in-crate unit tests in `lib.rs`/`lineage.rs` pin the counter and round-trip
//! shape on one live `Registry` instance; these tests pin the two properties a
//! real multi-invocation CLI depends on: the store survives a fresh `Registry`
//! handle (it is a real on-disk file, not an in-memory cache), and writing to
//! it never perturbs the existing family store it sits beside.
use std::fs;
use std::path::PathBuf;
use aura_core::{Scalar, Timestamp};
use aura_backtest::{RunMetrics, RunReport};
use aura_engine::{FamilySelection, RunManifest, SelectionMode};
use aura_registry::{
CampaignRunRecord, CellRealization, FamilyKind, Registry, StageRealization, StageSelection,
};
fn temp_dir(name: &str) -> PathBuf {
let dir = std::path::Path::new(env!("CARGO_TARGET_TMPDIR"))
.join(format!("aura-registry-campaign-e2e-{name}"));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).expect("create temp dir");
dir.join("runs.jsonl")
}
fn tiny_report() -> RunReport {
RunReport {
manifest: RunManifest {
commit: "c".to_string(),
params: vec![],
defaults: vec![],
window: (Timestamp(1), Timestamp(2)),
seed: 0,
broker: "b".to_string(),
selection: None,
instrument: None,
topology_hash: None,
project: None,
},
metrics: RunMetrics { total_pips: 1.0, max_drawdown: 0.0, bias_sign_flips: 0, r: None },
}
}
fn campaign_run(campaign: &str) -> CampaignRunRecord {
CampaignRunRecord {
campaign: campaign.to_string(),
process: "proc".to_string(),
run: 0,
seed: 1,
cells: vec![CellRealization {
strategy: "strat-id".to_string(),
instrument: "EURUSD".to_string(),
window_ms: (0, 1000),
regime: None,
regime_ordinal: 0,
fault: None,
coverage: None,
stages: vec![StageRealization {
block: "std::sweep".to_string(),
family_id: Some(format!("{campaign}-0-EURUSD-w0-s0-0")),
survivor_ordinals: None,
selection: Some(StageSelection {
winner_ordinal: 0,
params: vec![("x".to_string(), Scalar::i64(1))],
selection: FamilySelection {
selection_metric: "sqn_normalized".to_string(),
n_trials: 1,
raw_winner_metric: 1.0,
mode: SelectionMode::Argmax,
deflated_score: None,
overfit_probability: None,
n_resamples: None,
block_len: None,
seed: None,
neighbourhood_score: None,
n_neighbours: None,
},
}),
bootstrap: None,
window_faults: vec![],
}],
}],
generalizations: vec![],
trace_name: None,
}
}
/// Property: the campaign-run store is a real on-disk file, not a cache tied to
/// one `Registry` instance — a record appended by one `Registry::open` handle
/// is visible to a SECOND, freshly-opened handle over the same path. This is
/// what makes the store usable across separate CLI invocations (author runs
/// `aura campaign run` twice, in two processes, against the same registry).
#[test]
fn campaign_run_persists_across_fresh_registry_handles() {
let path = temp_dir("persist_across_handles");
let writer = Registry::open(&path);
let run = writer.append_campaign_run(&campaign_run("camp-a")).expect("append");
assert_eq!(run, 0);
// a brand-new handle, as a second CLI invocation would construct
let reader = Registry::open(&path);
let loaded = reader.load_campaign_runs().expect("load from fresh handle");
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].campaign, "camp-a");
assert_eq!(loaded[0].run, 0);
}
/// Property: `campaign_runs.jsonl` is a sibling store, wholly independent of
/// the existing `families.jsonl` flat-run store it sits beside — appending a
/// campaign run does not add, remove, or alter a single family member record,
/// and appending a family does not touch the campaign-run store. Two
/// independently-growing stores at the same registry root, per the module's
/// documented growth pattern.
#[test]
fn campaign_run_store_does_not_perturb_the_sibling_family_store() {
let path = temp_dir("sibling_isolation");
let reg = Registry::open(&path);
let before_families = reg.append_family("fam", FamilyKind::Sweep, &[tiny_report()]).unwrap();
let members_before = reg.load_family_members().expect("load families before");
assert_eq!(members_before.len(), 1);
reg.append_campaign_run(&campaign_run("camp-b")).expect("append campaign run");
// the family store is byte-for-byte unaffected by the campaign-run write
let members_after = reg.load_family_members().expect("load families after");
assert_eq!(members_after, members_before, "family store untouched by a campaign-run append");
assert_eq!(before_families, "fam-0");
// and the reverse: appending another family leaves the campaign-run store's
// single record alone
reg.append_family("fam2", FamilyKind::Sweep, &[tiny_report()]).unwrap();
let runs = reg.load_campaign_runs().expect("load campaign runs");
assert_eq!(runs.len(), 1, "campaign-run store untouched by a family append");
assert_eq!(runs[0].campaign, "camp-b");
}