Files
Aura/crates/aura-runner/tests/world_member_run_e2e.rs
T
claude 170c6c82dc feat: full-workspace C28 guard — the shell boundary is structural (#295 part 2)
Completes the shell-boundary cycle (tasks 10-13):

- c28_layering now enumerates the FULL workspace: every crate row is
  reconciled against its real production [dependencies], a completeness
  assertion pins the table to the on-disk crates/ set (a new crate can no
  longer escape the guard silently), the shell/assembly imported-by-
  nothing rule is asserted, and a shell-content check pins aura-cli to
  no-[lib] plus a closed allow-list of argv/translation/presentation
  modules. A new domain module in the shell now fails the suite.
- Ledger amendments: C28 gains the assembly position (aura-runner) and
  the corrected import-rule prose (the ratified aura-campaign ->
  aura-backtest production edge, #291/#292); phase 3 is marked done with
  the #297 process::exit residual named; a provenance note records this
  as structural-debt closure, not a demonstrated downstream blocker.
  C25 records the control-surface decision: the text artifact vocabulary
  is canonical, every control surface (CLI executor verbs, a future
  host, an MCP face, a World program) is a projection/executor over it —
  the which-projection-next ranking deliberately open on #295. C14 gets
  the executor-face amendment, C26 the binding-module relocation.
- aura_campaign::MemberRunner's doc names the shipped default
  implementor (aura-runner::DefaultMemberRunner) while the column keeps
  zero dependency on it.
- New library-only E2E fixture (aura-runner/tests/world_member_run_e2e):
  runs the canonical member recipe over a tiny synthetic archive with no
  aura-cli in the link graph, and pins C1 determinism (two independently
  constructed runners produce a byte-identical RunReport).
- campaign_run.rs module doc: intra-doc MemberRunner link re-anchored to
  aura_campaign::MemberRunner (the impl moved out with part 1).

Verification: cargo test --workspace green (1473 passed, 0 failed);
clippy --workspace --all-targets -D warnings clean; cargo doc clean of
NEW warnings (the five unresolved-link warnings in aura-std/aura-backtest
predate this cycle byte-identically at the anchor — #288-era doc drift,
left for the cycle audit).

refs #295
2026-07-21 06:35:31 +02:00

153 lines
6.4 KiB
Rust

//! Property (#295): the canonical member-run recipe — [`DefaultMemberRunner`]
//! driven through [`aura_campaign::CellSpec`] — is reachable as a **pure
//! library call**: no `aura` binary, no `aura-cli` crate in the dependency
//! graph (this test binary links only `aura-runner` + its own deps). This is
//! the acceptance evidence `examples/world_member_run.rs` demonstrates by
//! printing; here it is pinned by assertion instead of eyeballed output, and
//! runs over a tiny synthetic archive rather than skipping when no local host
//! archive is mounted.
//!
//! A second property rides along for free at no extra fixture cost (C1,
//! determinism): two independently constructed runners (fresh `DataServer`,
//! fresh `DefaultMemberRunner`) driven over byte-identical input produce a
//! byte-identical `RunReport` — same input, same run, reproducibly.
use std::collections::BTreeMap;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use aura_campaign::{CellSpec, MemberRunner};
use aura_ingest::DataServer;
use aura_runner::{DefaultMemberRunner, Env};
const SYMBOL: &str = "TESTSYM";
// 2024-06-03T08:00:00Z (a real Monday) — arbitrary, fixed, deterministic.
const BASE_MS: i64 = 1_717_401_600_000;
const N_BARS: i64 = 60;
/// A unique scratch directory under the OS temp root, removed on drop —
/// mirrors `aura-ingest`'s own `tests::ScratchDir` (pid + atomic counter, no
/// `tempfile` dev-dependency needed for one throwaway fixture dir).
struct ScratchDir(PathBuf);
impl ScratchDir {
fn new() -> 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-runner-e2e-{}-{n}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create scratch dir");
Self(dir)
}
fn path(&self) -> &Path {
&self.0
}
}
impl Drop for ScratchDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
/// Packs one `RawM1Record`-shaped 48-byte little-endian record — the exact
/// on-disk format `data-server` reads (mirrors `aura-ingest`'s and
/// `aura-cli`'s own synthetic-archive test helpers).
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
}
/// Writes a tiny, deterministic one-month M1 archive (`N_BARS` consecutive
/// 1-minute bars, strictly ascending close so the shipped Donchian breakout
/// blueprint actually latches a non-flat bias) plus its geometry sidecar,
/// under `dir`. Returns the inclusive `(first_ms, last_ms)` bar span.
fn write_tiny_archive(dir: &Path) -> (i64, i64) {
let path = dir.join(format!("{SYMBOL}_2024_06.m1"));
let file = std::fs::File::create(&path).expect("create synthetic m1 zip");
let mut zip = zip::ZipWriter::new(file);
zip.start_file::<String, ()>(format!("{SYMBOL}.bin"), Default::default())
.expect("start synthetic m1 zip entry");
for i in 0..N_BARS {
let ms = BASE_MS + i * 60_000;
let close = 100.0 + i as f64 * 2.0; // strictly ascending -> a real breakout
let open = close - 1.0;
let high = close + 0.5;
let low = open - 0.5;
zip.write_all(&pack_record(ms, open, high, low, close)).expect("write record");
}
zip.finish().expect("finish synthetic m1 zip");
std::fs::write(
dir.join(format!("{SYMBOL}.meta.json")),
"{\"schemaVersion\":2,\"symbol\":\"TESTSYM\",\"digits\":1,\"pipSize\":1,\
\"tickSize\":0.1,\"lotSize\":1,\"baseAsset\":\"TESTSYM\",\"quoteAsset\":\"EUR\"}",
)
.expect("write synthetic geometry sidecar");
(BASE_MS, BASE_MS + (N_BARS - 1) * 60_000)
}
/// One fresh `DefaultMemberRunner` over its own `DataServer` instance pointed
/// at `data_dir`, running `cell` end to end.
fn run_once(
data_dir: &Path,
cell: &CellSpec,
window_ms: (i64, i64),
) -> Result<aura_backtest::RunReport, aura_campaign::MemberFault> {
let env = Env::std();
let server = std::sync::Arc::new(DataServer::new(data_dir));
let runner = DefaultMemberRunner::new(&env, server, BTreeMap::new(), Vec::new());
runner.run_member(cell, &[], window_ms)
}
#[test]
/// Property: `DefaultMemberRunner::run_member` — the shipped C28 assembly
/// recipe — runs a real member backtest to completion through nothing but
/// `aura-runner` + `aura-campaign` library calls, and does so deterministically:
/// two independently constructed runners over byte-identical archive input
/// produce a byte-identical `RunReport` (C1).
fn default_member_runner_is_reachable_as_a_pure_library_call_and_is_deterministic() {
let scratch = ScratchDir::new();
let (first_ms, last_ms) = write_tiny_archive(scratch.path());
let blueprint_json =
include_str!("../../aura-cli/examples/r_breakout.json").to_string();
let cell = CellSpec {
strategy_ordinal: 0,
strategy_id: aura_research::content_id_of(&blueprint_json),
blueprint_json,
axes: BTreeMap::new(),
instrument: SYMBOL.to_string(),
window_ms: (first_ms, last_ms),
regime: None,
regime_ordinal: 0,
};
let report_a = run_once(scratch.path(), &cell, (first_ms, last_ms))
.expect("the member run must succeed over its own tiny archive");
let report_b = run_once(scratch.path(), &cell, (first_ms, last_ms))
.expect("a second, independently constructed runner must succeed identically");
assert_eq!(
report_a, report_b,
"same cell, same archive, two independent DefaultMemberRunner instances: \
the RunReport must be byte-identical (C1 determinism)"
);
assert!(
report_a.metrics.total_pips.abs() > 0.0,
"the fixture's strictly-ascending closes must drive a non-flat run, else \
the determinism check above is vacuous (both sides equally empty): {:?}",
report_a.metrics
);
}