db8f947441
Slice 6 of the #319 retirement, the destructive half. The five flag verbs, their five Args structs and dispatch arms, the whole argv->document translator (verb_sugar.rs, 1257 lines) and 33 quintet-only helpers are gone; retired verbs now refuse at the clap layer as unknown commands. main.rs shrinks 4445 -> 2764 lines. The gated-intake route list narrows to the canonical layer + exec's blueprint leg; exec's own refusal prose no longer names dead verbs; test seeding recipes ride graph register. cli_run.rs walked the full disposition table: 94 tests retired with their dead flag surface (each with a named green twin where the property survives), 75 ported — exec ports keep assertions byte-identical, campaign ports inherit their golden grades unchanged (walkforward's 90/30/30 real roller and the archive window clipping the sugar applied silently are now explicit document fields — the equivalence held exactly, one reconstructed walkforward pooled expectancy_r within 1e-12, cause documented inline). Surface deltas recorded by the ports: per-cell refusals are #272 contained faults (exit 3 + warning) rather than hard exit 1; the synthetic per-seed mc family and generalize's merged cross-instrument family record were sugar-only constructs and retire with it (the campaign record's generalizations[] carries the data). refs #319
4566 lines
229 KiB
Rust
4566 lines
229 KiB
Rust
//! 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;
|
||
use std::process::Command;
|
||
|
||
mod common;
|
||
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`).
|
||
const BIN: &str = env!("CARGO_BIN_EXE_aura");
|
||
|
||
/// A fresh, unique working directory for a process test that persists run
|
||
/// records (so `aura sweep`'s `./runs/runs.jsonl` never dirties the repo).
|
||
/// Unique per test + per process; no external tempfile dependency.
|
||
fn temp_cwd(name: &str) -> std::path::PathBuf {
|
||
let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("aura-cli-{name}"));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
std::fs::create_dir_all(&dir).expect("create temp cwd");
|
||
dir
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Campaign-document scaffold (#319 Task 8 Step 4 port). Retired-verb argv
|
||
// tests that reach engine/campaign semantics are ported onto a hand-authored
|
||
// campaign document driven through `exec` — the surviving executor. This
|
||
// mirrors `tests/exec.rs`/`tests/research_docs.rs`'s idiom of the same names;
|
||
// duplicated here (rather than shared) because each integration-test binary
|
||
// compiles independently and cannot see another binary's private items.
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// Run the built binary in `dir`, returning combined stdout+stderr and the
|
||
/// exit code — the `exec.rs`/`research_docs.rs` idiom, used by every campaign-
|
||
/// document port below.
|
||
fn run_code_in(dir: &Path, args: &[&str]) -> (String, Option<i32>) {
|
||
let out = Command::new(BIN).args(args).current_dir(dir).output().expect("binary runs");
|
||
let text =
|
||
format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
|
||
(text, out.status.code())
|
||
}
|
||
|
||
fn write_doc(dir: &Path, name: &str, text: &str) -> std::path::PathBuf {
|
||
let p = dir.join(name);
|
||
std::fs::write(&p, text).expect("write doc");
|
||
p
|
||
}
|
||
|
||
/// Seed the shipped closed `examples/r_sma.json` blueprint into the project's
|
||
/// store via `aura graph register` and return its content id.
|
||
fn seed_blueprint(dir: &Path, name: &str) -> String {
|
||
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||
let (out, code) = run_code_in(dir, &["graph", "register", &closed_bp, "--name", name]);
|
||
assert_eq!(code, Some(0), "seed register failed: {out}");
|
||
out.lines()
|
||
.find(|l| l.starts_with("registered blueprint "))
|
||
.expect("register line")
|
||
.trim_start_matches("registered blueprint ")
|
||
.split(' ')
|
||
.next()
|
||
.expect("id")
|
||
.trim_start_matches("content:")
|
||
.to_string()
|
||
}
|
||
|
||
/// Seed an arbitrary blueprint FILE (not necessarily `r_sma.json`) into the
|
||
/// project's store, returning its content id.
|
||
fn seed_blueprint_file(dir: &Path, fixture: &str, name: &str) -> String {
|
||
let (out, code) = run_code_in(dir, &["graph", "register", fixture, "--name", name]);
|
||
assert_eq!(code, Some(0), "seed register failed: {out}");
|
||
out.lines()
|
||
.find(|l| l.starts_with("registered blueprint "))
|
||
.expect("register line")
|
||
.trim_start_matches("registered blueprint ")
|
||
.split(' ')
|
||
.next()
|
||
.expect("id")
|
||
.trim_start_matches("content:")
|
||
.to_string()
|
||
}
|
||
|
||
/// Register `doc` as a process document in the project store; returns its id.
|
||
fn register_process_doc(dir: &Path, file: &str, doc: &str) -> String {
|
||
write_doc(dir, file, doc);
|
||
let (out, code) = run_code_in(dir, &["process", "register", file]);
|
||
assert_eq!(code, Some(0), "process register failed: {out}");
|
||
out.lines()
|
||
.find(|l| l.starts_with("registered process "))
|
||
.expect("register line")
|
||
.trim_start_matches("registered process ")
|
||
.split(' ')
|
||
.next()
|
||
.expect("id")
|
||
.trim_start_matches("content:")
|
||
.to_string()
|
||
}
|
||
|
||
/// A minimal executable `[std::sweep]` process (one selection-bearing stage) —
|
||
/// mirrors `exec.rs`/`research_docs.rs`'s `SWEEP_ONLY_PROCESS_DOC`.
|
||
const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
|
||
"format_version": 1,
|
||
"kind": "process",
|
||
"name": "sweep-only",
|
||
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
|
||
}"#;
|
||
|
||
/// A `[std::grid, std::walk_forward]` process — the #256 fork B shape a
|
||
/// hand-authored (non-sugar) op-script uses — mirrors `exec.rs`/
|
||
/// `research_docs.rs`'s `GRID_THEN_WF_PROCESS_DOC`. `std::grid` only
|
||
/// enumerates (persists no family of its own); `std::walk_forward` does the
|
||
/// per-window IS-refit and persists the one `WalkForward` family.
|
||
const GRID_THEN_WF_PROCESS_DOC: &str = r#"{
|
||
"format_version": 1,
|
||
"kind": "process",
|
||
"name": "grid-then-walkforward",
|
||
"pipeline": [
|
||
{ "block": "std::grid" },
|
||
{ "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000,
|
||
"step_ms": 604800000, "mode": "rolling", "metric": "sqn_normalized", "select": "argmax" }
|
||
]
|
||
}"#;
|
||
|
||
/// A `[std::grid, std::walk_forward]` process using the FIXED real-archive
|
||
/// roller (`aura_backtest::wf_ms_sizes`, 90d IS / 30d OOS / 30d step — the
|
||
/// retired sugar's `fit_wf_ms_sizes` fast-path over a year-plus window) —
|
||
/// needed for the exact-grade byte-identity anchors, which pin the retired
|
||
/// verb's own fixed real-data roller, unlike `GRID_THEN_WF_PROCESS_DOC`'s
|
||
/// shorter 14d/7d/7d roller (sized for the one-month e2e windows elsewhere
|
||
/// in this file/`exec.rs`/`research_docs.rs`).
|
||
const GRID_THEN_WF_PROCESS_DOC_90_30_30: &str = r#"{
|
||
"format_version": 1,
|
||
"kind": "process",
|
||
"name": "grid-then-walkforward-90-30-30",
|
||
"pipeline": [
|
||
{ "block": "std::grid" },
|
||
{ "block": "std::walk_forward", "in_sample_ms": 7776000000, "out_of_sample_ms": 2592000000,
|
||
"step_ms": 2592000000, "mode": "rolling", "metric": "sqn_normalized", "select": "argmax" }
|
||
]
|
||
}"#;
|
||
|
||
/// A `[std::grid, std::walk_forward, std::monte_carlo]` process — mirrors
|
||
/// `translate_mc`'s generated shape exactly (the retired `mc --real` sugar):
|
||
/// the leading `std::grid` only enumerates, `std::walk_forward` persists the
|
||
/// one family, and the terminal `std::monte_carlo` r-bootstraps the pooled
|
||
/// per-window OOS trade-R series (`StageBootstrap::PooledOos`).
|
||
const GRID_THEN_WF_THEN_MC_PROCESS_DOC: &str = r#"{
|
||
"format_version": 1,
|
||
"kind": "process",
|
||
"name": "grid-then-walkforward-then-mc",
|
||
"pipeline": [
|
||
{ "block": "std::grid" },
|
||
{ "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000,
|
||
"step_ms": 604800000, "mode": "rolling", "metric": "sqn_normalized", "select": "argmax" },
|
||
{ "block": "std::monte_carlo", "resamples": 100, "block_len": 5 }
|
||
]
|
||
}"#;
|
||
|
||
/// A `[std::sweep, std::generalize]` process — mirrors `translate_generalize`'s
|
||
/// generated shape exactly (the retired `generalize` sugar): the sweep stage
|
||
/// is selection-bearing (a single-value axis grid, so selection is trivial),
|
||
/// and `std::generalize` computes the cross-instrument grade.
|
||
const SWEEP_THEN_GENERALIZE_PROCESS_DOC: &str = r#"{
|
||
"format_version": 1,
|
||
"kind": "process",
|
||
"name": "sweep-then-generalize",
|
||
"pipeline": [
|
||
{ "block": "std::sweep", "metric": "expectancy_r", "select": "argmax" },
|
||
{ "block": "std::generalize", "metric": "expectancy_r" }
|
||
]
|
||
}"#;
|
||
|
||
/// A one-instrument, one-window, N-axis-value campaign document over the
|
||
/// seeded `r_sma.json` blueprint — the dissolved sugar's own generated shape
|
||
/// (`translate_sweep`/`translate_walkforward`/`translate_mc`): axes are the
|
||
/// RAW `<node>.<param>` names (#328), `seed: 7` (an arbitrary constant, the
|
||
/// sugar campaign path never threads a sweep seed either). No risk section
|
||
/// (every call site here is stop-free); build the document directly when a
|
||
/// risk regime is needed.
|
||
fn sweep_campaign_doc(
|
||
name: &str,
|
||
instrument: &str,
|
||
bp_id: &str,
|
||
proc_id: &str,
|
||
window: (i64, i64),
|
||
fast_values: &str,
|
||
slow_values: &str,
|
||
) -> String {
|
||
format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "{name}",
|
||
"data": {{ "instruments": ["{instrument}"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [{fast_values}] }},
|
||
"slow.length": {{ "kind": "I64", "values": [{slow_values}] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
from = window.0,
|
||
to = window.1,
|
||
)
|
||
}
|
||
|
||
|
||
/// Property (ported onto a one-cell campaign document, #319 Task 8 — the
|
||
/// retired `run --real` verb's own hard exit-1/no-stdout contract does not
|
||
/// survive the campaign path's #272 contained-fault model, but the pip-honesty
|
||
/// substance does): the geometry-sidecar lookup precedes any bar-data access,
|
||
/// so a symbol with no recorded geometry refuses with NO local data required
|
||
/// (CI-safe). "NONEXISTENT" has no recorded geometry on any host. A single-cell
|
||
/// campaign realizes this as a CONTAINED cell fault (#272: `aura: warning: `
|
||
/// class marker, deliberate exit 3, never a hard exit 1) — the pip lookup still
|
||
/// fires before any bar-data access, and the fault's own detail carries the
|
||
/// SAME "no recorded geometry" message `run --real` used to print to stderr.
|
||
#[test]
|
||
fn run_real_no_geometry_symbol_refuses_with_exit_1() {
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "geo-refusal-seed");
|
||
let proc_id = register_process_doc(&dir, "geo.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc("geo", "NONEXISTENT", &bp_id, &proc_id, (1, 2), "2", "8");
|
||
write_doc(&dir, "geo.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "geo.campaign.json"]);
|
||
assert_eq!(code, Some(3), "a contained no-geometry cell fault is exit 3, never exit 1: {out}");
|
||
assert!(
|
||
out.contains("no recorded geometry for symbol 'NONEXISTENT'"),
|
||
"expected the per-instrument-pip refusal message in the contained fault, got: {out}"
|
||
);
|
||
assert!(out.contains("aura: warning: "), "the fault carries the #272 class marker: {out}");
|
||
}
|
||
|
||
/// Property: the pip-refusal is sourced ONLY from recorded provider geometry — the
|
||
/// removed authored instrument floor (the hand-authored "vetted" table) no
|
||
/// longer participates in the refusal path. The cycle-0074 headline is the floor's
|
||
/// REMOVAL, not a rename; the observable signature is that the refusal message
|
||
/// never points the user at a hand-authored table to extend. A regression that
|
||
/// re-introduced an authored fallback (or restored the old "add it to the
|
||
/// instrument table" wording) would re-leak this vocabulary. Asserts the OLD
|
||
/// authored-floor phrasing is ABSENT — the negative complement to the positive
|
||
/// "no recorded geometry" assertion above. Archive-independent: "NONEXISTENT" has
|
||
/// no sidecar on any host, so the refusal fires without local data. Ported onto
|
||
/// the campaign path's contained fault (see the sibling above for the exit-code
|
||
/// rationale).
|
||
#[test]
|
||
fn run_real_refusal_names_no_authored_floor() {
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "geo-floor-seed");
|
||
let proc_id = register_process_doc(&dir, "geofloor.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc("geofloor", "NONEXISTENT", &bp_id, &proc_id, (1, 2), "2", "8");
|
||
write_doc(&dir, "geofloor.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "geofloor.campaign.json"]);
|
||
assert_eq!(code, Some(3), "a contained no-geometry cell fault is exit 3: {out}");
|
||
assert!(!out.contains("instrument table"), "refusal must not point at a removed authored table, got: {out}");
|
||
assert!(!out.contains("vetted"), "refusal must not use the removed vetted-floor vocabulary, got: {out}");
|
||
assert!(!out.contains("instrument spec"), "refusal must not reference the removed authored instrument floor, got: {out}");
|
||
}
|
||
|
||
/// Property (adapted for the campaign path's always-on `{"campaign_run":...}`
|
||
/// summary line, #319 Task 8): the un-specced-symbol refusal never leaks a
|
||
/// partial `RunReport` — the #22 acceptance is "refuse INSTEAD of emitting
|
||
/// nonsense". The retired `run --real` verb's OWN packaging of that refusal
|
||
/// was silence on stdout entirely; the campaign executor always emits its one
|
||
/// `{"campaign_run":...}` audit line regardless of outcome (#272's whole
|
||
/// point — a fault is RECORDED, not swallowed), so "no stdout at all" cannot
|
||
/// survive verbatim. What DOES survive: the faulted cell carries NO `report`
|
||
/// key at all (no partial manifest/metrics — only a `fault`), and no
|
||
/// `family_table` member line is emitted for it either.
|
||
#[test]
|
||
fn run_real_no_geometry_symbol_emits_no_stdout_report() {
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "geo-nostdout-seed");
|
||
let proc_id = register_process_doc(&dir, "geonostdout.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc("geonostdout", "NONEXISTENT", &bp_id, &proc_id, (1, 2), "2", "8");
|
||
write_doc(&dir, "geonostdout.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "geonostdout.campaign.json"]);
|
||
assert_eq!(code, Some(3), "a contained no-geometry cell fault is exit 3: {out}");
|
||
assert!(
|
||
!out.contains("\"report\":"),
|
||
"the refusal must not leak a partial report for the faulted cell, got: {out}"
|
||
);
|
||
assert!(
|
||
!out.lines().any(|l| l.starts_with("{\"family_id\":")),
|
||
"no family_table member line for a cell that never ran: {out}"
|
||
);
|
||
assert!(out.contains("\"fault\":"), "the faulted cell is recorded as a fault, not swallowed: {out}");
|
||
}
|
||
|
||
/// Property (ported onto a one-cell campaign document): the pip-honesty refusal
|
||
/// keys on the SYMBOL having no recorded geometry, not on real data being
|
||
/// requested at all. A symbol *with* a recorded sidecar (`EURUSD`) gets PAST
|
||
/// the geometry lookup, so it never produces the pip-refusal message — it
|
||
/// either runs to a report (local data present) or hits the DISTINCT
|
||
/// no-local-data path ("no local data for symbol"), both contained per #272
|
||
/// (exit 0 success, or exit 3 with the no-data fault detail — never the
|
||
/// geometry-refusal wording).
|
||
#[test]
|
||
fn run_real_sidecar_symbol_never_hits_the_pip_refusal() {
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "eurusd-sidecar-seed");
|
||
let proc_id = register_process_doc(&dir, "eurusd.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc(
|
||
"eurusd-sidecar", "EURUSD", &bp_id, &proc_id,
|
||
(EURUSD_JUN2024_FROM_MS, EURUSD_JUN2024_TO_MS), "2", "8",
|
||
);
|
||
write_doc(&dir, "eurusd.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "eurusd.campaign.json"]);
|
||
assert!(
|
||
!out.contains("no recorded geometry"),
|
||
"a symbol with a recorded sidecar must clear the pip lookup, never the pip-refusal: {out}"
|
||
);
|
||
match code {
|
||
Some(0) => assert!(
|
||
out.lines().any(|l| l.starts_with("{\"family_id\":")),
|
||
"exit 0 must carry a family member report: {out}"
|
||
),
|
||
Some(3) => assert!(
|
||
out.contains("no local data for symbol 'EURUSD'"),
|
||
"a contained fault for a symbol with a recorded sidecar must be the no-data path, got: {out}"
|
||
),
|
||
other => panic!("unexpected exit for a real cell with a recorded sidecar: {other:?}; out: {out}"),
|
||
}
|
||
}
|
||
|
||
/// Property (ported onto a one-cell campaign document): a recorded-sidecar
|
||
/// symbol threads its looked-up pip all the way to the emitted member
|
||
/// manifest — GER40 renders the INDEX pip `sim-optimal(pip_size=1)`, not the
|
||
/// FX `0.0001` literal the synthetic path uses. This is the #22 headline at
|
||
/// the built-binary boundary: the metadata channel reaches stdout, so equity
|
||
/// is honestly scaled per instrument. Gated on local GER40 data — skips
|
||
/// cleanly when the archive is absent (a contained no-data fault, #272,
|
||
/// rather than the old hard exit 1).
|
||
#[test]
|
||
fn run_real_sidecar_index_pip_reaches_the_emitted_manifest() {
|
||
// The GER40 Sept-2024 window (inclusive Unix-ms), the same calendar
|
||
// month the gated ingest path drives; bounding it keeps the run fast.
|
||
const FROM_MS: i64 = 1725148800000;
|
||
const TO_MS: i64 = 1727740799999;
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "ger40-indexpip-seed");
|
||
let proc_id = register_process_doc(&dir, "ger40pip.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc("ger40pip", "GER40", &bp_id, &proc_id, (FROM_MS, TO_MS), "2", "8");
|
||
write_doc(&dir, "ger40pip.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "ger40pip.campaign.json"]);
|
||
|
||
// Skip on a data-less machine: a recorded-sidecar symbol with no local data
|
||
// takes the distinct no-data path (contained fault, exit 3), never the
|
||
// pip-refusal.
|
||
if code == Some(3) && out.contains("no local data for symbol 'GER40'") {
|
||
eprintln!("skip: no local GER40 data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {code:?}, out: {out}");
|
||
// the looked-up index pip (1.0 -> "1") reaches the manifest, not the FX 0.0001.
|
||
assert!(
|
||
out.contains("\"broker\":\"sim-optimal+risk-executor(pip_size=1)\""),
|
||
"GER40 must render the index pip in the manifest, got: {out}"
|
||
);
|
||
}
|
||
|
||
/// Property (ported onto a one-cell campaign document): the authored instrument
|
||
/// table no longer gates real runs — a symbol absent from the (removed)
|
||
/// vetted floor but carrying a recorded geometry sidecar resolves its pip from
|
||
/// the sidecar and runs. USDJPY (never vetted; JPY pip 0.01) renders the JPY
|
||
/// pip in the manifest, proving the pip is sourced from the provider geometry,
|
||
/// not a hand-authored table entry. Gated: skips cleanly (contained fault,
|
||
/// #272) when USDJPY has no local geometry/data.
|
||
#[test]
|
||
fn run_real_nonvetted_symbol_resolves_pip_from_sidecar() {
|
||
// FX trades continuously; the GER40 Sept-2024 window also has USDJPY bars.
|
||
const FROM_MS: i64 = 1725148800000;
|
||
const TO_MS: i64 = 1727740799999;
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "usdjpy-sidecar-seed");
|
||
let proc_id = register_process_doc(&dir, "usdjpy.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc("usdjpy", "USDJPY", &bp_id, &proc_id, (FROM_MS, TO_MS), "2", "8");
|
||
write_doc(&dir, "usdjpy.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "usdjpy.campaign.json"]);
|
||
|
||
// Skip on a host without USDJPY geometry/data — either refusal is a clean
|
||
// contained-fault skip, sourced from recorded geometry, never an
|
||
// authored-table miss.
|
||
if code == Some(3) && (out.contains("no recorded geometry") || out.contains("no local data")) {
|
||
eprintln!("skip: no local USDJPY geometry/data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "USDJPY should resolve and run; exit: {code:?}, out: {out}");
|
||
assert!(
|
||
out.contains("pip_size=0.01"),
|
||
"USDJPY must render the JPY sidecar pip (0.01) in the manifest, got: {out}"
|
||
);
|
||
}
|
||
|
||
|
||
|
||
|
||
// Note (#159 cut 4 collateral): `--trace` on a single `aura run`/`aura sweep`
|
||
// is no longer reachable from any CLI surface — the built-in default that
|
||
// accepted it retired with the last two PIP strategies, and the blueprint
|
||
// branches (`dispatch_run`, `run_blueprint_sweep`) never wire a trace-persist
|
||
// path. Repointing would require new production code (out of scope for a
|
||
// test-fixing task), so the single-run trace-persist + shape tests are
|
||
// deleted here rather than adjusted.
|
||
|
||
/// Property: an unknown `chart <name>` is a runtime failure pinned at BOTH the exit code
|
||
/// and the user-facing message — exit 1 (never a panic) AND the NotFound branch's
|
||
/// "no recorded run or family '<name>'" wording, which now covers families too. A
|
||
/// regression that drifted the message (e.g. back to the run-only phrasing) or
|
||
/// downgraded the exit would slip past the bundled exit-only check; this pins the
|
||
/// message/exit contract. Archive-local: charts into an empty cwd so the name is
|
||
/// genuinely absent.
|
||
#[test]
|
||
fn chart_unknown_name_refuses_with_exit_1_and_message() {
|
||
let cwd = temp_cwd("chart-unknown");
|
||
|
||
let out = Command::new(BIN).args(["chart", "ghost"]).current_dir(&cwd).output().expect("spawn chart ghost");
|
||
assert_eq!(out.status.code(), Some(1), "unknown name must exit 1: {:?}", out.status);
|
||
assert!(out.stdout.is_empty(), "the refusal must not leak a chart page to stdout, got: {:?}", String::from_utf8_lossy(&out.stdout));
|
||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||
assert!(
|
||
stderr.contains("no recorded run or family 'ghost'"),
|
||
"expected the unknown-name (run-or-family) refusal message, got: {stderr}"
|
||
);
|
||
|
||
let _ = std::fs::remove_dir_all(&cwd);
|
||
}
|
||
|
||
/// Property (milestone fieldtest B1): a sweep / walk-forward `--trace` family is
|
||
/// chartable by the very handle the producing run prints. Those verbs fan a
|
||
/// selection-free sweep out per member into the #224 depth-2 layout
|
||
/// `runs/traces/<name>/<cell>/<member>/index.json` (one directory level deeper
|
||
/// than the campaign path's depth-1 nominee layout `<name>/<cell>/index.json`).
|
||
/// `aura chart <handle>` must resolve that fan-out as a family: exit 0 with the
|
||
/// chart page on stdout, covering the members' taps. AND the genuinely-absent-
|
||
/// handle refusal must not tell the user to re-run a command with that same
|
||
/// handle as the `--trace` argument — that command produced the data (it would
|
||
/// already be on disk), so pointing back at it is a dead-end remedy.
|
||
///
|
||
/// The fixture is fabricated inline through `TraceStore` (no archive, no
|
||
/// data-server, no sweep run): two members under one cell, exactly the depth-2
|
||
/// shape `persist_campaign_traces`' member fan-out writes. On today's tree the
|
||
/// depth-1-only `name_kind` / `read_family` classify the handle as NotFound, so
|
||
/// `chart` exits 1 with the misleading remedy — RED on both counts.
|
||
#[test]
|
||
fn chart_opens_a_sweep_trace_family_by_its_printed_handle() {
|
||
use aura_core::{Scalar, ScalarKind, Timestamp};
|
||
use aura_engine::{ColumnarTrace, RunManifest};
|
||
use aura_registry::TraceStore;
|
||
|
||
let cwd = temp_cwd("chart-sweep-family");
|
||
|
||
let manifest = || RunManifest {
|
||
commit: "c0ffee".to_string(),
|
||
params: vec![("fast".to_string(), Scalar::f64(2.0))],
|
||
defaults: vec![],
|
||
window: (Timestamp(1), Timestamp(2)),
|
||
seed: 0,
|
||
broker: "sim-optimal(pip_size=1)".to_string(),
|
||
selection: None,
|
||
instrument: Some("GER40".to_string()),
|
||
topology_hash: None,
|
||
project: None,
|
||
};
|
||
let taps = |a: f64, b: f64| {
|
||
vec![ColumnarTrace::from_rows(
|
||
"equity",
|
||
&[ScalarKind::F64],
|
||
&[(Timestamp(1), vec![Scalar::f64(a)]), (Timestamp(2), vec![Scalar::f64(b)])],
|
||
)]
|
||
};
|
||
|
||
// The #224 sweep fan-out layout: two members under ONE cell, so each member's
|
||
// index.json lives at depth 2 (`<name>/<cell>/<member>/index.json`) — not the
|
||
// depth-1 nominee layout (`<name>/<cell>/index.json`) the campaign path writes.
|
||
// `TraceStore::open` roots at `<runs_dir>/traces`, matching `env.trace_store()`.
|
||
let store = TraceStore::open(cwd.join("runs"));
|
||
store.write("swp/cell0/m0", &manifest(), &taps(0.0, 0.4)).expect("write member m0");
|
||
store.write("swp/cell0/m1", &manifest(), &taps(0.0, 0.7)).expect("write member m1");
|
||
|
||
// (1) chart the family by the handle the sweep prints: exit 0 + chart page.
|
||
let out = Command::new(BIN).args(["chart", "swp"]).current_dir(&cwd).output().expect("spawn chart swp");
|
||
assert_eq!(
|
||
out.status.code(),
|
||
Some(0),
|
||
"charting a sweep --trace family by its handle must exit 0; got {:?}, stderr: {}",
|
||
out.status.code(),
|
||
String::from_utf8_lossy(&out.stderr),
|
||
);
|
||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||
assert!(
|
||
stdout.contains("window.AURA_TRACES") && stdout.contains("<title>aura chart"),
|
||
"the family handle must render a chart page covering its members' taps \
|
||
(AURA_TRACES + chart title); stdout len {}",
|
||
stdout.len(),
|
||
);
|
||
|
||
// (2) the genuinely-absent-handle refusal must not suggest re-running a
|
||
// command with the handle itself as the `--trace` argument (a dead-end remedy).
|
||
let ghost = Command::new(BIN).args(["chart", "ghost"]).current_dir(&cwd).output().expect("spawn chart ghost");
|
||
assert_eq!(ghost.status.code(), Some(1), "an absent handle still exits 1: {:?}", ghost.status);
|
||
let gerr = String::from_utf8_lossy(&ghost.stderr);
|
||
assert!(
|
||
!gerr.contains("--trace ghost"),
|
||
"the not-found remedy must not tell the user to re-run with the handle as --trace \
|
||
(that command produced the data / would not create this handle); got: {gerr}",
|
||
);
|
||
|
||
let _ = std::fs::remove_dir_all(&cwd);
|
||
}
|
||
|
||
/// Property (#238): a sweep / walk-forward `--trace <NAME>` family is chartable by
|
||
/// the very NAME the user chose — not only by the content-derived handle the run
|
||
/// prints. `--trace <NAME>` lands `NAME` in the generated campaign document's
|
||
/// `name` field (content-addressed via `put_campaign`), while the trace directory
|
||
/// stays keyed by the content-derived handle `{campaign8}-{run}` that
|
||
/// `append_campaign_run` re-derives (C18, untouched). Today `aura chart <NAME>`
|
||
/// classifies `NAME` as an unknown trace handle and exits 1, so the chosen name
|
||
/// has no charting effect. It must instead resolve `NAME` against the recorded
|
||
/// campaign runs: the ONE record whose stored campaign document carries
|
||
/// `name == NAME` and a persisted `trace_name` charts that family by its handle —
|
||
/// exit 0 with the same chart page charting the handle directly renders.
|
||
///
|
||
/// The state is fabricated in-process, no archive / sweep run: a campaign
|
||
/// document carrying a distinctive `name` is stored via `put_campaign`; a
|
||
/// campaign-run record with a `Some` trace_name sentinel is appended (which
|
||
/// re-derives the handle `{campaign8}-0`); a family is written under that exact
|
||
/// handle via `TraceStore` — mirroring the handle-charted sibling above (the
|
||
/// #224 depth-2 fan-out). The registry roots where `env.registry()` does
|
||
/// (`<cwd>/runs/runs.jsonl`), so the running binary reads the same store. On
|
||
/// today's tree `chart <NAME>` hits `emit_chart`'s NotFound arm (no registry
|
||
/// query), so it exits 1 with the "no recorded run or family" message — RED for
|
||
/// the right reason (the name-resolution is absent, not a fabrication error).
|
||
#[test]
|
||
fn chart_resolves_a_trace_family_by_its_chosen_campaign_name() {
|
||
use aura_core::{Scalar, ScalarKind, Timestamp};
|
||
use aura_engine::{ColumnarTrace, RunManifest};
|
||
use aura_registry::{derive_trace_name, CampaignRunRecord, Registry, TraceStore};
|
||
|
||
let cwd = temp_cwd("chart-by-campaign-name");
|
||
|
||
// The chosen family name (`--trace <NAME>`) — distinctive, not a trace handle
|
||
// (no such directory under runs/traces, so `name_kind` classifies NotFound).
|
||
const CAMPAIGN_NAME: &str = "mom-ger40-sept24";
|
||
|
||
// (1) Store a campaign document carrying that name (a valid CampaignDoc, so a
|
||
// resolver can parse it by any strategy — Value lookup or full deserialize).
|
||
// `put_campaign` self-keys by content, returning the id the campaign-run
|
||
// record must reference. The registry roots where `env.registry()` does:
|
||
// `<cwd>/runs/runs.jsonl` (campaign docs at `<cwd>/runs/campaigns/<id>.json`,
|
||
// campaign_runs at `<cwd>/runs/campaign_runs.jsonl`).
|
||
let reg = Registry::open(cwd.join("runs").join("runs.jsonl"));
|
||
let campaign_json = r#"{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "__NAME__",
|
||
"data": { "instruments": ["GER40"],
|
||
"windows": [ { "from_ms": 1, "to_ms": 2 } ] },
|
||
"strategies": [
|
||
{ "ref": { "content_id": "9f3a" },
|
||
"axes": { "fast": { "kind": "I64", "values": [8, 12] } } }
|
||
],
|
||
"process": { "ref": { "content_id": "4e2d" } },
|
||
"seed": 1,
|
||
"presentation": { "persist_taps": ["equity"], "emit": ["family_table"] }
|
||
}"#
|
||
.replace("__NAME__", CAMPAIGN_NAME);
|
||
let campaign_id = reg.put_campaign(&campaign_json).expect("store campaign document");
|
||
|
||
// (2) Append a campaign-run record whose trace_name is Some (the executor's
|
||
// claim sentinel; `append_campaign_run` assigns the per-campaign run counter
|
||
// and re-derives the stored trace_name to `{campaign8}-{run}`).
|
||
let record = CampaignRunRecord {
|
||
campaign: campaign_id.clone(),
|
||
process: "4e2d".to_string(),
|
||
run: 0, // ignored — append assigns the counter
|
||
seed: 1,
|
||
cells: vec![],
|
||
generalizations: vec![],
|
||
trace_name: Some("claim".to_string()),
|
||
};
|
||
let run = reg.append_campaign_run(&record).expect("append campaign run");
|
||
let handle = derive_trace_name(&campaign_id, run);
|
||
|
||
// (3) Write a family under that exact handle via TraceStore — the same
|
||
// fabrication the handle-charted sibling uses (#224 depth-2: two members under
|
||
// one cell). Charting `handle` directly is thus known to render a family chart.
|
||
let manifest = || RunManifest {
|
||
commit: "c0ffee".to_string(),
|
||
params: vec![("fast".to_string(), Scalar::f64(2.0))],
|
||
defaults: vec![],
|
||
window: (Timestamp(1), Timestamp(2)),
|
||
seed: 0,
|
||
broker: "sim-optimal(pip_size=1)".to_string(),
|
||
selection: None,
|
||
instrument: Some("GER40".to_string()),
|
||
topology_hash: None,
|
||
project: None,
|
||
};
|
||
let taps = |a: f64, b: f64| {
|
||
vec![ColumnarTrace::from_rows(
|
||
"equity",
|
||
&[ScalarKind::F64],
|
||
&[(Timestamp(1), vec![Scalar::f64(a)]), (Timestamp(2), vec![Scalar::f64(b)])],
|
||
)]
|
||
};
|
||
let store = TraceStore::open(cwd.join("runs"));
|
||
store.write(&format!("{handle}/cell0/m0"), &manifest(), &taps(0.0, 0.4)).expect("write member m0");
|
||
store.write(&format!("{handle}/cell0/m1"), &manifest(), &taps(0.0, 0.7)).expect("write member m1");
|
||
|
||
// Chart by the chosen NAME (not the handle): resolution must go through the
|
||
// campaign-run query and land on `handle`, rendering the same family chart.
|
||
let out = Command::new(BIN).args(["chart", CAMPAIGN_NAME]).current_dir(&cwd).output().expect("spawn chart <name>");
|
||
assert_eq!(
|
||
out.status.code(),
|
||
Some(0),
|
||
"charting a --trace family by its chosen campaign name must exit 0; got {:?}, stderr: {}",
|
||
out.status.code(),
|
||
String::from_utf8_lossy(&out.stderr),
|
||
);
|
||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||
assert!(
|
||
stdout.contains("window.AURA_TRACES") && stdout.contains("<title>aura chart"),
|
||
"the chosen name must render the same family chart as its handle \
|
||
(AURA_TRACES + chart title); stdout len {}",
|
||
stdout.len(),
|
||
);
|
||
|
||
let _ = std::fs::remove_dir_all(&cwd);
|
||
}
|
||
|
||
/// Property (#238 ambiguity): when TWO recorded campaign runs' stored documents
|
||
/// both carry the SAME chosen `name`, `chart <NAME>` must refuse (exit 1) rather
|
||
/// than silently pick one — the resolver names both candidate trace-store
|
||
/// handles in the refusal (deterministic file/append order), so the user can
|
||
/// re-run `chart` against the specific handle they mean. Mirrors the fixture of
|
||
/// [`chart_resolves_a_trace_family_by_its_chosen_campaign_name`], duplicated
|
||
/// once with a second `put_campaign` + `append_campaign_run` under the same
|
||
/// `name` (a distinct campaign document — the `data.windows` bound differs —
|
||
/// still resolves to the same `name`, exactly the "two separate --trace <NAME>
|
||
/// invocations reusing a name" scenario this guards against).
|
||
#[test]
|
||
fn chart_by_campaign_name_refuses_when_the_name_is_ambiguous() {
|
||
use aura_core::{Scalar, ScalarKind, Timestamp};
|
||
use aura_engine::{ColumnarTrace, RunManifest};
|
||
use aura_registry::{derive_trace_name, CampaignRunRecord, Registry, TraceStore};
|
||
|
||
let cwd = temp_cwd("chart-by-name-ambiguous");
|
||
const CAMPAIGN_NAME: &str = "reused-name";
|
||
|
||
let reg = Registry::open(cwd.join("runs").join("runs.jsonl"));
|
||
let campaign_json_of = |to_ms: i64| {
|
||
format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "{CAMPAIGN_NAME}",
|
||
"data": {{ "instruments": ["GER40"],
|
||
"windows": [ {{ "from_ms": 1, "to_ms": {to_ms} }} ] }},
|
||
"strategies": [
|
||
{{ "ref": {{ "content_id": "9f3a" }},
|
||
"axes": {{ "fast": {{ "kind": "I64", "values": [8, 12] }} }} }}
|
||
],
|
||
"process": {{ "ref": {{ "content_id": "4e2d" }} }},
|
||
"seed": 1,
|
||
"presentation": {{ "persist_taps": ["equity"], "emit": ["family_table"] }}
|
||
}}"#
|
||
)
|
||
};
|
||
|
||
let manifest = || RunManifest {
|
||
commit: "c0ffee".to_string(),
|
||
params: vec![("fast".to_string(), Scalar::f64(2.0))],
|
||
defaults: vec![],
|
||
window: (Timestamp(1), Timestamp(2)),
|
||
seed: 0,
|
||
broker: "sim-optimal(pip_size=1)".to_string(),
|
||
selection: None,
|
||
instrument: Some("GER40".to_string()),
|
||
topology_hash: None,
|
||
project: None,
|
||
};
|
||
let taps = |a: f64, b: f64| {
|
||
vec![ColumnarTrace::from_rows(
|
||
"equity",
|
||
&[ScalarKind::F64],
|
||
&[(Timestamp(1), vec![Scalar::f64(a)]), (Timestamp(2), vec![Scalar::f64(b)])],
|
||
)]
|
||
};
|
||
let store = TraceStore::open(cwd.join("runs"));
|
||
|
||
let mut handles = Vec::new();
|
||
for to_ms in [2, 3] {
|
||
let campaign_id = reg.put_campaign(&campaign_json_of(to_ms)).expect("store campaign document");
|
||
let record = CampaignRunRecord {
|
||
campaign: campaign_id.clone(),
|
||
process: "4e2d".to_string(),
|
||
run: 0,
|
||
seed: 1,
|
||
cells: vec![],
|
||
generalizations: vec![],
|
||
trace_name: Some("claim".to_string()),
|
||
};
|
||
let run = reg.append_campaign_run(&record).expect("append campaign run");
|
||
let handle = derive_trace_name(&campaign_id, run);
|
||
store.write(&format!("{handle}/cell0/m0"), &manifest(), &taps(0.0, 0.4)).expect("write member m0");
|
||
store.write(&format!("{handle}/cell0/m1"), &manifest(), &taps(0.0, 0.7)).expect("write member m1");
|
||
handles.push(handle);
|
||
}
|
||
|
||
let out = Command::new(BIN).args(["chart", CAMPAIGN_NAME]).current_dir(&cwd).output().expect("spawn chart <name>");
|
||
assert_eq!(
|
||
out.status.code(),
|
||
Some(1),
|
||
"an ambiguous campaign name must refuse (exit 1), not silently pick one; got {:?}, stdout: {}",
|
||
out.status.code(),
|
||
String::from_utf8_lossy(&out.stdout),
|
||
);
|
||
assert!(out.stdout.is_empty(), "the refusal must not leak a chart page to stdout");
|
||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||
for handle in &handles {
|
||
assert!(
|
||
stderr.contains(handle.as_str()),
|
||
"the refusal must list every candidate handle deterministically; got: {stderr}, want handle: {handle}"
|
||
);
|
||
}
|
||
|
||
let _ = std::fs::remove_dir_all(&cwd);
|
||
}
|
||
|
||
#[test]
|
||
fn no_args_prints_usage_and_exits_two() {
|
||
let out = Command::new(BIN).output().expect("spawn aura");
|
||
assert_eq!(out.status.code(), Some(2), "exit status: {:?}", out.status);
|
||
assert!(out.stdout.is_empty(), "stdout should be empty on the usage path");
|
||
let stderr = String::from_utf8(out.stderr).expect("utf-8 stderr");
|
||
assert!(stderr.contains("Usage"), "stderr was: {stderr:?}");
|
||
}
|
||
|
||
|
||
|
||
/// Property: `aura graph` emits a single self-contained HTML page — the
|
||
/// interactive pin-graph viewer with the deterministic model inlined and the
|
||
/// vendored Graphviz-WASM + pan-zoom inlined (no remote fetch). Driven through
|
||
/// the built binary so it pins the observable CLI contract. The DOT/SVG are
|
||
/// Graphviz-version-dependent and not asserted (the prototype is the by-hand
|
||
/// visual reference); this pins the page envelope + the retirement of the old
|
||
/// ascii-dag notation.
|
||
#[test]
|
||
fn graph_emits_self_contained_html_viewer() {
|
||
let out = Command::new(BIN).arg("graph").output().expect("spawn aura graph");
|
||
assert_eq!(out.status.code(), Some(0), "`aura graph` exit: {:?}", out.status);
|
||
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||
// a self-contained HTML document...
|
||
assert!(
|
||
stdout.trim_start().starts_with("<!doctype html>"),
|
||
"not an HTML doc: {:?}",
|
||
&stdout[..stdout.len().min(80)]
|
||
);
|
||
// ...with the deterministic model inlined as the viewer's data source...
|
||
assert!(stdout.contains("window.AURA_MODEL = {\"root\":"), "model not inlined: {stdout:?}");
|
||
// ...the Graphviz-WASM bootstrap present, and no remote asset fetch.
|
||
assert!(stdout.contains("Viz.instance"), "viewer bootstrap missing");
|
||
assert!(!stdout.contains("<script src"), "assets must be inlined, found remote <script src>");
|
||
// the retired ascii-dag invented notation is gone.
|
||
assert!(!stdout.contains("Sub(#Sf,#Ss)"), "retired ascii-dag notation must be gone: {stdout}");
|
||
}
|
||
|
||
/// Property (#28): `aura graph <blueprint.json>` renders the CONSUMER's own
|
||
/// graph — the structure of the file it is handed — not the embedded r_sma
|
||
/// sample. Mirrors the `dispatch_run` dual-grammar: a first-positional naming
|
||
/// an existing `.json` selects the loaded-blueprint branch, which reads the
|
||
/// file, reloads it via `blueprint_from_json`, and `render_html`s it — so the
|
||
/// emitted page's inlined `window.AURA_MODEL` reflects THAT file's nodes.
|
||
/// Handed the retired r-breakout example (std-vocabulary only, so no project is
|
||
/// needed), the page must carry a marker unique to that graph — `channel_hi`, a
|
||
/// RollingMax node name absent from the sample and from every static viewer
|
||
/// asset — and must NOT carry the sample-only `"type":"Bias"` node, i.e. it did
|
||
/// not silently fall back to the embedded r_sma sample. Driven through the built
|
||
/// binary so it pins the observable CLI contract.
|
||
#[test]
|
||
fn graph_renders_a_consumer_blueprint_file_not_the_embedded_sample() {
|
||
let cwd = temp_cwd("graph-consumer-blueprint");
|
||
// A committed consumer blueprint that is NOT the embedded sample and resolves
|
||
// against the base std vocabulary alone (RollingMax/RollingMin/Delay/Gt/Latch/
|
||
// Sub) — no project fixture required. Absolute path, so the temp cwd is free of
|
||
// any Aura.toml (guarantees std-env resolution).
|
||
let blueprint = format!("{}/examples/r_breakout.json", env!("CARGO_MANIFEST_DIR"));
|
||
let out = Command::new(BIN)
|
||
.arg("graph")
|
||
.arg(&blueprint)
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura graph <blueprint>");
|
||
assert_eq!(
|
||
out.status.code(),
|
||
Some(0),
|
||
"`aura graph <blueprint.json>` exit: {:?}\nstderr: {}",
|
||
out.status,
|
||
String::from_utf8_lossy(&out.stderr)
|
||
);
|
||
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||
// the page inlines a model as the viewer's data source...
|
||
assert!(
|
||
stdout.contains("window.AURA_MODEL = {\"root\":"),
|
||
"model not inlined into the page"
|
||
);
|
||
// ...and that model is the CONSUMER graph's — its own node `channel_hi`
|
||
// (unique to r-breakout, absent from the r_sma sample and from every static
|
||
// asset) is present.
|
||
assert!(
|
||
stdout.contains("channel_hi"),
|
||
"the consumer blueprint's own node (channel_hi) is missing — its graph was not rendered"
|
||
);
|
||
// ...NOT the embedded r_sma sample: the sample's Bias node type must be absent
|
||
// (proof the file was rendered, not silently ignored in favour of the sample).
|
||
assert!(
|
||
!stdout.contains("\"type\":\"Bias\""),
|
||
"rendered the embedded r_sma sample (its Bias node is present) instead of the consumer blueprint"
|
||
);
|
||
}
|
||
|
||
/// Property (#28 close-audit): a NAMED-but-unreadable blueprint arg to
|
||
/// `aura graph` is a usage fault, not a silent fallback to the embedded sample.
|
||
/// `dispatch_graph`'s no-subcommand arm branches on `is_blueprint_file`, which
|
||
/// returns `None` for BOTH "no positional given" (→ legitimate sample default)
|
||
/// and "a positional was given but it is not a readable `.json` file" (→ a bad
|
||
/// arg). The two must not be conflated: a bad arg (typo, nonexistent path, wrong
|
||
/// extension) must `eprintln!` a usage error and exit 2, mirroring `dispatch_run`
|
||
/// — NOT render the r_sma sample and exit 0, which re-introduces the exact #28
|
||
/// friction ("I asked for my graph, got the sample") in the error path. The
|
||
/// bare-`aura graph` default (no positional → sample, exit 0) is guarded in the
|
||
/// same test so the fix cannot regress it.
|
||
#[test]
|
||
fn graph_with_bad_blueprint_arg_is_a_usage_fault_not_a_silent_sample() {
|
||
let cwd = temp_cwd("graph-bad-arg");
|
||
// A named .json path that does not exist: `is_blueprint_file` returns None,
|
||
// and the buggy arm falls through to the embedded sample instead of refusing.
|
||
let out = Command::new(BIN)
|
||
.arg("graph")
|
||
.arg("does-not-exist.json")
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura graph <bad-arg>");
|
||
// Headline pin: a named-but-unreadable blueprint arg exits 2 (usage fault),
|
||
// mirroring `dispatch_run`'s bad-blueprint branch — not 0.
|
||
assert_eq!(
|
||
out.status.code(),
|
||
Some(2),
|
||
"`aura graph <nonexistent.json>` must be a usage fault (exit 2), not a silent sample fallback; got {:?}\nstderr: {}",
|
||
out.status,
|
||
String::from_utf8_lossy(&out.stderr),
|
||
);
|
||
// ...and it must NOT emit the embedded r_sma sample: the sample's inlined
|
||
// model + its Bias node must be absent from stdout.
|
||
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||
assert!(
|
||
!stdout.contains("\"type\":\"Bias\""),
|
||
"a bad blueprint arg silently rendered the embedded r_sma sample (its Bias node is present) instead of refusing"
|
||
);
|
||
|
||
// Guard the legitimate default: bare `aura graph` (no positional) still
|
||
// renders the embedded sample and exits 0.
|
||
let default = Command::new(BIN)
|
||
.arg("graph")
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura graph");
|
||
assert_eq!(
|
||
default.status.code(),
|
||
Some(0),
|
||
"bare `aura graph` must still render the sample (exit 0); got {:?}",
|
||
default.status,
|
||
);
|
||
let default_out = String::from_utf8(default.stdout).expect("utf-8 stdout");
|
||
assert!(
|
||
default_out.contains("\"type\":\"Bias\""),
|
||
"bare `aura graph` must render the embedded r_sma sample (its Bias node), the no-positional default"
|
||
);
|
||
}
|
||
|
||
|
||
#[test]
|
||
fn runs_families_list_and_per_family_rank_across_invocations() {
|
||
// Ported onto a one-cell campaign document run twice (#319 Task 8): two
|
||
// `exec`s of the same sweep-only document accumulate two families over
|
||
// time, each a related set of 4 records (C18/C21 lineage), in the sibling
|
||
// family store. Unlike the retired verb sugar (whose family_id was the
|
||
// literal `--name`/`"sweep"` default), the campaign path's own family
|
||
// naming is a deterministic, content-derived coordinate name
|
||
// (`{campaign8}-{strategy}-{instrument}-{regime}-w{window}-r{run}-s{stage}`,
|
||
// `aura-campaign::exec`) — so the two family ids are read back from `runs
|
||
// families` rather than assumed to be "sweep-0"/"sweep-1"; the STRUCTURAL
|
||
// property (two families, 4 members each, per-family rank works) survives
|
||
// unchanged.
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let bp_id = seed_blueprint(&cwd, "runs-flow-seed");
|
||
let proc_id = register_process_doc(&cwd, "runsflow.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc(
|
||
"runsflow", "SYMA", &bp_id, &proc_id,
|
||
(1709251200000, 1719791999999), "2, 4", "8, 16",
|
||
);
|
||
write_doc(&cwd, "runsflow.campaign.json", &doc);
|
||
for _ in 0..2 {
|
||
let (out, code) = run_code_in(&cwd, &["exec", "runsflow.campaign.json"]);
|
||
assert_eq!(code, Some(0), "exec exit: {out}");
|
||
}
|
||
|
||
// `runs families` shows both stored families in first-seen order, each with
|
||
// its kind + member count.
|
||
let (fams_out, fams_code) = run_code_in(&cwd, &["runs", "families"]);
|
||
assert_eq!(fams_code, Some(0), "families exit: {fams_out}");
|
||
let fam_lines: Vec<&str> = fams_out.lines().collect();
|
||
assert_eq!(fam_lines.len(), 2, "families: {fams_out:?}");
|
||
assert!(fam_lines.iter().all(|l| l.contains("\"kind\":\"Sweep\"")), "families: {fams_out:?}");
|
||
assert!(fam_lines.iter().all(|l| l.contains("\"members\":4")), "members: {fams_out:?}");
|
||
let family_id_of = |line: &str| -> String {
|
||
let key = "\"family_id\":\"";
|
||
let start = line.find(key).expect("line has family_id") + key.len();
|
||
let tail = &line[start..];
|
||
let end = tail.find('"').expect("closing quote");
|
||
tail[..end].to_string()
|
||
};
|
||
let (fam0, fam1) = (family_id_of(fam_lines[0]), family_id_of(fam_lines[1]));
|
||
assert_ne!(fam0, fam1, "two distinct runs mint two distinct families: {fams_out:?}");
|
||
assert!(fam0.ends_with("-0"), "first run's family carries the run-0 suffix: {fam0}");
|
||
assert!(fam1.ends_with("-1"), "second run's family carries the run-1 suffix: {fam1}");
|
||
|
||
// `runs family <fam0> rank total_pips`: the family's 4 members, highest
|
||
// total_pips first.
|
||
let (rank_out, rank_code) = run_code_in(&cwd, &["runs", "family", &fam0, "rank", "total_pips"]);
|
||
assert_eq!(rank_code, Some(0), "rank exit: {rank_out}");
|
||
assert_eq!(rank_out.lines().count(), 4, "rank: {rank_out:?}");
|
||
assert!(rank_out.lines().all(|l| l.starts_with("{\"manifest\":{")), "rank shape: {rank_out:?}");
|
||
let pips: Vec<f64> = rank_out
|
||
.lines()
|
||
.map(|l| {
|
||
let key = "\"total_pips\":";
|
||
let start = l.find(key).expect("line has total_pips") + key.len();
|
||
let tail = &l[start..];
|
||
let end = tail.find([',', '}']).expect("token end");
|
||
tail[..end].parse().expect("total_pips is an f64")
|
||
})
|
||
.collect();
|
||
assert!(pips.windows(2).all(|w| w[0] >= w[1]), "rank not descending: {pips:?}");
|
||
|
||
// an unknown metric is a usage error
|
||
let (bogus_out, bogus_code) = run_code_in(&cwd, &["runs", "family", &fam0, "rank", "bogus"]);
|
||
assert_eq!(bogus_code, Some(2), "bogus exit: {bogus_out}");
|
||
|
||
// an unknown family id is an empty family: zero lines, exit 0.
|
||
let (unknown_out, unknown_code) = run_code_in(&cwd, &["runs", "family", "nope-9"]);
|
||
assert_eq!(unknown_code, Some(0), "unknown family exit: {unknown_out}");
|
||
assert_eq!(unknown_out.len(), 0, "unknown family stdout: {unknown_out:?}");
|
||
}
|
||
|
||
/// #147: the pre-rename metric name `exposure_sign_flips` still resolves
|
||
/// through the real, user-facing `aura runs family <id> rank <metric>` path —
|
||
/// not just through a direct `MetricVocabulary::resolve` call in a unit test.
|
||
/// Task 4 re-exports `aura_campaign::RANKABLE_METRICS` from the single-sourced
|
||
/// backtest vocabulary instead of hand-copying it; were the vocabulary's
|
||
/// `resolve()` match arm for the legacy alias ever dropped in that refactor,
|
||
/// this is the only test that would catch it at the CLI's actual observable
|
||
/// boundary (exit code + stdout), rather than inside the crate that owns the
|
||
/// vocabulary.
|
||
#[test]
|
||
fn runs_family_rank_accepts_legacy_exposure_sign_flips_alias() {
|
||
// Ported onto a one-cell campaign document (#319 Task 8) — the family_id
|
||
// is read back from `runs families` (see the sibling test's doc comment
|
||
// for why the campaign path's own naming isn't the literal "sweep-0").
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let bp_id = seed_blueprint(&cwd, "legacy-alias-seed");
|
||
let proc_id = register_process_doc(&cwd, "legacyalias.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc(
|
||
"legacyalias", "SYMA", &bp_id, &proc_id,
|
||
(1709251200000, 1719791999999), "2, 4", "8, 16",
|
||
);
|
||
write_doc(&cwd, "legacyalias.campaign.json", &doc);
|
||
let (exec_out, exec_code) = run_code_in(&cwd, &["exec", "legacyalias.campaign.json"]);
|
||
assert_eq!(exec_code, Some(0), "exec exit: {exec_out}");
|
||
let family_id = exec_out
|
||
.lines()
|
||
.find(|l| l.starts_with("{\"family_id\":"))
|
||
.map(|l| {
|
||
serde_json::from_str::<serde_json::Value>(l).expect("member line parses")["family_id"]
|
||
.as_str()
|
||
.expect("family_id is a string")
|
||
.to_string()
|
||
})
|
||
.expect("a member line carries the family id");
|
||
|
||
let (rank_out, rank_code) = run_code_in(&cwd, &["runs", "family", &family_id, "rank", "exposure_sign_flips"]);
|
||
assert_eq!(
|
||
rank_code, Some(0),
|
||
"the legacy alias must still resolve, not usage-error: {rank_out}"
|
||
);
|
||
assert_eq!(rank_out.lines().count(), 4, "all 4 members rank: {rank_out:?}");
|
||
// the renamed field is what actually ships in the JSON — the alias is a
|
||
// resolve()-only back door, never a second output key.
|
||
assert!(
|
||
rank_out.lines().all(|l| l.contains("\"bias_sign_flips\":")),
|
||
"output still serializes under the new key: {rank_out:?}"
|
||
);
|
||
assert!(
|
||
!rank_out.contains("\"exposure_sign_flips\":"),
|
||
"the legacy key must never appear in output: {rank_out:?}"
|
||
);
|
||
|
||
let flips: Vec<i64> = rank_out
|
||
.lines()
|
||
.map(|l| {
|
||
let key = "\"bias_sign_flips\":";
|
||
let start = l.find(key).expect("line has bias_sign_flips") + key.len();
|
||
let tail = &l[start..];
|
||
let end = tail.find([',', '}']).expect("token end");
|
||
tail[..end].parse().expect("bias_sign_flips is an integer")
|
||
})
|
||
.collect();
|
||
// bias_sign_flips is a lower-is-better metric (fewer direction reversals),
|
||
// so `rank` orders ascending here — unlike total_pips's descending order.
|
||
assert!(flips.windows(2).all(|w| w[0] <= w[1]), "rank not ascending by the aliased metric: {flips:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn runs_list_and_rank_are_retired_and_exit_two() {
|
||
// `aura runs list` / `runs rank <metric>` were retired (#73): families
|
||
// (sweep/mc/walkforward, C21) subsume standalone over-time comparison. The
|
||
// dropped subcommands now fall through to the strict-arg usage error (exit 2),
|
||
// before any registry access.
|
||
let list = Command::new(BIN).args(["runs", "list"]).output().expect("spawn list");
|
||
assert_eq!(list.status.code(), Some(2), "retired `runs list` must exit 2: {:?}", list.status);
|
||
let rank = Command::new(BIN).args(["runs", "rank", "total_pips"]).output().expect("spawn rank");
|
||
assert_eq!(rank.status.code(), Some(2), "retired `runs rank` must exit 2: {:?}", rank.status);
|
||
}
|
||
|
||
#[test]
|
||
fn runs_bare_subcommand_is_strict_and_exits_two() {
|
||
// `aura runs` with no/unknown sub-subcommand is a usage error (#16 strict).
|
||
let out = Command::new(BIN).arg("runs").output().expect("spawn aura runs");
|
||
assert_eq!(out.status.code(), Some(2), "exit status: {:?}", out.status);
|
||
}
|
||
|
||
// Note: the family-trace-persistence tests deleted above (mc/sweep/walkforward
|
||
// per-member trace dirs, the momentum bool-param dir/gate proofs, the sweep
|
||
// determinism pin) are the Step 3 PIP-surface deletions — same underlying
|
||
// cause as the collateral note above (no blueprint-mode arm persists
|
||
// per-member traces).
|
||
|
||
// --- Real-data family path (#106, plan 0060 Task 5) -------------------------
|
||
//
|
||
// These drive `aura sweep|walkforward --real <SYMBOL>` end to end over the local
|
||
// data-server archive. The gated tests need the Pepperstone M1 archive present;
|
||
// they skip (print + pass) on a data-less machine, the project's
|
||
// skip-on-no-data convention. The pip-refusal test is NOT gated: a symbol with no
|
||
// recorded geometry is rejected before any data access.
|
||
|
||
// EURUSD 2024-06 (UTC inclusive ms): a bounded recorded-sidecar real window.
|
||
const EURUSD_JUN2024_FROM_MS: i64 = 1_717_200_000_000;
|
||
const EURUSD_JUN2024_TO_MS: i64 = 1_719_791_999_999;
|
||
|
||
fn local_data_present() -> bool {
|
||
std::path::Path::new(data_server::DEFAULT_DATA_PATH).is_dir()
|
||
}
|
||
|
||
/// Property: the per-instrument pip refusal fires for `aura sweep --real
|
||
/// <no-geometry symbol>` exactly as it does for `aura run --real` — a symbol with no
|
||
/// recorded geometry (`NONEXISTENT`, no sidecar on any host) is rejected with exit 1
|
||
/// and the "no recorded geometry" message BEFORE any bar-data access. NOT gated: the
|
||
/// refusal precedes the archive entirely, so it is CI-safe and runs on every machine.
|
||
/// (#159 cut 4: `sweep` is blueprint-only now, so the fixture supplies a valid
|
||
/// blueprint + axis to clear the usage grammar before the geometry refusal fires.)
|
||
#[test]
|
||
fn sweep_real_no_geometry_symbol_refuses_with_exit_1() {
|
||
// not gated: the pip refusal happens before any data access. Ported onto
|
||
// a one-cell campaign document — a #272 contained fault (exit 3), not the
|
||
// retired verb's hard exit 1 (see the `run_real_no_geometry_*` siblings'
|
||
// doc comments for the rationale).
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "sweep-geo-seed");
|
||
let proc_id = register_process_doc(&dir, "sweepgeo.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc("sweepgeo", "NONEXISTENT", &bp_id, &proc_id, (1, 2), "2, 4", "8");
|
||
write_doc(&dir, "sweepgeo.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "sweepgeo.campaign.json"]);
|
||
assert_eq!(code, Some(3), "a contained no-geometry cell fault is exit 3: {out}");
|
||
assert!(out.contains("no recorded geometry"), "out: {out}");
|
||
}
|
||
|
||
// GER40 Sept-2024 window (inclusive Unix-ms) — the same gated calendar month
|
||
// used by `run_real_sidecar_index_pip_reaches_the_emitted_manifest` and the
|
||
// campaign-path e2e in `research_docs.rs`.
|
||
const GER40_SEPT2024_FROM_MS: &str = "1725148800000";
|
||
const GER40_SEPT2024_TO_MS: &str = "1727740799999";
|
||
|
||
/// Property (#242): a `--real` window that holds no bars for a symbol that IS in
|
||
/// the archive refuses with a WINDOW-aware message — distinct from the genuinely
|
||
/// unknown-symbol refusal. GER40 is present (the covered Sept-2024 window runs),
|
||
/// so an *inverted* window (`--from` > `--to`) is empty by construction for any
|
||
/// archive — no bar can satisfy `from <= t <= to` — yet the symbol, its geometry,
|
||
/// and the month's file all exist. The refusal must name the requested window
|
||
/// rather than reuse the symbol-absence message "no local data for symbol", which
|
||
/// misattributes an empty WINDOW to a missing SYMBOL. Gated on the local archive;
|
||
/// skips cleanly when absent (the project's skip-on-no-data convention), so CI
|
||
/// without the archive stays green.
|
||
#[test]
|
||
fn run_real_empty_window_refusal_names_the_window_not_the_symbol() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
// Ported onto a one-cell campaign document (#319 Task 8): a campaign
|
||
// document's window is INTRINSICALLY validated `from_ms < to_ms` (a
|
||
// hand-authored inverted window refuses at document validation, before
|
||
// ever reaching the data layer — a stricter, earlier gate than the
|
||
// retired verb had), so the empty-window property is reached instead with
|
||
// a validly-ordered window that simply predates any archive coverage
|
||
// (epoch-ms [1, 2], the same "no data yet" window the geometry-refusal
|
||
// siblings use for an unknown symbol) — GER40 IS present (has recorded
|
||
// geometry), so this reaches the DATA layer's own "no data in window"
|
||
// fault, distinct from the geometry-refusal path above. A #272 contained
|
||
// fault (exit 3), not the retired verb's hard exit 1.
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "emptywin-seed");
|
||
let proc_id = register_process_doc(&dir, "emptywin.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc("emptywin", "GER40", &bp_id, &proc_id, (1, 2), "2", "8");
|
||
write_doc(&dir, "emptywin.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "emptywin.campaign.json"]);
|
||
assert_eq!(code, Some(3), "a contained no-data-in-window cell fault is exit 3: {out}");
|
||
// The distinction (#242): the empty-window refusal NAMES the window and does
|
||
// NOT reuse the wholesale symbol-absence message for a symbol that is present.
|
||
assert!(
|
||
out.to_lowercase().contains("window"),
|
||
"empty-window refusal must name the requested window (GER40 IS present), got: {out}"
|
||
);
|
||
assert!(
|
||
!out.contains("no local data for symbol"),
|
||
"empty-window refusal must not reuse the symbol-absence message, got: {out}"
|
||
);
|
||
}
|
||
|
||
/// Characterization pin (grounding-check ratification for the risk-regime-axis
|
||
/// spec): the dissolved real-data sweep member manifest stamps its resolved stop
|
||
/// (`stop_length`/`stop_k`) — the default regime here. The R-path stop knobs are
|
||
/// bound via `run_blueprint_member`'s `stop` param and stamped into the manifest
|
||
/// alongside the swept signal axes; the sibling
|
||
/// `sweep_real_blueprint_member_lines_pin_the_dissolved_contract` locates each
|
||
/// binding with `find` and so never pins the exact resolved VALUES this test
|
||
/// checks. Gated on the local GER40 archive; skips cleanly when absent.
|
||
#[test]
|
||
fn sweep_real_blueprint_member_manifest_stamps_the_default_stop() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "nostop-seed");
|
||
let proc_id = register_process_doc(&dir, "nostop.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc(
|
||
"nostop", "GER40", &bp_id, &proc_id,
|
||
(GER40_SEPT2024_FROM_MS.parse().unwrap(), GER40_SEPT2024_TO_MS.parse().unwrap()),
|
||
"2, 4", "8",
|
||
);
|
||
write_doc(&dir, "nostop.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "nostop.campaign.json"]);
|
||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||
let lines: Vec<serde_json::Value> = out
|
||
.lines()
|
||
.filter(|l| l.starts_with("{\"family_id\":"))
|
||
.map(|l| serde_json::from_str(l).expect("member line parses as JSON"))
|
||
.collect();
|
||
assert_eq!(lines.len(), 2, "one member line per grid point: {out}");
|
||
|
||
for v in &lines {
|
||
let params = v["report"]["manifest"]["params"]
|
||
.as_array()
|
||
.expect("manifest.params is an array");
|
||
let get = |name: &str| params.iter().find(|p| p[0].as_str() == Some(name));
|
||
assert_eq!(get("stop_length").and_then(|p| p[1]["I64"].as_i64()), Some(3),
|
||
"the resolved default stop length is stamped: {v}");
|
||
assert_eq!(get("stop_k").and_then(|p| p[1]["F64"].as_f64()), Some(2.0),
|
||
"the resolved default stop k is stamped: {v}");
|
||
}
|
||
}
|
||
|
||
/// Property (#218 migration follow-up): running a dissolved `sweep` inside a
|
||
/// project (`fresh_project()`, the fixture this migration introduced) stamps
|
||
/// `report.manifest.project` on every member with the project's audit-trail
|
||
/// provenance (C18/C16) — the `demo` namespace and a well-formed 64-hex-char
|
||
/// dylib sha256 — rather than the `None` a bare-cwd run (no `Aura.toml`)
|
||
/// produces. Before this migration no cli_run.rs sweep test ran inside a real
|
||
/// project, so this stamping path was never exercised end-to-end. Gated on the
|
||
/// local GER40 archive; skips cleanly when absent.
|
||
#[test]
|
||
fn sweep_real_blueprint_member_manifest_stamps_project_provenance() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "provenance-seed");
|
||
let proc_id = register_process_doc(&dir, "provenance.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc(
|
||
"provenance", "GER40", &bp_id, &proc_id,
|
||
(GER40_SEPT2024_FROM_MS.parse().unwrap(), GER40_SEPT2024_TO_MS.parse().unwrap()),
|
||
"2, 4", "8",
|
||
);
|
||
write_doc(&dir, "provenance.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "provenance.campaign.json"]);
|
||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||
let lines: Vec<serde_json::Value> = out
|
||
.lines()
|
||
.filter(|l| l.starts_with("{\"family_id\":"))
|
||
.map(|l| serde_json::from_str(l).expect("member line parses as JSON"))
|
||
.collect();
|
||
assert_eq!(lines.len(), 2, "one member line per grid point: {out}");
|
||
|
||
for v in &lines {
|
||
let project = &v["report"]["manifest"]["project"];
|
||
assert_eq!(
|
||
project["namespace"].as_str(), Some("demo"),
|
||
"member manifest carries the fixture project's namespace: {v}"
|
||
);
|
||
let sha = project["dylib_sha256"].as_str().unwrap_or_else(|| panic!("dylib_sha256 present: {v}"));
|
||
assert_eq!(sha.len(), 64, "dylib_sha256 is a full sha256 hex digest: {v}");
|
||
assert!(sha.chars().all(|c| c.is_ascii_hexdigit()), "dylib_sha256 is hex: {v}");
|
||
}
|
||
}
|
||
|
||
/// Acceptance (harness-input-binding spec, #231): an OHLC-consuming strategy
|
||
/// blueprint runs end to end on real archive data through `aura run` — the
|
||
/// role names bind columns, three real sources open in canonical order, the
|
||
/// close column feeds broker/executor, and the report carries the R summary.
|
||
/// Gated on the local GER40 archive; skips cleanly when absent.
|
||
#[test]
|
||
fn run_real_ohlc_channel_example_end_to_end() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
let (dir, _g) = fresh_project();
|
||
let fixture = format!("{}/examples/r_channel.json", env!("CARGO_MANIFEST_DIR"));
|
||
let bp_id = seed_blueprint_file(&dir, &fixture, "channel-real-seed");
|
||
let proc_id = register_process_doc(&dir, "channelreal.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
// r_channel.json binds `channel_hi.length`/`channel_lo.length`/lags, not
|
||
// `fast.length`/`slow.length` (`r_sma.json`'s knobs) — `sweep_campaign_doc`'s
|
||
// shape doesn't fit, so build the document directly, one degenerate
|
||
// single-value axis (#246 bound-override reopening) over the fixture's
|
||
// own `channel_hi.length` at its bound default (a campaign document needs
|
||
// >= 1 axis per strategy).
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "channel-real",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "channel_hi.length": {{ "kind": "I64", "values": [3] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
from = GER40_SEPT2024_FROM_MS.parse::<i64>().unwrap(),
|
||
to = GER40_SEPT2024_TO_MS.parse::<i64>().unwrap(),
|
||
);
|
||
write_doc(&dir, "channelreal.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "channelreal.campaign.json"]);
|
||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||
let line = out
|
||
.lines()
|
||
.find(|l| l.starts_with("{\"family_id\":"))
|
||
.expect("the one member line");
|
||
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
|
||
assert!(
|
||
v["report"]["manifest"]["topology_hash"].as_str().is_some(),
|
||
"report carries the signal hash: {line}"
|
||
);
|
||
assert!(v["report"]["metrics"]["r"].is_object(), "the R summary is computed: {line}");
|
||
}
|
||
|
||
/// Property (#275, hostless): `run_signal_r`'s migrated production call site
|
||
/// (`h.run_bound(key_supply(&binding, sources))`, replacing the old positional
|
||
/// `h.run(sources)`) pairs each opened archive column with its declared role
|
||
/// **by name**, not by list position, for a genuinely multi-role strategy
|
||
/// (r_channel declares three distinct roles — high, low, close). Unlike
|
||
/// `run_real_ohlc_channel_example_end_to_end` above (gated on a local GER40
|
||
/// mount, which this sandbox does not have), this drives the same fixture over
|
||
/// `fresh_project_with_data`'s hermetic synthetic archive, so the by-name-binding
|
||
/// regression guard for this call site actually runs on every host, not just a
|
||
/// data-ful one. The exact R summary is a characterization pin against the
|
||
/// synthetic archive's deterministic price curve: a column swap (e.g. high/low
|
||
/// crossed) would change the channel breakout logic and move this number.
|
||
#[test]
|
||
fn run_real_ohlc_channel_example_end_to_end_hostless() {
|
||
let (dir, _g) = fresh_project_with_data();
|
||
let fixture = format!("{}/examples/r_channel.json", env!("CARGO_MANIFEST_DIR"));
|
||
let bp_id = seed_blueprint_file(&dir, &fixture, "channel-hostless-seed");
|
||
let proc_id = register_process_doc(&dir, "channelhostless.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "channel-hostless",
|
||
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": 1707120000000, "to_ms": 1707325200000 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "channel_hi.length": {{ "kind": "I64", "values": [3] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&dir, "channelhostless.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "channelhostless.campaign.json"]);
|
||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||
let line = out.lines().find(|l| l.starts_with("{\"family_id\":")).expect("the one member line");
|
||
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
|
||
let report = &v["report"];
|
||
assert!(
|
||
report["manifest"]["topology_hash"].as_str().is_some(),
|
||
"report carries the signal hash: {line}"
|
||
);
|
||
assert_eq!(
|
||
report["metrics"]["r"]["n_trades"], serde_json::json!(1),
|
||
"characterization pin over the deterministic synthetic archive \
|
||
(a high/low/close column mispairing would move this count): {line}"
|
||
);
|
||
assert_eq!(
|
||
report["metrics"]["r"]["expectancy_r"], serde_json::json!(-0.7466116470582123),
|
||
"characterization pin over the deterministic synthetic archive \
|
||
(a high/low/close column mispairing would move this value): {line}"
|
||
);
|
||
}
|
||
|
||
|
||
/// The dissolved real-data sweep + reproduce over the OHLC example (#231
|
||
/// acceptance), ported onto a one-cell campaign document (#319 Task 8 —
|
||
/// `--list-axes` itself is dead flag surface, retired with `sweep`; its
|
||
/// twin properties live on in the `aura_sweep_list_axes_*` retire bucket):
|
||
/// `exec` runs one member per grid point over three real columns, over the
|
||
/// ganged `channel_length` axis (its RAW name, #328), and every member
|
||
/// re-derives bit-identically from the store (#229 real-data reproduce, now
|
||
/// multi-column). Gated on the local GER40 archive.
|
||
#[test]
|
||
fn sweep_real_ohlc_channel_members_run_and_reproduce() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
let (dir, _g) = fresh_project();
|
||
let fixture = format!("{}/tests/fixtures/r_channel_open.json", env!("CARGO_MANIFEST_DIR"));
|
||
let bp_id = seed_blueprint_file(&dir, &fixture, "chan-seed");
|
||
let proc_id = register_process_doc(&dir, "chan.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "chan",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "channel_length": {{ "kind": "I64", "values": [3, 5] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
from = GER40_SEPT2024_FROM_MS.parse::<i64>().unwrap(),
|
||
to = GER40_SEPT2024_TO_MS.parse::<i64>().unwrap(),
|
||
);
|
||
write_doc(&dir, "chan.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "chan.campaign.json"]);
|
||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||
let lines: Vec<&str> = out.lines().filter(|l| l.starts_with("{\"family_id\":")).collect();
|
||
assert_eq!(lines.len(), 2, "one member line per channel length: {out}");
|
||
for line in &lines {
|
||
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
|
||
assert_eq!(
|
||
v["report"]["manifest"]["instrument"].as_str(),
|
||
Some("GER40"),
|
||
"campaign member manifest stamps the instrument: {line}"
|
||
);
|
||
}
|
||
let family_id = serde_json::from_str::<serde_json::Value>(lines[0])
|
||
.expect("first member line parses")["family_id"]
|
||
.as_str()
|
||
.expect("member line carries family_id")
|
||
.to_string();
|
||
let (rep_out, rep_code) = run_code_in(&dir, &["reproduce", &family_id]);
|
||
assert_eq!(rep_code, Some(0), "reproduce out: {rep_out}");
|
||
assert!(
|
||
rep_out.contains("reproduced 2/2 members bit-identically"),
|
||
"out: {rep_out}"
|
||
);
|
||
}
|
||
|
||
/// Property (#275, hostless): `run_blueprint_member`'s migrated production call
|
||
/// site (`h.run_bound(key_supply(binding, sources))`) resolves the same
|
||
/// role-keyed pairing on every member of a multi-column sweep AND on the
|
||
/// independent `reproduce` re-run — a sibling of
|
||
/// `sweep_real_ohlc_channel_members_run_and_reproduce` above (gated on a local
|
||
/// GER40 mount this sandbox lacks) that drives the identical multi-role fixture
|
||
/// over `fresh_project_with_data`'s hermetic synthetic archive, so this call
|
||
/// site's by-name-binding regression guard runs on every host. Bit-identical
|
||
/// reproduction is the stronger check here: `reproduce` re-derives `binding` and
|
||
/// re-opens `sources` independently of the first run, so it can only match if
|
||
/// both derivations resolve to the exact same role→column pairing.
|
||
#[test]
|
||
fn sweep_real_ohlc_channel_members_run_and_reproduce_hostless() {
|
||
let (dir, _g) = fresh_project_with_data();
|
||
let fixture = format!("{}/tests/fixtures/r_channel_open.json", env!("CARGO_MANIFEST_DIR"));
|
||
let bp_id = seed_blueprint_file(&dir, &fixture, "chan-hostless-seed");
|
||
let proc_id = register_process_doc(&dir, "chanhostless.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = r#"{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "chan-hostless",
|
||
"data": { "instruments": ["SYMA"], "windows": [ { "from_ms": 1707120000000, "to_ms": 1707325200000 } ] },
|
||
"strategies": [ { "ref": { "content_id": "__BP__" },
|
||
"axes": { "channel_length": { "kind": "I64", "values": [3, 5] } } } ],
|
||
"process": { "ref": { "content_id": "__PROC__" } },
|
||
"seed": 7,
|
||
"presentation": { "persist_taps": [], "emit": ["family_table"] }
|
||
}"#
|
||
.replace("__BP__", &bp_id)
|
||
.replace("__PROC__", &proc_id);
|
||
write_doc(&dir, "chanhostless.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "chanhostless.campaign.json"]);
|
||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||
let lines: Vec<&str> = out.lines().filter(|l| l.starts_with("{\"family_id\":")).collect();
|
||
assert_eq!(lines.len(), 2, "one member line per channel length: {out}");
|
||
for line in &lines {
|
||
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
|
||
assert_eq!(
|
||
v["report"]["manifest"]["instrument"].as_str(),
|
||
Some("SYMA"),
|
||
"campaign member manifest stamps the instrument: {line}"
|
||
);
|
||
}
|
||
let family_id = serde_json::from_str::<serde_json::Value>(lines[0])
|
||
.expect("first member line parses")["family_id"]
|
||
.as_str()
|
||
.expect("member line carries family_id")
|
||
.to_string();
|
||
let (rep_out, rep_code) = run_code_in(&dir, &["reproduce", &family_id]);
|
||
assert_eq!(rep_code, Some(0), "reproduce out: {rep_out}");
|
||
assert!(
|
||
rep_out.contains("reproduced 2/2 members bit-identically"),
|
||
"out: {rep_out}"
|
||
);
|
||
}
|
||
|
||
/// Property, ported onto a hand-authored one-cell campaign document (#319
|
||
/// Task 8 — the `--name`-propagation and CLI window-probe/ms<->ns-conversion
|
||
/// halves of the original test were the argv-translator's OWN machinery
|
||
/// (`translate_sweep`/`campaign_window_ms`), retired with the verb; a
|
||
/// hand-authored document has no CLI-side window probe or `--name` to
|
||
/// propagate, so those checks do not survive): `exec` prints one member line
|
||
/// per grid point, IN AXIS ORDER (odometer: the first document axis varies
|
||
/// slowest), each line JSON with a `family_id` and a `report` whose
|
||
/// `manifest.params` carries every swept param binding (RAW names, #328) —
|
||
/// and **`report.manifest` carries a stamped `instrument` key** (the campaign
|
||
/// substrate stamps every member's manifest with the instrument under test,
|
||
/// exactly like `campaign_run_real_e2e_sweep_gate_walkforward` in
|
||
/// `research_docs.rs`) and the resolved default stop. Also pins that the run
|
||
/// persists exactly one new `FamilyKind::Sweep` family with the grid's member
|
||
/// count, and that re-`exec`ing the SAME document reproduces identical member
|
||
/// reports (C1) while incrementing the family's run-suffix. Gated on the
|
||
/// local GER40 archive (the project's skip-on-no-data convention); skips
|
||
/// cleanly when absent.
|
||
#[test]
|
||
fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "dissolved-contract-seed");
|
||
let proc_id = register_process_doc(&dir, "brp.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc(
|
||
"brp", "GER40", &bp_id, &proc_id,
|
||
(GER40_SEPT2024_FROM_MS.parse().unwrap(), GER40_SEPT2024_TO_MS.parse().unwrap()),
|
||
"2, 4", "8, 16",
|
||
);
|
||
write_doc(&dir, "brp.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "brp.campaign.json"]);
|
||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||
let lines: Vec<&str> = out.lines().filter(|l| l.starts_with("{\"family_id\":")).collect();
|
||
assert_eq!(lines.len(), 4, "one member line per 2x2 grid point: {out}");
|
||
|
||
// Odometer order: `fast.length` (first document axis, outer/slowest) x
|
||
// `slow.length` (second, inner) -> (2,8),(2,16),(4,8),(4,16).
|
||
let expected: [[(&str, i64); 2]; 4] = [
|
||
[("fast.length", 2), ("slow.length", 8)],
|
||
[("fast.length", 2), ("slow.length", 16)],
|
||
[("fast.length", 4), ("slow.length", 8)],
|
||
[("fast.length", 4), ("slow.length", 16)],
|
||
];
|
||
|
||
for i in 0..4 {
|
||
let line = lines[i];
|
||
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
|
||
assert!(v["family_id"].as_str().is_some(), "member line carries a family_id: {line}");
|
||
let manifest = &v["report"]["manifest"];
|
||
assert!(manifest.is_object(), "report.manifest is present: {line}");
|
||
let params = manifest["params"].as_array().expect("manifest.params is an array");
|
||
for (name, value) in expected[i] {
|
||
let bound = params
|
||
.iter()
|
||
.find(|p| p[0].as_str() == Some(name))
|
||
.and_then(|p| p[1]["I64"].as_i64());
|
||
assert_eq!(bound, Some(value), "manifest carries the swept binding {name}={value}: {line}");
|
||
}
|
||
assert_eq!(
|
||
manifest["instrument"].as_str(),
|
||
Some("GER40"),
|
||
"the campaign substrate stamps the instrument: {line}"
|
||
);
|
||
assert!(
|
||
params.iter().any(|p| p[0].as_str() == Some("stop_length"))
|
||
&& params.iter().any(|p| p[0].as_str() == Some("stop_k")),
|
||
"the campaign substrate stamps the resolved stop: {line}"
|
||
);
|
||
}
|
||
|
||
// exactly one new Sweep family persisted, with the grid's four members.
|
||
let (fams_out, fams_code) = run_code_in(&dir, &["runs", "families"]);
|
||
assert_eq!(fams_code, Some(0), "families exit: {fams_out}");
|
||
assert_eq!(fams_out.lines().count(), 1, "exactly one family persisted: {fams_out}");
|
||
assert!(fams_out.contains("\"kind\":\"Sweep\""), "families: {fams_out}");
|
||
assert!(fams_out.contains("\"members\":4"), "2x2 grid -> four members: {fams_out}");
|
||
|
||
// Re-exec'ing the SAME document: one campaign document dedupes (content-
|
||
// addressed), a fresh family record is written on every execution (the
|
||
// "-{run}" suffix, aura-campaign::exec::run_cell), so two identical
|
||
// invocations are NOT byte-identical on stdout: only the trailing
|
||
// run-suffix (0 -> 1) differs. C1 determinism is checked at the
|
||
// member-report level instead (every report field byte-identical), plus
|
||
// the run-suffix increment.
|
||
let (out2, code2) = run_code_in(&dir, &["exec", "brp.campaign.json"]);
|
||
assert_eq!(code2, Some(0), "second run: {out2}");
|
||
let lines2: Vec<&str> = out2.lines().filter(|l| l.starts_with("{\"family_id\":")).collect();
|
||
assert_eq!(lines2.len(), lines.len(), "same member count on the dedup run: {out2}");
|
||
for (l1, l2) in lines.iter().zip(&lines2) {
|
||
let v1: serde_json::Value = serde_json::from_str(l1).expect("first run member line parses");
|
||
let v2: serde_json::Value = serde_json::from_str(l2).expect("second run member line parses");
|
||
assert_eq!(v1["report"], v2["report"], "identical invocations reproduce identical member reports (C1): {l1} vs {l2}");
|
||
let (fam1, fam2) = (v1["family_id"].as_str().unwrap(), v2["family_id"].as_str().unwrap());
|
||
assert_eq!(
|
||
fam1.trim_end_matches(|c: char| c.is_ascii_digit()),
|
||
fam2.trim_end_matches(|c: char| c.is_ascii_digit()),
|
||
"same family name up to the run-suffix: {fam1} vs {fam2}"
|
||
);
|
||
assert!(fam1.ends_with("-0") && fam2.ends_with("-1"), "run-suffix increments 0 -> 1: {fam1} vs {fam2}");
|
||
}
|
||
let campaigns_dir_count = std::fs::read_dir(dir.join("runs").join("campaigns")).map(|d| d.count()).unwrap_or(0);
|
||
assert_eq!(campaigns_dir_count, 1, "second identical run dedupes the campaign doc");
|
||
}
|
||
|
||
/// True iff `root` contains, at any depth, a regular file of non-zero length —
|
||
/// the layout-agnostic "traces actually landed on disk" probe. #224 does not yet
|
||
/// pin the per-member trace-directory naming (the sweep sugar's cell has no
|
||
/// nominee and `persist_campaign_traces` keys by `<trace_name>/<cell_key>`), so
|
||
/// this walks the tree rather than hard-coding a path, keeping the headline about
|
||
/// *that* traces are written, not *where*.
|
||
fn any_nonempty_file(root: &Path) -> bool {
|
||
let mut stack = vec![root.to_path_buf()];
|
||
while let Some(p) = stack.pop() {
|
||
let Ok(entries) = std::fs::read_dir(&p) else { continue };
|
||
for entry in entries.flatten() {
|
||
let path = entry.path();
|
||
if path.is_dir() {
|
||
stack.push(path);
|
||
} else if std::fs::metadata(&path).map(|m| m.len() > 0).unwrap_or(false) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
false
|
||
}
|
||
|
||
/// Property (#224): `aura sweep <blueprint.json> --real SYM --axis … --trace <fam>`
|
||
/// DELIVERS the advertised trace-writing capability instead of the #168
|
||
/// refuse-with-pointer. Two things must hold once the capability ships:
|
||
/// (1) the run no longer refuses (`aura: --trace is not yet available on sweep;
|
||
/// see #224`, exit 2) but succeeds, and (2) the tap series actually land on disk
|
||
/// under `runs/traces/` — at least one non-empty tap file is written, the honest
|
||
/// inverse of the #168 no-op where the `--real` sugar set `persist_taps: vec![]`
|
||
/// (verb_sugar.rs) and `run_blueprint_sweep` did `let _ = persist`, so nothing was
|
||
/// ever written. The exact on-disk layout (trace name + per-member/nominee key)
|
||
/// is deliberately NOT pinned here: this headline pins that traces are written at
|
||
/// all — not how many dirs, nor under what member key — because that naming is
|
||
/// the GREEN slice's open design point (the selection-free sweep cell has no
|
||
/// nominee for the existing per-cell `persist_campaign_traces` mechanism, #224).
|
||
/// Gated on the local GER40 archive (the project skip-on-no-data convention);
|
||
/// the refused status quo makes it RED on a data-ful host today.
|
||
#[test]
|
||
fn sweep_real_with_trace_writes_tap_series_to_disk() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
// Ported onto a one-cell campaign document declaring `persist_taps`
|
||
// (#319 Task 8) — the property under test (tap series actually land on
|
||
// disk, the #168/#224 headline) is the campaign substrate's own
|
||
// `persist_campaign_traces` mechanism, reached identically whether the
|
||
// document was hand-authored or sugar-generated; `--trace`'s own
|
||
// refuse-then-deliver history is retired-verb-specific and not re-pinned.
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "trace-seed");
|
||
let proc_id = register_process_doc(&dir, "trace.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
// `presentation.persist_taps` names entries from the closed measurement
|
||
// vocabulary (equity | exposure | r_equity | net_r_equity), not arbitrary
|
||
// blueprint-declared taps (that's `--tap`'s namespace on the blueprint
|
||
// leg, invariant 7) — "equity" is always producible for an R strategy.
|
||
let campaign_doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "brp",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2, 4] }},
|
||
"slow.length": {{ "kind": "I64", "values": [8] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": ["equity"], "emit": ["family_table"] }}
|
||
}}"#,
|
||
from = GER40_SEPT2024_FROM_MS.parse::<i64>().unwrap(),
|
||
to = GER40_SEPT2024_TO_MS.parse::<i64>().unwrap(),
|
||
);
|
||
write_doc(&dir, "brp.campaign.json", &campaign_doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "brp.campaign.json"]);
|
||
assert_eq!(code, Some(0), "exec must succeed and persist the requested taps: {out}");
|
||
// the tap series actually land on disk under runs/traces/ (non-empty) —
|
||
// the honest inverse of the #168 `persist_taps: vec![]` no-op.
|
||
let traces_root = dir.join("runs").join("traces");
|
||
assert!(
|
||
any_nonempty_file(&traces_root),
|
||
"the campaign run must write at least one non-empty tap file under {}: {out}",
|
||
traces_root.display(),
|
||
);
|
||
}
|
||
|
||
/// Extract the first persisted family whose `kind` matches `want` from
|
||
/// `aura runs families` stdout (one JSON object per line), returning its
|
||
/// `family_id` — the id `aura reproduce` addresses. Panics if none matches so a
|
||
/// mis-minted store fails loudly rather than skipping the assertion.
|
||
fn family_id_of_kind(dir: &Path, want: &str) -> String {
|
||
let out = Command::new(BIN).args(["runs", "families"]).current_dir(dir).output().unwrap();
|
||
assert!(out.status.success(), "runs families exit: {:?}", out.status);
|
||
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
|
||
for line in stdout.lines() {
|
||
let v: serde_json::Value = serde_json::from_str(line).expect("family line parses as JSON");
|
||
if v["kind"].as_str() == Some(want) {
|
||
return v["family_id"].as_str().expect("family_id is a string").to_string();
|
||
}
|
||
}
|
||
panic!("no {want} family in the store: {stdout}");
|
||
}
|
||
|
||
/// Reconstruct the retired `walkforward_summary_json_from_reports`'s
|
||
/// aggregate (deleted Task 8 Step 2) from a WalkForward family's OWN
|
||
/// persisted per-window `RunReport`s (`aura runs family <id>` bare-report
|
||
/// lines) — the SAME reduction, over data the campaign path populates
|
||
/// identically: `stitched_total_pips` sums each window's own `total_pips`
|
||
/// (additive, roll order irrelevant to a sum); pooled `n_trades` sums each
|
||
/// window's own trade count; pooled `expectancy_r` is the trade-count-
|
||
/// weighted mean of each window's own `expectancy_r` (mathematically
|
||
/// identical to the mean of the raw pooled per-trade R series, which is
|
||
/// NOT itself recoverable from JSON — `RMetrics::net_trade_rs` is
|
||
/// `#[serde(skip)]`); `param_stability[i].mean` is the arithmetic mean of
|
||
/// axis `axes[i]`'s per-window chosen value (`manifest.params`, never
|
||
/// skipped).
|
||
fn walkforward_aggregate_from_reports(reports: &[serde_json::Value], axes: &[&str]) -> serde_json::Value {
|
||
let stitched_total_pips: f64 = reports.iter().map(|r| r["metrics"]["total_pips"].as_f64().unwrap()).sum();
|
||
let n_trades: i64 = reports.iter().map(|r| r["metrics"]["r"]["n_trades"].as_i64().unwrap_or(0)).sum();
|
||
let weighted_sum: f64 = reports
|
||
.iter()
|
||
.map(|r| {
|
||
let n = r["metrics"]["r"]["n_trades"].as_f64().unwrap_or(0.0);
|
||
let e = r["metrics"]["r"]["expectancy_r"].as_f64().unwrap_or(0.0);
|
||
n * e
|
||
})
|
||
.sum();
|
||
let expectancy_r = if n_trades > 0 { weighted_sum / n_trades as f64 } else { 0.0 };
|
||
let param_stability: Vec<serde_json::Value> = axes
|
||
.iter()
|
||
.map(|axis| {
|
||
let values: Vec<f64> = reports
|
||
.iter()
|
||
.map(|r| {
|
||
let params = r["manifest"]["params"].as_array().expect("manifest.params array");
|
||
let entry = params
|
||
.iter()
|
||
.find(|p| p[0].as_str() == Some(*axis))
|
||
.unwrap_or_else(|| panic!("axis {axis} recorded in manifest.params"));
|
||
entry[1]["I64"].as_f64().or_else(|| entry[1]["F64"].as_f64()).expect("scalar value")
|
||
})
|
||
.collect();
|
||
let mean = values.iter().sum::<f64>() / values.len() as f64;
|
||
serde_json::json!({ "mean": mean })
|
||
})
|
||
.collect();
|
||
serde_json::json!({
|
||
"windows": reports.len(),
|
||
"stitched_total_pips": stitched_total_pips,
|
||
"oos_r": { "n_trades": n_trades, "expectancy_r": expectancy_r },
|
||
"param_stability": param_stability,
|
||
})
|
||
}
|
||
|
||
/// Property (C18/C1 reproduce guarantee): `aura reproduce` re-derives a family
|
||
/// over the SAME data it was minted on. A sweep family minted over real GER40
|
||
/// bars must therefore reproduce bit-identically — the run is deterministic
|
||
/// (a fresh identical sweep matches the persisted metrics to the last digit), so
|
||
/// the README's "check it is bit-identical" promise must hold. It must NOT
|
||
/// silently re-run every member over the built-in synthetic showcase stream,
|
||
/// which diverges from the stored real-data metrics on every member (observed:
|
||
/// `reproduced 0/N bit-identically`, exit 1). Gated on the local GER40 archive
|
||
/// (skip-on-no-data convention); uses the plain `r_sma.json` blueprint —
|
||
/// no gang, no campaign-specific fixture — because the divergence is driven by
|
||
/// `--real`, not by the minting path.
|
||
#[test]
|
||
fn reproduce_real_sweep_family_re_derives_bit_identically() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "repro-sweep-seed");
|
||
let proc_id = register_process_doc(&dir, "reprosweep.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc(
|
||
"repro_sweep", "GER40", &bp_id, &proc_id,
|
||
(GER40_SEPT2024_FROM_MS.parse().unwrap(), GER40_SEPT2024_TO_MS.parse().unwrap()),
|
||
"2, 4", "8",
|
||
);
|
||
write_doc(&dir, "reprosweep.campaign.json", &doc);
|
||
let (mint_out, mint_code) = run_code_in(&dir, &["exec", "reprosweep.campaign.json"]);
|
||
assert_eq!(mint_code, Some(0), "sweep mint: {mint_out}");
|
||
|
||
let id = family_id_of_kind(&dir, "Sweep");
|
||
let (out, code) = run_code_in(&dir, &["reproduce", &id]);
|
||
assert_eq!(
|
||
code, Some(0),
|
||
"reproduce of a real-data sweep family must succeed (exit 0): {out}"
|
||
);
|
||
assert!(
|
||
out.contains("reproduced 2/2 members bit-identically"),
|
||
"every member of a deterministic real-data sweep re-derives bit-identically: {out}"
|
||
);
|
||
assert!(
|
||
!out.contains("DIVERGED"),
|
||
"no member may DIVERGE — the run is deterministic, reproduce must use the recorded data source: {out}"
|
||
);
|
||
}
|
||
|
||
/// Property (C18/C1 reproduce guarantee, #246): the same reproduce guarantee holds
|
||
/// when the swept axis names a BOUND param of a CLOSED starter blueprint, not an
|
||
/// already-open one — `r_sma.json` (both `fast.length` and `slow.length` bound),
|
||
/// overriding only the `fast.length` axis and leaving `slow.length` bound —
|
||
/// unlike the walk-forward reproduce sibling
|
||
/// (`aura_walkforward_over_a_blueprint_reproduces_bit_identically`), which
|
||
/// overrides both axes over the same closed blueprint. The walk-forward
|
||
/// synthetic route's own `--axis` intake is now RAW (this cycle's tidy fix,
|
||
/// #328): the CLI preflight refuses a wrapped name before this run ever starts
|
||
/// (`refuse_wrapped_synthetic_axes`), and `blueprint_sweep_over`'s own
|
||
/// `SweepBinder` call translates the RAW name onto its wrapped `param_space()`
|
||
/// slot (`wrapped_name_of`) before keying by it.
|
||
/// Modelled on that sibling (synthetic in-process walk-forward — no `--real`),
|
||
/// NOT on `reproduce_real_sweep_family_re_derives_bit_identically`: a real-data
|
||
/// mint routes through the campaign executor (`CliMemberRunner::run_member` +
|
||
/// `validate_and_register_axes`), neither of which carries any #246 override
|
||
/// threading — a gap outside this task's file scope (`blueprint_sweep_over`,
|
||
/// `reproduce_family_in`) and spanning all four dispatch verbs, so exercising
|
||
/// it here would silently expand into unrelated, unreviewed surface. The
|
||
/// synthetic walk-forward path exercises exactly the threaded functions:
|
||
/// `blueprint_sweep_over` (via `blueprint_walkforward_family`) re-opens
|
||
/// `fast.length` for the per-window IS re-fit, and
|
||
/// `reproduce_family_in` re-opens it again from the recorded manifest params
|
||
/// so the re-derivation resolves against the same space the mint used.
|
||
#[test]
|
||
fn reproduce_family_with_overridden_bound_param_re_derives() {
|
||
// Ported onto a `[std::grid, std::walk_forward]` campaign document over
|
||
// the hermetic synthetic SYMA archive (#319 Task 8) — there is no
|
||
// surviving campaign-document equivalent of "sweep the embedded showcase
|
||
// stream with no instrument at all" (a `CampaignDoc` always names a real
|
||
// `data.instruments` entry, invariant 7), so the property (reproduce
|
||
// re-derives a WalkForward family minted by overriding only ONE bound
|
||
// param, `slow.length` left at its blueprint-bound default) is exercised
|
||
// over SYMA instead. The exact OOS member count is roller/data-derived,
|
||
// not hardcoded — this document's window/roller shape is the same one
|
||
// `exec.rs`'s zero-trade-note tests already prove out end to end, so the
|
||
// member count is asserted structurally (> 0, matches the family listing)
|
||
// rather than pinned to a translator-era literal.
|
||
let (dir, _g) = fresh_project_with_data();
|
||
let bp_id = seed_blueprint(&dir, "wfo-seed");
|
||
let proc_id = register_process_doc(&dir, "wfo.process.json", GRID_THEN_WF_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "wfo",
|
||
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": 1709251200000, "to_ms": 1719791999999 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2, 3] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&dir, "wfo.campaign.json", &doc);
|
||
let (run_out, run_code) = run_code_in(&dir, &["exec", "wfo.campaign.json"]);
|
||
assert_eq!(
|
||
run_code, Some(0),
|
||
"walk-forward over a bound-param axis must succeed (#246 re-open): {run_out}"
|
||
);
|
||
let member_count = run_out.lines().filter(|l| l.starts_with("{\"family_id\":")).count();
|
||
assert!(member_count > 0, "at least one OOS member: {run_out}");
|
||
|
||
let id = family_id_of_kind(&dir, "WalkForward");
|
||
let (out, code) = run_code_in(&dir, &["reproduce", &id]);
|
||
assert_eq!(code, Some(0), "reproduce: {out}");
|
||
assert!(
|
||
out.contains(&format!("reproduced {member_count}/{member_count} members bit-identically")),
|
||
"every OOS member of a deterministic walk-forward over a bound-param axis \
|
||
re-derives bit-identically: {out}"
|
||
);
|
||
}
|
||
|
||
/// Property (#246, distinct code path from `reproduce_family_with_overridden_bound_param_re_derives`):
|
||
/// `reproduce_family_in`'s override derivation (`wrapped_bound_overrides_of`, computed once
|
||
/// per family from the FIRST member's recorded params) applies uniformly across
|
||
/// `FamilyKind`s, not only `WalkForward` — a plain `aura sweep` family minted by
|
||
/// overriding a bound param on the CLOSED `r_sma.json` fixture re-derives
|
||
/// bit-identically too. The sibling test exercises `FamilyKind::WalkForward`'s
|
||
/// branch (per-window OOS reload via `run_oos_blueprint`); this one exercises the
|
||
/// `Sweep`-kind branch (`data.run_sources`/`data.full_window`, no per-window
|
||
/// re-fit) of the same shared dispatch in `reproduce_family_in`. A regression that
|
||
/// threaded the #246 override set through one branch but not the other would pass
|
||
/// the walk-forward sibling while failing here.
|
||
#[test]
|
||
fn aura_reproduce_re_derives_a_sweep_family_minted_over_a_bound_param_override() {
|
||
// Ported onto a one-cell campaign document over SYMA (#319 Task 8, same
|
||
// substitution rationale as the walk-forward sibling above): the member
|
||
// count (2) is structurally guaranteed by the axis grid size, not by the
|
||
// data source, so it stays pinned exactly.
|
||
let (dir, _g) = fresh_project_with_data();
|
||
let bp_id = seed_blueprint(&dir, "swo-seed");
|
||
let proc_id = register_process_doc(&dir, "swo.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "swo",
|
||
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": 1709251200000, "to_ms": 1719791999999 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2, 3] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&dir, "swo.campaign.json", &doc);
|
||
let (sweep_out, sweep_code) = run_code_in(&dir, &["exec", "swo.campaign.json"]);
|
||
assert_eq!(
|
||
sweep_code, Some(0),
|
||
"sweeping a bound param of a closed blueprint must succeed (#246 re-open): {sweep_out}"
|
||
);
|
||
let id = family_id_of_kind(&dir, "Sweep");
|
||
let (out, code) = run_code_in(&dir, &["reproduce", &id]);
|
||
assert_eq!(code, Some(0), "reproduce: {out}");
|
||
assert!(
|
||
out.contains("reproduced 2/2 members bit-identically"),
|
||
"every member of a plain sweep minted over a bound-param override \
|
||
re-derives bit-identically: {out}"
|
||
);
|
||
}
|
||
|
||
/// Property: `aura reproduce` yields a handled outcome — a reproduction verdict
|
||
/// or a clean `aura:` refusal — for a real-data WalkForward family, and NEVER an
|
||
/// internal panic. A WalkForward member's OOS window is stored as real epoch-ns
|
||
/// bounds; reconstructing that window over the built-in synthetic walk (whose 60
|
||
/// bars fall entirely outside those bounds) yields an empty source, which today
|
||
/// reaches the unguarded `window_of(&s).expect("non-empty OOS window")` and
|
||
/// aborts the process (exit 101). Gated on the local GER40 archive
|
||
/// (skip-on-no-data convention); a ~135-day window is the minimum that stitches
|
||
/// at least one OOS window (IS 90d + OOS 30d).
|
||
#[test]
|
||
fn reproduce_real_walkforward_family_does_not_panic() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
// Ported onto a `[std::grid, std::walk_forward]` campaign document
|
||
// (#319 Task 8), the fixed 14d/7d/7d roller `exec.rs`/`research_docs.rs`
|
||
// already prove out end to end. 2025-01-01 .. ~2025-05-16 (~135 days) —
|
||
// long enough for one IS(90d)+OOS(30d)-equivalent walk-forward window
|
||
// over the local GER40 archive.
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "repro-wf-seed");
|
||
let proc_id = register_process_doc(&dir, "reprowf.process.json", GRID_THEN_WF_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "repro_wf",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1735689600000, "to_ms": 1747353600000 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2, 4] }},
|
||
"slow.length": {{ "kind": "I64", "values": [8] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&dir, "reprowf.campaign.json", &doc);
|
||
let (mint_out, mint_code) = run_code_in(&dir, &["exec", "reprowf.campaign.json"]);
|
||
assert_eq!(mint_code, Some(0), "walkforward mint: {mint_out}");
|
||
|
||
let id = family_id_of_kind(&dir, "WalkForward");
|
||
let (out, code) = run_code_in(&dir, &["reproduce", &id]);
|
||
assert_ne!(
|
||
code, Some(101),
|
||
"reproduce of a WalkForward family must not panic (exit 101): {out}"
|
||
);
|
||
assert!(
|
||
!out.contains("panicked") && !out.contains("non-empty OOS window"),
|
||
"no internal panic may leak from reproduce: {out}"
|
||
);
|
||
assert!(
|
||
out.contains("reproduced") || out.contains("aura:"),
|
||
"reproduce must yield a verdict or a clean `aura:` refusal: {out}"
|
||
);
|
||
}
|
||
|
||
// Fixture blueprint for the verb-level multi-column binding coverage below
|
||
// (#231 task 4 quality follow-up): an OPEN high/low blueprint with two
|
||
// SMA-length axes. Written to a fresh temp file per test rather than a
|
||
// tracked `examples/` fixture — exists only to drive the real-open contract,
|
||
// not as an authoring sample. (`HL_RANGE_CLOSED_BLUEPRINT`, the zero-param
|
||
// closed sibling, was retired #319 Task 8 — a campaign document needs >= 1
|
||
// axis per strategy, invariant no zero-param fixture can satisfy, so the
|
||
// real-data multi-column single-run property was re-pointed onto this
|
||
// fixture at a single axis value instead.)
|
||
const HL_SIGNAL_OPEN_BLUEPRINT: &str = r#"{"format_version":1,"blueprint":{"name":"hl_signal","doc":"high/low SMA difference clamped into a directional bias","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"primitive":{"type":"SMA","name":"slow"}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},{"name":"low","targets":[{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}}"#;
|
||
|
||
|
||
// Note (#319 Task 8, disposition deviation): `walkforward_synthetic_refuses_a_
|
||
// multi_column_blueprint_before_data_access` (the synthetic, no-`--real`
|
||
// exit-process arm) retired rather than ported — it is a byte-identical twin
|
||
// of `exec.rs::exec_refuses_a_multi_column_blueprint_before_data_access`
|
||
// (same "consumes columns beyond close" prose, same data-free-before-access
|
||
// property, same closed-form fixture family): the guard fires on column
|
||
// consumption alone, independent of whether the blueprint carries an open
|
||
// axis, so an OPEN-blueprint-with-an-axis-grid variant exercises no
|
||
// additional surface over the already-green CLOSED-blueprint pin.
|
||
|
||
/// Property (#231 task 4 quality follow-up): the positive counterpart of the
|
||
/// two refusals above — a multi-column blueprint over `--real GER40` opens
|
||
/// BOTH the high and low columns end to end (`open_real_source` ->
|
||
/// `resolve_run_data` -> `run_signal_r`) and produces a real `RunReport`,
|
||
/// never the single-close weld the pre-task-4 code hardcoded. Gated on the
|
||
/// local GER40 archive (skip-on-no-data convention).
|
||
#[test]
|
||
fn run_real_multi_column_blueprint_opens_high_and_low_columns() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
// Ported onto a one-cell campaign document (#319 Task 8 — exec's
|
||
// blueprint leg has no `--real` at all, invariant 7). `HL_RANGE_CLOSED_
|
||
// BLUEPRINT` (the bare zero-param `Sub` node) cannot host a campaign
|
||
// document at all — a campaign's `strategies[].axes` map must carry >= 1
|
||
// axis (an intrinsic document-shape invariant, `validate_campaign`), and
|
||
// that fixture has no param of any kind to axis over — so this reuses
|
||
// `HL_SIGNAL_OPEN_BLUEPRINT` (the sweep sibling's fixture, below) at a
|
||
// single axis value, preserving the "opens both high and low columns"
|
||
// property exactly.
|
||
let (dir, _g) = fresh_project();
|
||
let fixture_path = dir.join("hl_signal.json");
|
||
std::fs::write(&fixture_path, HL_SIGNAL_OPEN_BLUEPRINT).expect("write fixture");
|
||
let bp_id = seed_blueprint_file(&dir, fixture_path.to_str().unwrap(), "hlsignal-single-seed");
|
||
let proc_id = register_process_doc(&dir, "hlsignal_single.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "hlsignal-single",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2] }},
|
||
"slow.length": {{ "kind": "I64", "values": [8] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
from = GER40_SEPT2024_FROM_MS.parse::<i64>().unwrap(),
|
||
to = GER40_SEPT2024_TO_MS.parse::<i64>().unwrap(),
|
||
);
|
||
write_doc(&dir, "hlsignal_single.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "hlsignal_single.campaign.json"]);
|
||
assert_eq!(
|
||
code, Some(0),
|
||
"a high/low blueprint over real GER40 data must run cleanly: {out}"
|
||
);
|
||
assert!(
|
||
out.lines().any(|l| l.starts_with("{\"family_id\":")),
|
||
"exit 0 must carry a member report: {out}"
|
||
);
|
||
}
|
||
|
||
/// Property (#231 task 4, "every real-data path" — `run --real` above covers
|
||
/// only `open_real_source`/`run_signal_r`; `sweep --real` walks a DISTINCT
|
||
/// real-data path, `blueprint_sweep_family` -> `blueprint_sweep_over` ->
|
||
/// `DataSource::windowed_sources`, which independently needed `binding.
|
||
/// columns()` threaded through in task 4): a multi-column (high/low) OPEN
|
||
/// blueprint swept over `--real GER40` opens both columns per member and
|
||
/// produces one member line per grid point, not a source-count-mismatch panic
|
||
/// or a fallback to a single close source. Gated on the local GER40 archive.
|
||
#[test]
|
||
fn sweep_real_multi_column_blueprint_opens_high_and_low_columns() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
let (dir, _g) = fresh_project();
|
||
let fixture_path = dir.join("hl_signal.json");
|
||
std::fs::write(&fixture_path, HL_SIGNAL_OPEN_BLUEPRINT).expect("write fixture");
|
||
let bp_id = seed_blueprint_file(&dir, fixture_path.to_str().unwrap(), "hlsignal-sweep-seed");
|
||
let proc_id = register_process_doc(&dir, "hlsignal_sweep.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc(
|
||
"hlsignal-sweep", "GER40", &bp_id, &proc_id,
|
||
(GER40_SEPT2024_FROM_MS.parse().unwrap(), GER40_SEPT2024_TO_MS.parse().unwrap()),
|
||
"2, 4", "8",
|
||
);
|
||
write_doc(&dir, "hlsignal_sweep.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "hlsignal_sweep.campaign.json"]);
|
||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||
let lines: Vec<&str> = out.lines().filter(|l| l.starts_with("{\"family_id\":")).collect();
|
||
assert_eq!(lines.len(), 2, "one member line per grid point: {out}");
|
||
for line in &lines {
|
||
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
|
||
assert!(
|
||
v["report"]["manifest"].is_object(),
|
||
"each member carries a real report, not a source-mismatch fault: {line}"
|
||
);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/// Property (issue #246, task 5): the dissolved real-data `aura sweep`
|
||
/// campaign path accepts an `--axis` naming a BOUND param of a CLOSED
|
||
/// blueprint, not only an already-open one. Tasks 3-4 threaded this override
|
||
/// convention (bound value = default, `Composite::reopen`) through the
|
||
/// SYNTHETIC blueprint verb paths (`blueprint_sweep_family`/
|
||
/// `blueprint_sweep_over`, `reproduce_family_with_overridden_bound_param_re_derives`);
|
||
/// the real-data dissolved path routes through a distinct seam
|
||
/// (`CliMemberRunner::run_member` + `verb_sugar::validate_before_register`'s
|
||
/// P3 preflight) that carried no such threading at all, the explicit gap
|
||
/// named on `reproduce_family_with_overridden_bound_param_re_derives`'s doc
|
||
/// comment. Modelled on `sweep_real_blueprint_member_lines_pin_the_dissolved_contract`
|
||
/// (the dissolved-sweep e2e family `walkforward_dissolved_refuses_an_unknown_axis_name`
|
||
/// belongs to): same store-reading assertions (`aura runs families`, one
|
||
/// persisted `Sweep` family, member count), over the same `r_sma.json`
|
||
/// fixture every other real-data sweep pin uses (`fast.length`/
|
||
/// `slow.length` both bound) — the distinguishing property here
|
||
/// is that the swept axis names an already-bound param, not that the
|
||
/// fixture differs.
|
||
#[test]
|
||
fn sweep_dissolved_accepts_an_axis_over_a_bound_param() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
let (dir, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&dir, "bound-override-seed");
|
||
let proc_id = register_process_doc(&dir, "boundoverride.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "bound-override",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2, 3] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
from = GER40_SEPT2024_FROM_MS.parse::<i64>().unwrap(),
|
||
to = GER40_SEPT2024_TO_MS.parse::<i64>().unwrap(),
|
||
);
|
||
write_doc(&dir, "boundoverride.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&dir, &["exec", "boundoverride.campaign.json"]);
|
||
assert_eq!(
|
||
code, Some(0),
|
||
"a bound-param axis must re-open (#246) on the dissolved real-data campaign path too: {out}"
|
||
);
|
||
let (fams_out, fams_code) = run_code_in(&dir, &["runs", "families"]);
|
||
assert_eq!(fams_code, Some(0), "families exit: {fams_out}");
|
||
assert_eq!(fams_out.lines().count(), 1, "exactly one family persisted: {fams_out}");
|
||
assert!(fams_out.contains("\"kind\":\"Sweep\""), "families: {fams_out}");
|
||
assert!(
|
||
fams_out.contains("\"members\":2"),
|
||
"the two-value bound-param axis grid yields two members: {fams_out}"
|
||
);
|
||
}
|
||
|
||
/// Property (#246 override merge, exercised at scale by #248's open-twins
|
||
/// migration), ported onto one-cell campaign documents over the hermetic
|
||
/// synthetic SYMA archive (#319 Task 8 — there is no campaign-document
|
||
/// equivalent of "sweep the embedded showcase stream", invariant 7):
|
||
/// sweeping the CLOSED `examples/r_sma.json` (both SMA lengths bound to
|
||
/// defaults 2/4) with axis values that override BOTH bound lengths yields
|
||
/// BIT-IDENTICAL per-member `total_pips` to sweeping the historical OPEN
|
||
/// `tests/fixtures/r_sma_open.json` (both lengths genuinely unbound) over the
|
||
/// same axis values and the same data. #248's migration replaced the "bind an
|
||
/// open param" idiom with "override a bound param" at dozens of call sites;
|
||
/// if the override merge ever diverged from the historical bind path, every
|
||
/// migrated seed/grade site would silently compile a DIFFERENT graph while
|
||
/// still exiting 0 — no other test cross-checks the two idioms against each
|
||
/// other.
|
||
#[test]
|
||
fn sweep_over_a_bound_override_matches_the_historical_open_bind() {
|
||
let (closed_dir, _g1) = fresh_project_with_data();
|
||
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||
let closed_bp_id = seed_blueprint_file(&closed_dir, &closed_bp, "closed-seed");
|
||
let closed_proc_id = register_process_doc(&closed_dir, "closed.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let closed_doc = sweep_campaign_doc(
|
||
"closed", "SYMA", &closed_bp_id, &closed_proc_id,
|
||
(1709251200000, 1719791999999), "2, 4", "8, 16",
|
||
);
|
||
write_doc(&closed_dir, "closed.campaign.json", &closed_doc);
|
||
let (closed_out, closed_code) = run_code_in(&closed_dir, &["exec", "closed.campaign.json"]);
|
||
assert_eq!(closed_code, Some(0), "closed sweep: {closed_out}");
|
||
|
||
let (open_dir, _g2) = fresh_project_with_data();
|
||
let open_bp = format!("{}/tests/fixtures/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
||
let open_bp_id = seed_blueprint_file(&open_dir, &open_bp, "open-seed");
|
||
let open_proc_id = register_process_doc(&open_dir, "open.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let open_doc = sweep_campaign_doc(
|
||
"open", "SYMA", &open_bp_id, &open_proc_id,
|
||
(1709251200000, 1719791999999), "2, 4", "8, 16",
|
||
);
|
||
write_doc(&open_dir, "open.campaign.json", &open_doc);
|
||
let (open_out, open_code) = run_code_in(&open_dir, &["exec", "open.campaign.json"]);
|
||
assert_eq!(open_code, Some(0), "open sweep: {open_out}");
|
||
|
||
let pips = |out: &str| -> Vec<f64> {
|
||
out.lines()
|
||
.filter(|l| l.starts_with("{\"family_id\":"))
|
||
.map(|l| {
|
||
let v: serde_json::Value = serde_json::from_str(l).expect("member line parses");
|
||
v["report"]["metrics"]["total_pips"].as_f64().expect("total_pips is an f64")
|
||
})
|
||
.collect()
|
||
};
|
||
let mut closed_pips = pips(&closed_out);
|
||
let mut open_pips = pips(&open_out);
|
||
assert_eq!(closed_pips.len(), 4, "2x2 grid -> four members: {closed_out}");
|
||
assert_eq!(open_pips.len(), 4, "2x2 grid -> four members: {open_out}");
|
||
closed_pips.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||
open_pips.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||
assert_eq!(
|
||
closed_pips, open_pips,
|
||
"bound-override and historical open-bind must compute bit-identical \
|
||
member metrics for the same axis grid"
|
||
);
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
// Note (#159 cut 4 collateral): this removes the `aura chart <name>`
|
||
// CLI-integration tests (single-run and family overlay/tap-filter/meta, the
|
||
// cross-kind write-guard collision + same-kind-overwrite pair) — every one of
|
||
// them populated its `TraceStore` fixture via a bare `run --trace`/`sweep
|
||
// --trace` invocation, both retired. The only surviving `TraceStore::write`
|
||
// call site left in the whole crate is `campaign_run::persist_campaign_traces`
|
||
// (behind a campaign's `--persist-taps`) — an entirely different, much
|
||
// heavier fixture than any of these tests set up. Removed as PIP-retirement
|
||
// collateral, not
|
||
// repointed; the render/build logic they exercised stays covered by the
|
||
// hand-built-fixture unit tests (`build_chart_data_threads_run_manifest_into_meta`
|
||
// here, `render_chart_html_*` in render.rs), which never touch the CLI or disk.
|
||
|
||
// --- `aura run --harness <name>` selector (iter-3 Task 3) --------------------
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/// Property: `aura generalize` grades a single r-sma candidate across two
|
||
/// instruments — its stdout carries the per-instrument breakdown + the worst-case
|
||
/// floor + the sign-agreement, then the persisted CrossInstrument family handle.
|
||
/// Gated on local GER40/USDJPY data (the shared Sept-2024 window), skips cleanly
|
||
/// when the archive is absent.
|
||
#[test]
|
||
fn generalize_grades_a_candidate_across_two_instruments() {
|
||
// Ported onto a `[std::sweep, std::generalize]` campaign document
|
||
// (#319 Task 8) — the grade is the campaign's own native
|
||
// `generalizations[].generalization` record, byte-identical in
|
||
// computation to the retired `generalize_json` CLI wrapper's field set.
|
||
let (cwd, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&cwd, "generalize-shape-seed");
|
||
let proc_id = register_process_doc(&cwd, "gen.process.json", SWEEP_THEN_GENERALIZE_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "gen",
|
||
"data": {{ "instruments": ["GER40", "USDJPY"],
|
||
"windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [3] }},
|
||
"slow.length": {{ "kind": "I64", "values": [12] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 14, "k": 2.0 }} }} ],
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
from = 1725148800000i64,
|
||
to = 1727740799999i64,
|
||
);
|
||
write_doc(&cwd, "gen.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "gen.campaign.json"]);
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40/USDJPY data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {out}");
|
||
let record_line = out
|
||
.lines()
|
||
.find(|l| l.starts_with("{\"campaign_run\":"))
|
||
.expect("the always-on final campaign_run line");
|
||
let v: serde_json::Value = serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
|
||
let grade = &v["campaign_run"]["generalizations"][0]["generalization"];
|
||
assert!(grade.is_object(), "aggregate object: {record_line}");
|
||
assert_eq!(grade["n_instruments"].as_u64(), Some(2), "two instruments: {record_line}");
|
||
assert!(grade["worst_case"].is_number(), "worst_case present: {record_line}");
|
||
assert!(grade["sign_agreement"].is_number(), "sign_agreement present: {record_line}");
|
||
let per = grade["per_instrument"].as_array().expect("per_instrument array");
|
||
let instruments: Vec<&str> = per.iter().map(|p| p[0].as_str().unwrap()).collect();
|
||
assert!(instruments.contains(&"GER40") && instruments.contains(&"USDJPY"), "per-instrument breakdown: {record_line}");
|
||
}
|
||
|
||
/// Characterization pin (byte-identity anchor for the generalize dissolution,
|
||
/// #210), ported onto a `[std::sweep, std::generalize]` campaign document
|
||
/// (#319 Task 8): the current `aura generalize` bound its protective stop as
|
||
/// a grid axis; the dissolution rebinds the same stop through the
|
||
/// risk-regime seam (`RiskRegime::Vol -> StopRule::Vol`) — this pins the
|
||
/// EXACT R grade of the identical (blueprint, instruments, axes, stop,
|
||
/// window) coordinate, now realized through the campaign's own
|
||
/// `generalizations[].generalization` record rather than the retired CLI's
|
||
/// `generalize_json` wrapper. Gated on the shared GER40/USDJPY Sept-2024
|
||
/// archive; skips cleanly on a data refusal.
|
||
#[test]
|
||
fn generalize_real_e2e_pins_the_exact_current_grade() {
|
||
let (cwd, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&cwd, "generalize-pin-seed");
|
||
let proc_id = register_process_doc(&cwd, "genpin.process.json", SWEEP_THEN_GENERALIZE_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "genpin",
|
||
"data": {{ "instruments": ["GER40", "USDJPY"],
|
||
"windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [3] }},
|
||
"slow.length": {{ "kind": "I64", "values": [12] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 14, "k": 2.0 }} }} ],
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
from = 1725148800000i64,
|
||
to = 1727740799999i64,
|
||
);
|
||
write_doc(&cwd, "genpin.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "genpin.campaign.json"]);
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40/USDJPY data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {out}");
|
||
let record_line = out
|
||
.lines()
|
||
.find(|l| l.starts_with("{\"campaign_run\":"))
|
||
.expect("the always-on final campaign_run line");
|
||
let v: serde_json::Value = serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
|
||
let g = &v["campaign_run"]["generalizations"][0]["generalization"];
|
||
assert_eq!(g["selection_metric"].as_str(), Some("expectancy_r"), "metric: {record_line}");
|
||
assert_eq!(g["n_instruments"].as_u64(), Some(2), "two instruments: {record_line}");
|
||
assert_eq!(g["sign_agreement"].as_u64(), Some(2), "both agree in sign: {record_line}");
|
||
// EXACT R floats — the byte-identity anchor the dissolution must preserve.
|
||
assert_eq!(g["worst_case"].as_f64(), Some(0.005795903617609842), "worst-case R floor: {record_line}");
|
||
let per = g["per_instrument"].as_array().expect("per_instrument is an array");
|
||
assert_eq!(per.len(), 2, "two per-instrument grades: {record_line}");
|
||
assert_eq!(per[0][0].as_str(), Some("GER40"), "first instrument: {record_line}");
|
||
assert_eq!(per[0][1].as_f64(), Some(0.01056371324510624), "GER40 expectancy R: {record_line}");
|
||
assert_eq!(per[1][0].as_str(), Some("USDJPY"), "second instrument: {record_line}");
|
||
assert_eq!(per[1][1].as_f64(), Some(0.005795903617609842), "USDJPY expectancy R: {record_line}");
|
||
}
|
||
|
||
/// Property (#220 generalize vertical): `aura generalize` grades ANY sweepable
|
||
/// blueprint, not just the historical r-sma candidate — the whole point of
|
||
/// dropping `--strategy r-sma`/`--fast`/`--slow` for a positional blueprint +
|
||
/// generic `--axis`. Every other generalize E2E test here still exercises
|
||
/// `r_sma.json`, so a regression that silently special-cased r-sma paths
|
||
/// back in (e.g. resolving axes against a hardcoded `sma_signal.*` namespace)
|
||
/// would ship green there while breaking every other blueprint. This runs the
|
||
/// unrelated r-breakout candidate (the ganged `channel_length` axis, no
|
||
/// `sma_signal` node in sight) end-to-end and asserts a well-formed two-
|
||
/// instrument grade. Gated on the shared GER40/USDJPY Sept-2024 archive; skips
|
||
/// cleanly on a data refusal.
|
||
#[test]
|
||
fn generalize_grades_a_non_r_sma_blueprint() {
|
||
// Ported onto a `[std::sweep, std::generalize]` campaign document
|
||
// (#319 Task 8), same shape as `generalize_grades_a_candidate_across_
|
||
// two_instruments` but over the unrelated r-breakout candidate.
|
||
let (cwd, _g) = fresh_project();
|
||
let fixture = format!("{}/tests/fixtures/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
|
||
let bp_id = seed_blueprint_file(&cwd, &fixture, "breakout-gen-seed");
|
||
let proc_id = register_process_doc(&cwd, "breakoutgen.process.json", SWEEP_THEN_GENERALIZE_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "breakout-gen",
|
||
"data": {{ "instruments": ["GER40", "USDJPY"],
|
||
"windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "channel_length": {{ "kind": "I64", "values": [20] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 14, "k": 2.0 }} }} ],
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
from = 1725148800000i64,
|
||
to = 1727740799999i64,
|
||
);
|
||
write_doc(&cwd, "breakoutgen.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "breakoutgen.campaign.json"]);
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40/USDJPY data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {out}");
|
||
let record_line = out
|
||
.lines()
|
||
.find(|l| l.starts_with("{\"campaign_run\":"))
|
||
.expect("the always-on final campaign_run line");
|
||
let v: serde_json::Value = serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
|
||
let grade = &v["campaign_run"]["generalizations"][0]["generalization"];
|
||
assert!(grade.is_object(), "aggregate object: {record_line}");
|
||
assert_eq!(grade["n_instruments"].as_u64(), Some(2), "two instruments: {record_line}");
|
||
assert!(grade["worst_case"].is_number(), "worst_case present: {record_line}");
|
||
assert!(grade["sign_agreement"].is_number(), "sign_agreement present: {record_line}");
|
||
let per = grade["per_instrument"].as_array().expect("per_instrument array");
|
||
let instruments: Vec<&str> = per.iter().map(|p| p[0].as_str().unwrap()).collect();
|
||
assert!(instruments.contains(&"GER40") && instruments.contains(&"USDJPY"), "per-instrument breakdown: {record_line}");
|
||
}
|
||
|
||
/// Characterization pin (byte-identity anchor for the walkforward dissolution,
|
||
/// #210). The current `aura walkforward` runs its per-window IS-refit inline
|
||
/// (`walkforward_family` -> `select_winner` -> OOS run -> stitch/pool); the
|
||
/// dissolution reroutes the same rolling IS-sweep -> OOS-run -> deflation through
|
||
/// the campaign `std::walk_forward` stage, and must retain the OOS pip curve the
|
||
/// campaign runner currently discards (`oos_equity: vec![]`) to reproduce
|
||
/// `stitched_total_pips`, plus reconstruct pooled `oos_r`. The multi-point grid
|
||
/// (fast 3,5 x slow 12,20) makes the per-window winner selection non-degenerate --
|
||
/// `param_stability` varies across windows -- so a winner-pick divergence between
|
||
/// the inline and campaign refit cannot ship green. This pins the EXACT current
|
||
/// grade of the identical invocation over a fixed 2025 GER40 window; after the
|
||
/// dissolution the same command must reproduce these bytes (the acceptance gate).
|
||
/// Gated on the shared GER40 archive; skips cleanly on a data refusal.
|
||
#[test]
|
||
fn walkforward_real_e2e_pins_the_exact_current_grade() {
|
||
// Ported onto a `[std::grid, std::walk_forward]` campaign document
|
||
// (#319 Task 8): the fixed real-archive roller (`aura_backtest::
|
||
// wf_ms_sizes`, 90d IS / 30d OOS / 30d step — the retired sugar's
|
||
// `fit_wf_ms_sizes` fast-path, since this ~1-year window comfortably
|
||
// exceeds IS+OOS) over the archive's OWN clipped coverage of the
|
||
// requested 2025 span (discovered via a preliminary run's `coverage`
|
||
// field: the campaign path clips `--from`/`--to` to the actual archive
|
||
// bounds internally, exactly as the retired CLI's `campaign_window_ms`
|
||
// did). The `{"walkforward":…}` aggregate itself no longer exists as a
|
||
// CLI output (deleted Task 8 Step 2); it is reconstructed by
|
||
// `walkforward_aggregate_from_reports` from the family's own persisted
|
||
// per-window reports — the same reduction, over data the campaign path
|
||
// populates identically, so it reproduces the anchor byte-for-byte.
|
||
let (cwd, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&cwd, "wf-pin-seed");
|
||
let proc_id = register_process_doc(&cwd, "wfpin.process.json", GRID_THEN_WF_PROCESS_DOC_90_30_30);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "wfpin",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1735776899999, "to_ms": 1767128220000 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [3, 5] }},
|
||
"slow.length": {{ "kind": "I64", "values": [12, 20] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 14, "k": 2.0 }} }} ],
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "wfpin.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "wfpin.campaign.json"]);
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40 data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {out}");
|
||
|
||
let id = family_id_of_kind(&cwd, "WalkForward");
|
||
let (fam_out, fam_code) = run_code_in(&cwd, &["runs", "family", &id]);
|
||
assert_eq!(fam_code, Some(0), "runs family exit: {fam_out}");
|
||
let reports: Vec<serde_json::Value> = fam_out
|
||
.lines()
|
||
.filter(|l| l.starts_with('{'))
|
||
.map(|l| serde_json::from_str(l).expect("member line parses as JSON"))
|
||
.collect();
|
||
let wf = walkforward_aggregate_from_reports(&reports, &["fast.length", "slow.length"]);
|
||
assert_eq!(wf["windows"].as_u64(), Some(9), "window count: {wf}");
|
||
// EXACT floats -- the byte-identity anchor the dissolution must preserve.
|
||
assert_eq!(wf["stitched_total_pips"].as_f64(), Some(-1039.8606666650755), "stitched OOS pips: {wf}");
|
||
let r = &wf["oos_r"];
|
||
assert_eq!(r["n_trades"].as_u64(), Some(20681), "pooled OOS trade count: {wf}");
|
||
// expectancy_r is reconstructed as a trade-count-weighted mean of each
|
||
// window's own expectancy_r (RunReport.metrics.r.net_trade_rs, the raw
|
||
// per-trade series the direct pooled mean would use, is `#[serde(skip)]`
|
||
// and unrecoverable from JSON) — mathematically identical to the direct
|
||
// pooled mean, but floating-point summation is not associative, so this
|
||
// reconstruction differs from the anchor at the ULP level (~5e-17
|
||
// relative) rather than bit-for-bit; every other field above (window
|
||
// count, stitched pips, trade count, param-stability means) matches
|
||
// exactly, confirming the underlying computation is unchanged.
|
||
let expectancy_r = r["expectancy_r"].as_f64().expect("expectancy_r is an f64");
|
||
assert!(
|
||
(expectancy_r - (-0.002397100685333715)).abs() < 1e-12,
|
||
"pooled OOS expectancy R (within float-reconstruction tolerance): {wf}"
|
||
);
|
||
let ps = wf["param_stability"].as_array().expect("param_stability is an array");
|
||
assert_eq!(ps[0]["mean"].as_f64(), Some(3.888888888888889), "fast-MA refit mean: {wf}");
|
||
assert_eq!(ps[1]["mean"].as_f64(), Some(17.333333333333332), "slow-MA refit mean: {wf}");
|
||
}
|
||
|
||
/// Property (#220 walkforward vertical): `aura walkforward --real` IS-refits ANY
|
||
/// sweepable blueprint through the campaign path, not just the historical r-sma
|
||
/// candidate — the whole point of dropping `--strategy r-sma`/`--fast`/`--slow`
|
||
/// for a positional blueprint + generic `--axis`. Every other walkforward E2E
|
||
/// test here still exercises `r_sma.json`; a regression that silently
|
||
/// special-cased the refit's axis lookup back to a hardcoded `sma_signal.*`
|
||
/// namespace (e.g. in `walkforward_summary_json_from_reports`'s per-axis
|
||
/// `raw_matches_wrapped` search) would ship green there while breaking every
|
||
/// other blueprint. This runs the unrelated r-breakout candidate
|
||
/// (the ganged `channel_length` axis, no `sma_signal` node in sight) end-to-end
|
||
/// and asserts a well-formed multi-window summary whose `param_stability` has
|
||
/// exactly one entry per bound axis (1 ganged blueprint axis + the 2 stop columns).
|
||
/// The window count (9) is roller/data-derived, not strategy-derived, so it is
|
||
/// pinned exactly against the same GER40 2025 span the r-sma anchor uses.
|
||
/// Gated on the shared GER40 archive; skips cleanly on a data refusal.
|
||
#[test]
|
||
fn walkforward_dissolves_a_non_r_sma_blueprint() {
|
||
// Ported onto a `[std::grid, std::walk_forward]` campaign document
|
||
// (#319 Task 8), same fixed 90/30/30 roller + clipped window as
|
||
// `walkforward_real_e2e_pins_the_exact_current_grade`, over the unrelated
|
||
// r-breakout candidate.
|
||
let (cwd, _g) = fresh_project();
|
||
let fixture = format!("{}/tests/fixtures/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
|
||
let bp_id = seed_blueprint_file(&cwd, &fixture, "breakout-wfpin-seed");
|
||
let proc_id = register_process_doc(&cwd, "breakoutwfpin.process.json", GRID_THEN_WF_PROCESS_DOC_90_30_30);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "breakoutwfpin",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1735776899999, "to_ms": 1767128220000 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "channel_length": {{ "kind": "I64", "values": [10, 20] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 14, "k": 2.0 }} }} ],
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "breakoutwfpin.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "breakoutwfpin.campaign.json"]);
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40 data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {out}");
|
||
|
||
let id = family_id_of_kind(&cwd, "WalkForward");
|
||
let (fam_out, fam_code) = run_code_in(&cwd, &["runs", "family", &id]);
|
||
assert_eq!(fam_code, Some(0), "runs family exit: {fam_out}");
|
||
let reports: Vec<serde_json::Value> = fam_out
|
||
.lines()
|
||
.filter(|l| l.starts_with('{'))
|
||
.map(|l| serde_json::from_str(l).expect("member line parses as JSON"))
|
||
.collect();
|
||
let wf = walkforward_aggregate_from_reports(&reports, &["channel_length", "stop_length", "stop_k"]);
|
||
assert_eq!(wf["windows"].as_u64(), Some(9), "window count (data/roller-derived): {wf}");
|
||
assert!(wf["oos_r"]["n_trades"].as_u64().is_some(), "pooled OOS trade count present: {wf}");
|
||
let ps = wf["param_stability"].as_array().expect("param_stability is an array");
|
||
assert_eq!(ps.len(), 3, "one entry per bound axis (channel_length, stop_length, stop_k): {wf}");
|
||
}
|
||
|
||
|
||
|
||
/// Characterization pin (#215): the same identical invocation as
|
||
/// `walkforward_real_e2e_pins_the_exact_current_grade` (fixture, real
|
||
/// instrument, axes, stop, window), but selecting `plateau:worst` instead of
|
||
/// the default argmax. Plateau scores the closed neighbourhood over the full
|
||
/// IS grid rather than picking the single best point, so its winner (and
|
||
/// therefore its OOS stitch) legitimately differs from the argmax anchor;
|
||
/// this pins that divergence exactly, so a regression that silently
|
||
/// collapsed plateau back to argmax (or vice versa) inside
|
||
/// `run_walk_forward_stage`'s `select_sweep_winner` dispatch goes red here
|
||
/// even though the argmax anchor stays green. Gated on the shared GER40
|
||
/// archive; skips cleanly on a data refusal.
|
||
#[test]
|
||
fn walkforward_real_e2e_pins_the_exact_current_plateau_grade() {
|
||
// Ported onto a `[std::grid, std::walk_forward(select=plateau:worst)]`
|
||
// campaign document (#319 Task 8), same fixed 90/30/30 roller + clipped
|
||
// window as the argmax anchor above.
|
||
let (cwd, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&cwd, "wf-plateau-pin-seed");
|
||
let plateau_process_doc = r#"{
|
||
"format_version": 1,
|
||
"kind": "process",
|
||
"name": "grid-then-walkforward-plateau",
|
||
"pipeline": [
|
||
{ "block": "std::grid" },
|
||
{ "block": "std::walk_forward", "in_sample_ms": 7776000000, "out_of_sample_ms": 2592000000,
|
||
"step_ms": 2592000000, "mode": "rolling", "metric": "sqn_normalized", "select": "plateau:worst" }
|
||
]
|
||
}"#;
|
||
let proc_id = register_process_doc(&cwd, "wfplateau.process.json", plateau_process_doc);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "wfplateau",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1735776899999, "to_ms": 1767128220000 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [3, 5] }},
|
||
"slow.length": {{ "kind": "I64", "values": [12, 20] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 14, "k": 2.0 }} }} ],
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "wfplateau.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "wfplateau.campaign.json"]);
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40 data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {out}");
|
||
|
||
let id = family_id_of_kind(&cwd, "WalkForward");
|
||
let (fam_out, fam_code) = run_code_in(&cwd, &["runs", "family", &id]);
|
||
assert_eq!(fam_code, Some(0), "runs family exit: {fam_out}");
|
||
let reports: Vec<serde_json::Value> = fam_out
|
||
.lines()
|
||
.filter(|l| l.starts_with('{'))
|
||
.map(|l| serde_json::from_str(l).expect("member line parses as JSON"))
|
||
.collect();
|
||
let wf = walkforward_aggregate_from_reports(&reports, &["fast.length", "slow.length"]);
|
||
assert_eq!(wf["windows"].as_u64(), Some(9), "window count: {wf}");
|
||
// EXACT floats -- the byte-identity anchor a plateau-vs-argmax regression must preserve.
|
||
assert_eq!(wf["stitched_total_pips"].as_f64(), Some(-968.3776666637464), "stitched OOS pips: {wf}");
|
||
let r = &wf["oos_r"];
|
||
assert_eq!(r["n_trades"].as_u64(), Some(20667), "pooled OOS trade count: {wf}");
|
||
// See the argmax anchor's doc comment: expectancy_r is a reconstructed
|
||
// trade-count-weighted mean (raw net_trade_rs is unrecoverable from
|
||
// JSON), so it is compared within a tight float-reconstruction tolerance
|
||
// rather than bit-for-bit.
|
||
let expectancy_r = r["expectancy_r"].as_f64().expect("expectancy_r is an f64");
|
||
assert!(
|
||
(expectancy_r - 0.007635120926372627).abs() < 1e-12,
|
||
"pooled OOS expectancy R (within float-reconstruction tolerance): {wf}"
|
||
);
|
||
let ps = wf["param_stability"].as_array().expect("param_stability is an array");
|
||
assert_eq!(ps[0]["mean"].as_f64(), Some(4.777777777777778), "fast-MA refit mean: {wf}");
|
||
assert_eq!(ps[1]["mean"].as_f64(), Some(16.444444444444443), "slow-MA refit mean: {wf}");
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
// Note (#319 Task 8, disposition finding): `generalize_without_explicit_
|
||
// window_resolves_a_shared_window_and_completes` and `generalize_without_
|
||
// explicit_window_resolves_the_intersection_of_all_symbols` retired rather
|
||
// than ported. Both pinned the retired `dispatch_generalize`'s OWN no-window
|
||
// fallback (`campaign_window_ms` + `intersect_shared_window`, both deleted
|
||
// Task 8 Step 2): resolving a shared campaign window automatically from the
|
||
// listed symbols' archive geometry when `--from`/`--to` are omitted. A
|
||
// hand-authored `CampaignDoc` has no such fallback — `data.windows` is a
|
||
// required field the schema always demands explicitly (confirmed:
|
||
// `validate_campaign` never treats a missing window as "resolve one"); there
|
||
// is no campaign-document capability this ports onto, so this is pure
|
||
// argv-translator machinery, not a campaign/engine property. The second
|
||
// test's own disposition note ("window-resolution halves retire; the
|
||
// cross-instrument grading property ports") already anticipated this split —
|
||
// the surviving "generalize grades N instruments over an explicit shared
|
||
// window" property stays fully covered by `generalize_grades_a_candidate_
|
||
// across_two_instruments` and its siblings above.
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/// Property, ported onto a `[std::sweep, std::generalize]` campaign document
|
||
/// (#319 Task 8 — disposition finding): a successful campaign generalize run
|
||
/// persists its per-instrument runs as discoverable, but NOT as a merged
|
||
/// `CrossInstrument` family the way the retired sugar did.
|
||
/// `aura-campaign::exec`'s `Generalize` stage handling never calls
|
||
/// `append_family` (only its `Sweep`/`WalkForward` siblings do — grepped:
|
||
/// the crate's only two `append_family` call sites) — the grade lives solely
|
||
/// in the `campaign_runs.jsonl` record's `generalizations` field. What DOES
|
||
/// survive discoverably: exactly one `Sweep` family PER INSTRUMENT (each
|
||
/// cell's own candidate run, one member), and the generalize grade itself,
|
||
/// readable back via `aura campaign runs <id>` (or the always-on final
|
||
/// stdout line) carrying `n_instruments`. This pins the adapted-but-real
|
||
/// discoverability the campaign path actually provides. Gated on local
|
||
/// GER40/USDJPY data (the shared Sept-2024 window); skips cleanly when absent.
|
||
#[test]
|
||
fn generalize_persists_a_discoverable_cross_instrument_family() {
|
||
let (cwd, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&cwd, "gen-persist-seed");
|
||
let proc_id = register_process_doc(&cwd, "genpersist.process.json", SWEEP_THEN_GENERALIZE_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "genpersist",
|
||
"data": {{ "instruments": ["GER40", "USDJPY"],
|
||
"windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [3] }},
|
||
"slow.length": {{ "kind": "I64", "values": [12] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 14, "k": 2.0 }} }} ],
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
from = 1725148800000i64,
|
||
to = 1727740799999i64,
|
||
);
|
||
write_doc(&cwd, "genpersist.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "genpersist.campaign.json"]);
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40/USDJPY data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {out}");
|
||
|
||
let record_line = out
|
||
.lines()
|
||
.find(|l| l.starts_with("{\"campaign_run\":"))
|
||
.expect("the always-on final campaign_run line");
|
||
let v: serde_json::Value = serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
|
||
assert_eq!(
|
||
v["campaign_run"]["generalizations"][0]["generalization"]["n_instruments"].as_u64(), Some(2),
|
||
"the generalize grade is discoverable in the campaign-run record: {record_line}"
|
||
);
|
||
|
||
let (fams_out, fams_code) = run_code_in(&cwd, &["runs", "families"]);
|
||
assert_eq!(fams_code, Some(0), "families exit: {fams_out}");
|
||
let sweeps: Vec<&str> = fams_out.lines().filter(|l| l.contains("\"kind\":\"Sweep\"")).collect();
|
||
assert_eq!(sweeps.len(), 2, "one durable per-instrument Sweep family: {fams_out:?}");
|
||
assert!(sweeps.iter().all(|l| l.contains("\"members\":1")), "one member per instrument cell: {fams_out:?}");
|
||
}
|
||
|
||
/// Property (#210 T3, dissolution), ported onto a `[std::sweep, std::
|
||
/// generalize]` campaign document (#319 Task 8 — same disposition finding as
|
||
/// the sibling above): the per-instrument grades are attributed to the RIGHT
|
||
/// instrument in the RIGHT order — not merely correct in count. Reads
|
||
/// `generalizations[0].generalization.per_instrument` (order-preserving)
|
||
/// directly off the campaign-run record rather than `runs family
|
||
/// generalize-0` (no such merged family exists on the campaign path). Asserts
|
||
/// GER40 first with the exact-grade anchor's GER40 `expectancy_r`, then
|
||
/// USDJPY with its own. Gated on the local GER40/USDJPY data.
|
||
#[test]
|
||
fn generalize_family_members_are_attributed_to_the_right_instrument() {
|
||
let (cwd, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&cwd, "gen-attrib-seed");
|
||
let proc_id = register_process_doc(&cwd, "genattrib.process.json", SWEEP_THEN_GENERALIZE_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "genattrib",
|
||
"data": {{ "instruments": ["GER40", "USDJPY"],
|
||
"windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [3] }},
|
||
"slow.length": {{ "kind": "I64", "values": [12] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 14, "k": 2.0 }} }} ],
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
from = 1725148800000i64,
|
||
to = 1727740799999i64,
|
||
);
|
||
write_doc(&cwd, "genattrib.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "genattrib.campaign.json"]);
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40/USDJPY data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {out}");
|
||
|
||
let record_line = out
|
||
.lines()
|
||
.find(|l| l.starts_with("{\"campaign_run\":"))
|
||
.expect("the always-on final campaign_run line");
|
||
let v: serde_json::Value = serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
|
||
let per = v["campaign_run"]["generalizations"][0]["generalization"]["per_instrument"]
|
||
.as_array()
|
||
.expect("per_instrument is an array");
|
||
assert_eq!(per.len(), 2, "one entry per instrument: {record_line}");
|
||
assert_eq!(per[0][0].as_str(), Some("GER40"), "first entry is GER40: {record_line}");
|
||
assert_eq!(
|
||
per[0][1].as_f64(), Some(0.01056371324510624),
|
||
"GER40's own expectancy_r, matching the exact-grade anchor: {record_line}"
|
||
);
|
||
assert_eq!(per[1][0].as_str(), Some("USDJPY"), "second entry is USDJPY: {record_line}");
|
||
assert_eq!(
|
||
per[1][1].as_f64(), Some(0.005795903617609842),
|
||
"USDJPY's own expectancy_r, matching the exact-grade anchor: {record_line}"
|
||
);
|
||
}
|
||
|
||
/// Property (#218 migration follow-up), ported onto a `[std::sweep, std::
|
||
/// generalize]` campaign document (#319 Task 8 — same disposition finding):
|
||
/// `generalize`'s per-instrument members route through the shared campaign
|
||
/// member runner, which stamps `manifest.project` with the running project's
|
||
/// provenance (C18/C16) for every persisted member. Reads the provenance off
|
||
/// the two per-instrument Sweep families (see the sibling tests' doc comments
|
||
/// for why no merged "generalize-0" family exists on the campaign path).
|
||
/// Gated on the shared GER40/USDJPY Sept-2024 archive.
|
||
#[test]
|
||
fn generalize_family_members_stamp_project_provenance() {
|
||
let (cwd, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&cwd, "gen-provenance-seed");
|
||
let proc_id = register_process_doc(&cwd, "genprovenance.process.json", SWEEP_THEN_GENERALIZE_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "genprovenance",
|
||
"data": {{ "instruments": ["GER40", "USDJPY"],
|
||
"windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [3] }},
|
||
"slow.length": {{ "kind": "I64", "values": [12] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 14, "k": 2.0 }} }} ],
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
from = 1725148800000i64,
|
||
to = 1727740799999i64,
|
||
);
|
||
write_doc(&cwd, "genprovenance.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "genprovenance.campaign.json"]);
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40/USDJPY data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {out}");
|
||
|
||
let (fams_out, fams_code) = run_code_in(&cwd, &["runs", "families"]);
|
||
assert_eq!(fams_code, Some(0), "families exit: {fams_out}");
|
||
let sweep_ids: Vec<String> = fams_out
|
||
.lines()
|
||
.filter(|l| l.contains("\"kind\":\"Sweep\""))
|
||
.map(|l| {
|
||
let v: serde_json::Value = serde_json::from_str(l).expect("family line parses");
|
||
v["family_id"].as_str().expect("family_id").to_string()
|
||
})
|
||
.collect();
|
||
assert_eq!(sweep_ids.len(), 2, "one per-instrument Sweep family: {fams_out:?}");
|
||
for id in &sweep_ids {
|
||
let (fam_out, fam_code) = run_code_in(&cwd, &["runs", "family", id]);
|
||
assert_eq!(fam_code, Some(0), "runs family exit: {fam_out}");
|
||
let member: serde_json::Value = serde_json::from_str(fam_out.lines().next().expect("one member line"))
|
||
.expect("member line parses as JSON");
|
||
let project = &member["manifest"]["project"];
|
||
assert_eq!(
|
||
project["namespace"].as_str(), Some("demo"),
|
||
"campaign-path member manifest carries the fixture project's namespace: {fam_out}"
|
||
);
|
||
let sha = project["dylib_sha256"].as_str()
|
||
.unwrap_or_else(|| panic!("dylib_sha256 present: {fam_out}"));
|
||
assert_eq!(sha.len(), 64, "dylib_sha256 is a full sha256 hex digest: {fam_out}");
|
||
}
|
||
}
|
||
|
||
/// `aura sweep <blueprint.json> --axis <name>=<csv> …` (#166): sweeps a loaded CLOSED
|
||
/// blueprint over its named bound-override axes and persists a discoverable Sweep family.
|
||
/// Both bound knobs (fast/slow) must be re-opened by axes; the 2×2 grid yields 4 members.
|
||
#[test]
|
||
fn aura_sweep_loads_a_blueprint_and_persists_a_sweep_family() {
|
||
// Ported onto a one-cell campaign document over SYMA (#319 Task 8 — there
|
||
// is no campaign-document equivalent of "sweep the embedded showcase
|
||
// stream with no instrument", invariant 7); both bound knobs (fast/slow)
|
||
// re-opened by axes, the 2x2 grid yields 4 members.
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let bp_id = seed_blueprint(&cwd, "blueprint-sweep-seed");
|
||
let proc_id = register_process_doc(&cwd, "f.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc("f", "SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999), "2, 4", "8, 16");
|
||
write_doc(&cwd, "f.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "f.campaign.json"]);
|
||
assert_eq!(code, Some(0), "exec exit: {out}");
|
||
let (fo, fams_code) = run_code_in(&cwd, &["runs", "families"]);
|
||
assert_eq!(fams_code, Some(0), "families exit: {fo}");
|
||
assert!(fo.contains("\"kind\":\"Sweep\""), "families: {fo}");
|
||
assert!(fo.contains("\"members\":4"), "2×2 grid -> four members: {fo}");
|
||
}
|
||
|
||
|
||
/// Property (#158/#164, C18/C11/C12): a blueprint sweep content-addresses its topology
|
||
/// — the canonical blueprint is persisted ONCE under `runs/blueprints/<topology_hash>.json`,
|
||
/// keyed by the very SHA256 every member's manifest carries, holding exactly the bytes
|
||
/// `blueprint_to_json(blueprint_from_json(doc))` yields. That stored topology is what makes
|
||
/// a generated sweep family reproducible from disk (the `aura reproduce` re-derivation).
|
||
#[test]
|
||
fn aura_sweep_persists_the_canonical_blueprint_keyed_by_topology_hash() {
|
||
use aura_engine::{blueprint_from_json, blueprint_to_json};
|
||
use aura_vocabulary::std_vocabulary;
|
||
use sha2::{Digest, Sha256};
|
||
|
||
// Ported onto a one-cell campaign document over SYMA (#319 Task 8, same
|
||
// rationale as the sibling above).
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
||
let bp_id = seed_blueprint(&cwd, "blueprint-sweep-store-seed");
|
||
let proc_id = register_process_doc(&cwd, "f.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc("f", "SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999), "2, 4", "8, 16");
|
||
write_doc(&cwd, "f.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "f.campaign.json"]);
|
||
assert_eq!(code, Some(0), "exec exit: {out}");
|
||
|
||
// exactly one blueprint stored for the whole family (one topology per family).
|
||
let store = cwd.join("runs/blueprints");
|
||
let mut entries: Vec<_> = std::fs::read_dir(&store)
|
||
.unwrap_or_else(|e| panic!("the sweep must create the blueprint store {store:?}: {e}"))
|
||
.map(|e| e.expect("dir entry").path())
|
||
.collect();
|
||
entries.sort();
|
||
assert_eq!(entries.len(), 1, "one stored topology per family: {entries:?}");
|
||
|
||
// keyed by a 64-hex topology hash, holding the canonical re-serialization.
|
||
let stored_path = &entries[0];
|
||
let stem = stored_path.file_stem().expect("stem").to_str().expect("utf-8");
|
||
assert_eq!(stem.len(), 64, "content id is a 64-hex SHA256: {stem}");
|
||
assert!(stem.bytes().all(|b| b.is_ascii_hexdigit()), "content id is hex: {stem}");
|
||
|
||
let doc = std::fs::read_to_string(&fixture).expect("read fixture");
|
||
let expected = blueprint_to_json(&blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads"))
|
||
.expect("re-serializes");
|
||
let stored = std::fs::read_to_string(stored_path).expect("read stored blueprint");
|
||
assert_eq!(stored, expected, "the store holds the canonical blueprint bytes");
|
||
|
||
// content-addressing, the feature's load-bearing property: the key IS the SHA256 of
|
||
// the stored bytes — `get_blueprint(hash)` addresses exactly these bytes by identity,
|
||
// not by coincidence of the store and the members computing the hash separately.
|
||
let content_id: String =
|
||
Sha256::digest(stored.as_bytes()).iter().map(|b| format!("{b:02x}")).collect();
|
||
assert_eq!(stem, content_id, "the store key is the SHA256 of its content");
|
||
|
||
// and that key is exactly what every family member's manifest carries — the hash the
|
||
// reproduce path reads back to fetch the topology. All 4 members share the one topology.
|
||
let members = std::fs::read_to_string(cwd.join("runs/families.jsonl")).expect("read family store");
|
||
let carried = members.matches(&format!("\"topology_hash\":\"{stem}\"")).count();
|
||
assert_eq!(carried, 4, "every member records the store key as its topology_hash: {members}");
|
||
|
||
let _ = std::fs::remove_dir_all(&cwd);
|
||
}
|
||
|
||
/// An `--axis` name that does not resolve against the loaded blueprint's param_space
|
||
/// fails clean (a named BindError, non-zero exit), not a panic. The CLOSED fixture has
|
||
/// bound-override knobs, so a nonsense axis name (`nope`) is genuinely unknown — it names
|
||
/// neither a re-openable bound param nor an already-open knob (#246: a bound name would
|
||
/// re-open instead).
|
||
#[test]
|
||
fn aura_sweep_rejects_an_unknown_axis() {
|
||
// Ported onto a one-cell campaign document over SYMA (#319 Task 8).
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let bp_id = seed_blueprint(&cwd, "unknown-axis-seed");
|
||
let proc_id = register_process_doc(&cwd, "unknownaxis.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "f",
|
||
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": 1709251200000, "to_ms": 1719791999999 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "nope": {{ "kind": "I64", "values": [1, 2] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "unknownaxis.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "unknownaxis.campaign.json"]);
|
||
assert_ne!(code, Some(0), "an unknown axis must fail the campaign run: {out}");
|
||
assert!(
|
||
out.to_lowercase().contains("nope"),
|
||
"names the unresolved axis: {out}"
|
||
);
|
||
}
|
||
|
||
/// Property (#158, C18 "re-derives full results on demand"): `aura reproduce <family-id>`
|
||
/// re-derives every member of a persisted sweep family from the content-addressed
|
||
/// blueprint store and reports each bit-identical, exiting 0. Drives the verb through the
|
||
/// binary — the `reproduce_family` stdout contract (a per-member `bit-identical` line plus
|
||
/// a `reproduced N/N` summary) that the `reproduce_family_in` unit seam bypasses.
|
||
#[test]
|
||
fn aura_reproduce_re_derives_a_persisted_sweep_family() {
|
||
// Ported onto a one-cell campaign document over SYMA (#319 Task 8, same
|
||
// rationale as the sweep-family siblings above); the family_id is read
|
||
// back from `runs families`, not assumed to be the literal "f-0".
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let bp_id = seed_blueprint(&cwd, "blueprint-reproduce-seed");
|
||
let proc_id = register_process_doc(&cwd, "f.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc("f", "SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999), "2, 4", "8, 16");
|
||
write_doc(&cwd, "f.campaign.json", &doc);
|
||
let (sweep_out, sweep_code) = run_code_in(&cwd, &["exec", "f.campaign.json"]);
|
||
assert_eq!(sweep_code, Some(0), "sweep exit: {sweep_out}");
|
||
|
||
let id = family_id_of_kind(&cwd, "Sweep");
|
||
let (out, code) = run_code_in(&cwd, &["reproduce", &id]);
|
||
assert_eq!(code, Some(0), "reproduce exit: {out}");
|
||
// one verdict line per member (all bit-identical) + the summary; the label renders
|
||
// param values portably (`length=2`, not the `I64(2)` Debug form).
|
||
assert_eq!(
|
||
out.lines().filter(|l| l.contains("reproduced: bit-identical")).count(),
|
||
4,
|
||
"every member re-derives bit-identically: {out}"
|
||
);
|
||
assert!(out.contains("fast.length=2"), "label renders values portably: {out}");
|
||
assert!(!out.contains("I64("), "label must not leak the Scalar Debug form: {out}");
|
||
assert!(out.contains("reproduced 4/4 members bit-identically"), "summary line: {out}");
|
||
}
|
||
|
||
/// Property (C1/C18 integrity net): when a persisted member's stored metrics no longer
|
||
/// match its re-derivation, `aura reproduce` reports that member `DIVERGED` and exits 1
|
||
/// — the non-happy branch of the verb. Forced by tampering one member's on-disk
|
||
/// `total_pips` after the sweep (the store's bit-identical compare IS the integrity check).
|
||
#[test]
|
||
fn aura_reproduce_reports_a_diverged_member_and_exits_one() {
|
||
// Ported onto a one-cell campaign document over SYMA (#319 Task 8).
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let bp_id = seed_blueprint(&cwd, "blueprint-reproduce-diverge-seed");
|
||
let proc_id = register_process_doc(&cwd, "f.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc("f", "SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999), "2, 4", "8, 16");
|
||
write_doc(&cwd, "f.campaign.json", &doc);
|
||
let (sweep_out, sweep_code) = run_code_in(&cwd, &["exec", "f.campaign.json"]);
|
||
assert_eq!(sweep_code, Some(0), "sweep exit: {sweep_out}");
|
||
|
||
// corrupt the first member's stored `total_pips` so its re-run metrics differ.
|
||
let store = cwd.join("runs/families.jsonl");
|
||
let s = std::fs::read_to_string(&store).expect("read family store");
|
||
let key = "\"total_pips\":";
|
||
let at = s.find(key).expect("a member records total_pips") + key.len();
|
||
let end = at + s[at..].find(',').expect("total_pips value terminates");
|
||
let corrupted = format!("{}999.0{}", &s[..at], &s[end..]);
|
||
std::fs::write(&store, corrupted).expect("write corrupted store");
|
||
|
||
let id = family_id_of_kind(&cwd, "Sweep");
|
||
let (out, code) = run_code_in(&cwd, &["reproduce", &id]);
|
||
assert_eq!(code, Some(1), "a diverged member exits 1: {out}");
|
||
assert!(out.contains("reproduced: DIVERGED"), "the tampered member is reported DIVERGED: {out}");
|
||
assert!(out.contains("reproduced 3/4 members bit-identically"), "summary counts the diverged member: {out}");
|
||
}
|
||
|
||
/// E2E (acc 2): persist a blueprint sweep, then `aura reproduce <id>` re-derives every
|
||
/// member bit-identically from the content-addressed store and exits 0.
|
||
#[test]
|
||
fn aura_reproduce_re_derives_a_persisted_sweep_bit_identically() {
|
||
// Ported onto a one-cell campaign document over SYMA (#319 Task 8).
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let bp_id = seed_blueprint(&cwd, "reproduce-roundtrip-seed");
|
||
let proc_id = register_process_doc(&cwd, "smacross.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc(
|
||
"smacross", "SYMA", &bp_id, &proc_id,
|
||
(1709251200000, 1719791999999), "2", "4, 6",
|
||
);
|
||
write_doc(&cwd, "smacross.campaign.json", &doc);
|
||
let (sweep_out, sweep_code) = run_code_in(&cwd, &["exec", "smacross.campaign.json"]);
|
||
assert_eq!(sweep_code, Some(0), "sweep exit: {sweep_out}");
|
||
|
||
let id = family_id_of_kind(&cwd, "Sweep");
|
||
let (out, code) = run_code_in(&cwd, &["reproduce", &id]);
|
||
assert_eq!(code, Some(0), "reproduce exits 0: {out}");
|
||
assert!(
|
||
out.contains("reproduced 2/2 members bit-identically"),
|
||
"every member re-derives: {out}"
|
||
);
|
||
}
|
||
|
||
// Note (#319 Task 8, disposition finding): `aura_mc_over_a_blueprint_
|
||
// reproduces_bit_identically` retired rather than ported. `aura mc <closed
|
||
// blueprint.json> --seeds N` drove `aura-runner::family::blueprint_mc_
|
||
// family` — a per-RANDOM-SEED synthetic family (each member draws a distinct
|
||
// synthetic price walk from the SAME fixed blueprint), a wholly different
|
||
// axis of variation than anything a `CampaignDoc` models (strategy params /
|
||
// instrument / window — never "vary only the seed, same everything else").
|
||
// There is no document field to bind a seed axis onto, and the plan's own
|
||
// Task 9 ("aura-runner family-builder retirement") schedules `blueprint_mc_
|
||
// family` for deletion as a builder with no surviving CLI entry point — this
|
||
// property is not "translated", it is retired along with the builder.
|
||
// Reported to the orchestrator rather than fabricated onto an unrelated
|
||
// substitute.
|
||
|
||
// Note (#319 Task 8, disposition finding): `aura_mc_synthetic_member_panic_
|
||
// is_contained` retired for the same reason (the `mc --seeds` poison-member
|
||
// containment test rides the same retiring `blueprint_mc_family` seam); the
|
||
// general "member-compile faults are contained identically across every
|
||
// verb/path" property stays fully covered by its ported
|
||
// `aura_walkforward_synthetic_member_panic_is_contained` /
|
||
// `aura_sweep_synthetic_member_panic_is_contained` siblings below plus the
|
||
// real-data #272 containment tests in `research_docs.rs`.
|
||
|
||
/// E2E (#173): an IS-refit walk-forward over a loaded blueprint persists a
|
||
/// content-addressed WalkForward family that `aura reproduce`s bit-identically —
|
||
/// each OOS member's windowed slice + winner params are recovered from its manifest.
|
||
/// The synthetic walk-forward route's own `--axis` intake is now RAW (this
|
||
/// cycle's tidy fix, #328 batch 2).
|
||
#[test]
|
||
fn aura_walkforward_over_a_blueprint_reproduces_bit_identically() {
|
||
// Ported onto a `[std::grid, std::walk_forward]` campaign document over
|
||
// SYMA (#319 Task 8, same substitution rationale as
|
||
// `reproduce_family_with_overridden_bound_param_re_derives`): the OOS
|
||
// member count is roller/data-derived, so it is read back rather than
|
||
// pinned to the translator-era literal (3).
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let bp_id = seed_blueprint(&cwd, "wfr-seed");
|
||
let proc_id = register_process_doc(&cwd, "wfr.process.json", GRID_THEN_WF_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "wfr",
|
||
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": 1709251200000, "to_ms": 1719791999999 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2, 3] }},
|
||
"slow.length": {{ "kind": "I64", "values": [4, 6] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "wfr.campaign.json", &doc);
|
||
let (run_out, run_code) = run_code_in(&cwd, &["exec", "wfr.campaign.json"]);
|
||
assert_eq!(run_code, Some(0), "stderr: {run_out}");
|
||
let member_count = run_out.lines().filter(|l| l.starts_with("{\"family_id\":")).count();
|
||
assert!(member_count > 0, "at least one OOS member: {run_out}");
|
||
|
||
let id = family_id_of_kind(&cwd, "WalkForward");
|
||
let (out, code) = run_code_in(&cwd, &["reproduce", &id]);
|
||
assert_eq!(code, Some(0), "out: {out}");
|
||
assert!(
|
||
out.contains(&format!("reproduced {member_count}/{member_count} members bit-identically")),
|
||
"out: {out}"
|
||
);
|
||
}
|
||
|
||
/// E2E (#173), ported onto a `[std::grid, std::walk_forward]` campaign
|
||
/// document over SYMA (#319 Task 8): the loaded-blueprint walk-forward runs +
|
||
/// persists a WalkForward family and stores exactly one content-addressed
|
||
/// blueprint. The `{"walkforward":…}` aggregate-JSON summary carrying
|
||
/// `oos_r`/`stitched_total_pips`/`param_stability` was the retired CLI
|
||
/// verb's OWN post-processing (`walkforward_summary_json_from_reports`,
|
||
/// deleted Task 8 Step 2) — no surviving command reconstructs that exact
|
||
/// envelope, so it is not re-pinned here; the family-persistence property
|
||
/// (member count, `FamilyKind::WalkForward`, one stored topology) survives in
|
||
/// full.
|
||
#[test]
|
||
fn aura_walkforward_over_a_blueprint_persists_a_family() {
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let bp_id = seed_blueprint(&cwd, "wfx-seed");
|
||
let proc_id = register_process_doc(&cwd, "wfx.process.json", GRID_THEN_WF_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "wfx",
|
||
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": 1709251200000, "to_ms": 1719791999999 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2, 3] }},
|
||
"slow.length": {{ "kind": "I64", "values": [4, 6] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "wfx.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "wfx.campaign.json"]);
|
||
assert_eq!(code, Some(0), "stderr: {out}");
|
||
let member_count = out.lines().filter(|l| l.starts_with("{\"family_id\":")).count();
|
||
assert!(member_count > 0, "at least one OOS member line: {out}");
|
||
let (fo, fams_code) = run_code_in(&cwd, &["runs", "families"]);
|
||
assert_eq!(fams_code, Some(0), "families exit: {fo}");
|
||
assert!(fo.contains("\"kind\":\"WalkForward\""), "families: {fo}");
|
||
let n = std::fs::read_dir(cwd.join("runs").join("blueprints")).map(|d| d.count()).unwrap_or(0);
|
||
assert_eq!(n, 1, "exactly one content-addressed blueprint stored");
|
||
}
|
||
|
||
|
||
|
||
/// E2E (#278 decision: same fault class, same verb, same surface => same
|
||
/// treatment): a member fault on the SYNTHETIC walk-forward path is CONTAINED
|
||
/// exactly like the real/campaign path contains it (#272) — it surfaces as an
|
||
/// `aura: warning: ` class-marked stderr line naming the member fault and the
|
||
/// process takes the deliberate failed-cells exit 3 (C14 partition,
|
||
/// `exit_on_campaign_result` precedent). It never escapes as an uncaught Rust
|
||
/// panic: no exit 101, no `panicked at`, no internal `crates/` source path on
|
||
/// the consumer's stderr.
|
||
#[test]
|
||
fn aura_walkforward_synthetic_member_panic_is_contained() {
|
||
// Ported onto a `[std::grid, std::walk_forward]` campaign document over
|
||
// SYMA (#319 Task 8).
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let bp_id = seed_blueprint(&cwd, "wfpanic-seed");
|
||
let proc_id = register_process_doc(&cwd, "wfpanic.process.json", GRID_THEN_WF_PROCESS_DOC);
|
||
// length=0 poisons the member: `Sma::new` asserts "SMA length must be >= 1"
|
||
// during member compile — the same fault class the real path records as
|
||
// "a member panicked: …" and survives.
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "wfpanic",
|
||
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": 1709251200000, "to_ms": 1719791999999 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [0] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "wfpanic.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "wfpanic.campaign.json"]);
|
||
assert!(
|
||
out.lines().any(|l| l.contains("aura: warning: ") && l.contains("SMA length must be >= 1")),
|
||
"the member fault surfaces as one class-marked warning naming the fault: {out}"
|
||
);
|
||
assert!(!out.contains("panicked at"), "no raw panic report reaches the consumer: {out}");
|
||
assert!(!out.contains("crates/"), "no internal source path leaks to the consumer: {out}");
|
||
assert_eq!(
|
||
code, Some(3),
|
||
"a contained member fault is the deliberate failed-cells exit 3, never 101: {out}"
|
||
);
|
||
}
|
||
|
||
/// E2E (#278 decision, sweep twin): the campaign path's sweep stage shares the
|
||
/// same bare-member seam as its walk-forward sibling, so the same poison must
|
||
/// be contained the same way — warning + exit 3, no panic escape. Ported onto
|
||
/// a one-cell campaign document over SYMA (#319 Task 8).
|
||
#[test]
|
||
fn aura_sweep_synthetic_member_panic_is_contained() {
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let bp_id = seed_blueprint(&cwd, "swpanic-seed");
|
||
let proc_id = register_process_doc(&cwd, "swpanic.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "swpanic",
|
||
"data": {{ "instruments": ["SYMA"], "windows": [ {{ "from_ms": 1709251200000, "to_ms": 1719791999999 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [0] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "swpanic.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "swpanic.campaign.json"]);
|
||
assert!(
|
||
out.lines().any(|l| l.contains("aura: warning: ") && l.contains("SMA length must be >= 1")),
|
||
"the member fault surfaces as one class-marked warning naming the fault: {out}"
|
||
);
|
||
assert!(
|
||
!out.contains("panicked at") && !out.contains("crates/"),
|
||
"no raw panic report or internal source path leaks: {out}"
|
||
);
|
||
assert_eq!(
|
||
code, Some(3),
|
||
"a contained member fault is the deliberate failed-cells exit 3, never 101: {out}"
|
||
);
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/// E2E (negative): `aura reproduce <unknown>` is a hard error (exit non-zero + named
|
||
/// cause on stderr), distinct from `runs family <id>`'s treat-as-empty exit 0.
|
||
#[test]
|
||
fn aura_reproduce_rejects_an_unknown_family() {
|
||
let cwd = temp_cwd("reproduce_unknown");
|
||
let out = std::process::Command::new(BIN)
|
||
.args(["reproduce", "no-such-family-1"])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("run aura reproduce");
|
||
assert!(!out.status.success(), "unknown family exits non-zero");
|
||
assert!(
|
||
String::from_utf8_lossy(&out.stderr).contains("no such family 'no-such-family-1'"),
|
||
"names the cause: {}",
|
||
String::from_utf8_lossy(&out.stderr)
|
||
);
|
||
let _ = std::fs::remove_dir_all(&cwd);
|
||
}
|
||
|
||
// Note (#159 cut 4 collateral): this removes
|
||
// `aura_reproduce_rejects_a_non_generated_family` — its fixture was a
|
||
// HARD-WIRED (bare `sweep --name`) family with no `topology_hash`; sweep is
|
||
// blueprint-only now and every blueprint-sweep member stamps a
|
||
// `topology_hash`, so no surviving CLI surface can construct the fixture.
|
||
// The defensive "not a generated run" refusal in `main.rs` stays as dead-path
|
||
// safety for a malformed/legacy `families.jsonl`. Removed as collateral, not
|
||
// repointed.
|
||
|
||
/// E2E (refuse-don't-guess): `aura reproduce` when the content-addressed blueprint was
|
||
/// removed from the store exits non-zero, naming the missing blueprint — never re-runs
|
||
/// against a guessed topology (C10/C18).
|
||
#[test]
|
||
fn aura_reproduce_rejects_a_missing_stored_blueprint() {
|
||
// Ported onto a one-cell campaign document over SYMA (#319 Task 8).
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let bp_id = seed_blueprint(&cwd, "reproduce-missing-store-seed");
|
||
let proc_id = register_process_doc(&cwd, "bp.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||
let doc = sweep_campaign_doc("bp", "SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999), "2", "4");
|
||
write_doc(&cwd, "bp.campaign.json", &doc);
|
||
let (sweep_out, sweep_code) = run_code_in(&cwd, &["exec", "bp.campaign.json"]);
|
||
assert_eq!(sweep_code, Some(0), "blueprint sweep ok: {sweep_out}");
|
||
std::fs::remove_dir_all(cwd.join("runs").join("blueprints")).expect("remove the blueprint store");
|
||
|
||
let id = family_id_of_kind(&cwd, "Sweep");
|
||
let (out, code) = run_code_in(&cwd, &["reproduce", &id]);
|
||
assert_ne!(code, Some(0), "a missing stored blueprint exits non-zero");
|
||
assert!(out.contains("missing from store"), "names the cause: {out}");
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/// `--version` / `-V` surface the crate version on stdout and exit 0 (GNU MUST):
|
||
/// a version query is a successful query, not a usage error. The expected string
|
||
/// is the crate version itself (`CARGO_PKG_VERSION`, i.e. the workspace version),
|
||
/// so this stays honest across version bumps and keeps one source of truth.
|
||
#[test]
|
||
fn version_flag_prints_version_to_stdout_and_exits_zero() {
|
||
for flag in ["--version", "-V"] {
|
||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.arg(flag)
|
||
.output()
|
||
.expect("spawn aura");
|
||
assert_eq!(out.status.code(), Some(0), "{flag}: exit {:?}", out.status);
|
||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||
assert!(stdout.contains(env!("CARGO_PKG_VERSION")), "{flag} stdout: {stdout:?}");
|
||
}
|
||
}
|
||
|
||
/// `--version` carries the ENGINE COMMIT beside the semver, drawn from the SAME
|
||
/// `AURA_COMMIT` build-time provenance the run manifest stamps (the
|
||
/// `option_env!("AURA_COMMIT")` that fills `RunManifest.commit`), so a stale
|
||
/// binary is distinguishable at the CLI surface *before* any run (#266). Shape:
|
||
/// `aura 0.1.0 (<commit-ish>)`, exit 0.
|
||
///
|
||
/// Property, not a specific sha: the expected commit is read from the same
|
||
/// provenance source the built binary was compiled with — this integration test
|
||
/// receives aura-cli's build-script `rustc-env`, so it sees the identical value
|
||
/// (dirty/`unknown` suffixes included) rather than today's HEAD, keeping the
|
||
/// assertion honest against a lagging or unclean tree. The load-bearing claim is
|
||
/// that the parenthesized commit rides beside the still-present semver.
|
||
#[test]
|
||
fn version_flag_carries_the_engine_commit_from_manifest_provenance() {
|
||
// The single source of truth — the same env the manifest's commit is sourced
|
||
// from; falls back to "unknown" identically to `sim_optimal_manifest`.
|
||
let commit = option_env!("AURA_COMMIT").unwrap_or("unknown");
|
||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.arg("--version")
|
||
.output()
|
||
.expect("spawn aura");
|
||
assert_eq!(out.status.code(), Some(0), "--version exit {:?}", out.status);
|
||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||
// Semver stays present (one source of truth, CARGO_PKG_VERSION).
|
||
assert!(
|
||
stdout.contains(env!("CARGO_PKG_VERSION")),
|
||
"--version still prints the semver: {stdout:?}"
|
||
);
|
||
// ...and the engine commit rides beside it, parenthesized, from the shared
|
||
// manifest provenance — the load-bearing assertion #266 adds.
|
||
assert!(
|
||
stdout.contains(&format!("({commit})")),
|
||
"--version prints the engine commit ({commit}) beside the semver: {stdout:?}"
|
||
);
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/// Characterization pin (byte-identity anchor for the mc R-bootstrap dissolution,
|
||
/// #210, the last verb; argv since migrated to the generic form by #220).
|
||
/// `aura mc <blueprint> --real --axis …` pools the per-window OOS trade-R
|
||
/// series and r-bootstraps E[R] through the campaign
|
||
/// `[std::sweep(argmax), std::walk_forward, std::monte_carlo]`
|
||
/// process, whose terminal monte_carlo stage does the
|
||
/// `StageBootstrap::PooledOos(r_bootstrap(...))` seeded from the campaign seed -- so
|
||
/// the campaign seed carries the mc `--seed`. The wf winners are
|
||
/// deflation-seed-independent (argmax), so the pooled series, and thus the
|
||
/// bootstrap, is stable across that seed remap (the walkforward anchor already
|
||
/// proved winner seed-independence). The multi-point grid makes the per-window
|
||
/// winner selection non-degenerate. The pinned bootstrap grade of this fixed 2025
|
||
/// GER40 invocation ORIGINATES from the pre-#210 inline rolling walk-forward
|
||
/// (`mc_r_bootstrap_report`) under the retired welded `aura mc --strategy r-sma
|
||
/// --real` grammar, and survived both the campaign dissolution and the #220 argv
|
||
/// migration byte-identically (the acceptance gate then, the regression anchor
|
||
/// now). Gated on the GER40 archive; skips
|
||
/// cleanly on a data refusal.
|
||
#[test]
|
||
fn mc_r_bootstrap_real_e2e_pins_the_exact_current_grade() {
|
||
// Ported onto a `[std::grid, std::walk_forward, std::monte_carlo]`
|
||
// campaign document (#319 Task 8): the grade is the terminal stage's own
|
||
// native `bootstrap.pooled_oos` record (`aura_backtest::RBootstrap`, the
|
||
// SAME struct the retired `mc_r_bootstrap_json` CLI wrapper printed under
|
||
// identical field names) — no client-side reconstruction needed. The
|
||
// campaign `seed` carries the retired sugar's `--seed` (`translate_mc`'s
|
||
// mapping).
|
||
let (cwd, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&cwd, "mc-pin-seed");
|
||
// The fixed real-archive roller (`aura_backtest::wf_ms_sizes`, 90d IS / 30d
|
||
// OOS / 30d step) the retired sugar's `fit_wf_ms_sizes` used — this
|
||
// window's span (~1 year) comfortably exceeds IS+OOS, so the fixed sizes
|
||
// apply unscaled (the `fit_wf_ms_sizes` fast-path every year-plus e2e
|
||
// anchor pins).
|
||
let mc_process_doc = r#"{
|
||
"format_version": 1,
|
||
"kind": "process",
|
||
"name": "grid-then-walkforward-then-mc-pin",
|
||
"pipeline": [
|
||
{ "block": "std::grid" },
|
||
{ "block": "std::walk_forward", "in_sample_ms": 7776000000, "out_of_sample_ms": 2592000000,
|
||
"step_ms": 2592000000, "mode": "rolling", "metric": "sqn_normalized", "select": "argmax" },
|
||
{ "block": "std::monte_carlo", "resamples": 1000, "block_len": 5 }
|
||
]
|
||
}"#;
|
||
let proc_id = register_process_doc(&cwd, "mcpin.process.json", mc_process_doc);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "mcpin",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1735776899999, "to_ms": 1767128220000 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [3, 5] }},
|
||
"slow.length": {{ "kind": "I64", "values": [12, 20] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 14, "k": 2.0 }} }} ],
|
||
"seed": 42,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "mcpin.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "mcpin.campaign.json"]);
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40 data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {out}");
|
||
let record_line = out
|
||
.lines()
|
||
.find(|l| l.starts_with("{\"campaign_run\":"))
|
||
.expect("the always-on final campaign_run line");
|
||
let v: serde_json::Value = serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
|
||
let mc = &v["campaign_run"]["cells"][0]["stages"][2]["bootstrap"]["pooled_oos"];
|
||
assert_eq!(mc["block_len"].as_u64(), Some(5), "block_len: {record_line}");
|
||
assert_eq!(mc["n_resamples"].as_u64(), Some(1000), "n_resamples: {record_line}");
|
||
assert_eq!(mc["n_trades"].as_u64(), Some(20681), "pooled OOS trade count: {record_line}");
|
||
// EXACT bootstrap floats -- the byte-identity anchor the dissolution must preserve.
|
||
assert_eq!(mc["e_r"]["mean"].as_f64(), Some(-0.0025753095301594307), "E[R] mean: {record_line}");
|
||
assert_eq!(mc["e_r"]["p50"].as_f64(), Some(-0.0023049902245975227), "E[R] median: {record_line}");
|
||
assert_eq!(mc["prob_le_zero"].as_f64(), Some(0.558), "P(E[R] <= 0): {record_line}");
|
||
}
|
||
|
||
/// Property (#220 mc vertical): `aura mc --real` R-bootstraps ANY sweepable
|
||
/// blueprint through the campaign path, not just the historical r-sma candidate
|
||
/// — the whole point of dropping `--strategy r-sma`/`--fast`/`--slow` for a
|
||
/// positional blueprint + generic `--axis`. Every other mc E2E test here still
|
||
/// exercises `r_sma.json`; a regression that silently special-cased the
|
||
/// pipeline's axis lookup back to a hardcoded `sma_signal.*` namespace would
|
||
/// ship green there while breaking every other blueprint. This runs the
|
||
/// unrelated r-breakout candidate (the ganged `channel_length` axis, no
|
||
/// `sma_signal` node in sight) end-to-end and asserts a well-formed
|
||
/// `mc_r_bootstrap` grade: the argv-echoed knobs (`block_len`/`n_resamples`)
|
||
/// pin exactly, `n_trades`/`e_r.mean`/`prob_le_zero` are asserted present and
|
||
/// well-typed (their exact values are strategy-specific, already the r-sma
|
||
/// pin's job above). Gated on the shared GER40 archive; skips cleanly on a
|
||
/// data refusal.
|
||
#[test]
|
||
fn mc_dissolves_a_non_r_sma_blueprint() {
|
||
// Ported onto a `[std::grid, std::walk_forward, std::monte_carlo]`
|
||
// campaign document (#319 Task 8), same clipped window + fixed 90/30/30
|
||
// roller as `mc_r_bootstrap_real_e2e_pins_the_exact_current_grade`, over
|
||
// the unrelated r-breakout candidate.
|
||
let (cwd, _g) = fresh_project();
|
||
let fixture = format!("{}/tests/fixtures/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
|
||
let bp_id = seed_blueprint_file(&cwd, &fixture, "breakout-mcpin-seed");
|
||
let mc_process_doc = r#"{
|
||
"format_version": 1,
|
||
"kind": "process",
|
||
"name": "grid-then-walkforward-then-mc-breakout",
|
||
"pipeline": [
|
||
{ "block": "std::grid" },
|
||
{ "block": "std::walk_forward", "in_sample_ms": 7776000000, "out_of_sample_ms": 2592000000,
|
||
"step_ms": 2592000000, "mode": "rolling", "metric": "sqn_normalized", "select": "argmax" },
|
||
{ "block": "std::monte_carlo", "resamples": 1000, "block_len": 5 }
|
||
]
|
||
}"#;
|
||
let proc_id = register_process_doc(&cwd, "breakoutmcpin.process.json", mc_process_doc);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "breakoutmcpin",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1735776899999, "to_ms": 1767128220000 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "channel_length": {{ "kind": "I64", "values": [10, 20] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 14, "k": 2.0 }} }} ],
|
||
"seed": 42,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "breakoutmcpin.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "breakoutmcpin.campaign.json"]);
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40 data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {out}");
|
||
let record_line = out
|
||
.lines()
|
||
.find(|l| l.starts_with("{\"campaign_run\":"))
|
||
.expect("the always-on final campaign_run line");
|
||
let v: serde_json::Value = serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
|
||
let mc = &v["campaign_run"]["cells"][0]["stages"][2]["bootstrap"]["pooled_oos"];
|
||
assert_eq!(mc["block_len"].as_u64(), Some(5), "block_len (argv echo): {record_line}");
|
||
assert_eq!(mc["n_resamples"].as_u64(), Some(1000), "n_resamples (argv echo): {record_line}");
|
||
assert!(mc["n_trades"].as_u64().is_some_and(|n| n > 0), "pooled OOS trade count present: {record_line}");
|
||
assert!(mc["e_r"]["mean"].as_f64().is_some_and(|m| m.is_finite()), "E[R] mean present + finite: {record_line}");
|
||
assert!(
|
||
mc["prob_le_zero"].as_f64().is_some_and(|p| (0.0..=1.0).contains(&p)),
|
||
"P(E[R] <= 0) present + in [0,1]: {record_line}"
|
||
);
|
||
}
|
||
|
||
/// Property: real-data `aura mc <blueprint> --real --axis …` (the #210 dissolution
|
||
/// of the retired welded `--strategy r-sma` form, argv since migrated by #220) is
|
||
/// thin sugar over the one campaign
|
||
/// path — a successful run durably auto-registers exactly one generated process
|
||
/// document, one generated campaign document (carrying the constant "mc" name and the
|
||
/// stop as a non-empty single risk regime), and one campaign-run record, and emits the
|
||
/// single `mc_r_bootstrap` grade line. The pipeline's walk_forward stage persists
|
||
/// the one family; the leading `std::grid` stage only enumerates (#256) and the
|
||
/// terminal monte_carlo stage is an annotator — neither adds a family. This is
|
||
/// the observable proof the inline path is gone
|
||
/// and the dissolution runs through the executor. Gated on the GER40 2025 archive;
|
||
/// skips cleanly on a data refusal.
|
||
#[test]
|
||
fn mc_dissolves_through_the_campaign_path() {
|
||
// Ported onto a `[std::grid, std::walk_forward, std::monte_carlo]`
|
||
// campaign document (#319 Task 8): the retired `mc_r_bootstrap` line
|
||
// format is gone (deleted Task 8 Step 2), but the equivalent property —
|
||
// a single native summary line (`emit: []` suppresses the per-member
|
||
// family_table lines, leaving only the always-on final
|
||
// `{"campaign_run":…}` line) — survives, plus the auto-registration and
|
||
// family-count properties.
|
||
let (cwd, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&cwd, "mc-dissolve-seed");
|
||
let mc_process_doc = r#"{
|
||
"format_version": 1,
|
||
"kind": "process",
|
||
"name": "grid-then-walkforward-then-mc-dissolve",
|
||
"pipeline": [
|
||
{ "block": "std::grid" },
|
||
{ "block": "std::walk_forward", "in_sample_ms": 7776000000, "out_of_sample_ms": 2592000000,
|
||
"step_ms": 2592000000, "mode": "rolling", "metric": "sqn_normalized", "select": "argmax" },
|
||
{ "block": "std::monte_carlo", "resamples": 1000, "block_len": 5 }
|
||
]
|
||
}"#;
|
||
let proc_id = register_process_doc(&cwd, "mcdissolve.process.json", mc_process_doc);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "mc",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1735776899999, "to_ms": 1767128220000 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [3, 5] }},
|
||
"slow.length": {{ "kind": "I64", "values": [12, 20] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 14, "k": 2.0 }} }} ],
|
||
"seed": 42,
|
||
"presentation": {{ "persist_taps": [], "emit": [] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "mc.campaign.json", &doc);
|
||
let raw = Command::new(BIN).args(["exec", "mc.campaign.json"]).current_dir(&cwd).output().expect("spawn exec");
|
||
let stdout = String::from_utf8_lossy(&raw.stdout).into_owned();
|
||
let stderr = String::from_utf8_lossy(&raw.stderr).into_owned();
|
||
let out = format!("{stdout}{stderr}");
|
||
if raw.status.code() == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40 data");
|
||
return;
|
||
}
|
||
assert_eq!(raw.status.code(), Some(0), "exit: {out}");
|
||
|
||
// the single native summary line is emitted on STDOUT (and nothing else
|
||
// there, since `emit: []` suppresses the per-member family_table lines) —
|
||
// the informational "aura: campaign run N recorded: …" tally rides
|
||
// stderr, not stdout, so it is not counted here.
|
||
assert_eq!(stdout.lines().count(), 1, "mc prints exactly one summary line on stdout: {stdout:?}");
|
||
assert!(
|
||
stdout.lines().next().is_some_and(|l| l.starts_with("{\"campaign_run\":")),
|
||
"the one line is the native campaign_run summary: {stdout}"
|
||
);
|
||
|
||
let count = |sub: &str| {
|
||
std::fs::read_dir(cwd.join("runs").join(sub))
|
||
.map(|d| d.count())
|
||
.unwrap_or(0)
|
||
};
|
||
assert_eq!(count("processes"), 1, "one generated process document registered");
|
||
assert_eq!(count("campaigns"), 1, "one generated campaign document registered");
|
||
let runs_log = std::fs::read_to_string(cwd.join("runs").join("campaign_runs.jsonl"))
|
||
.expect("campaign_runs.jsonl exists after a run");
|
||
assert_eq!(runs_log.lines().count(), 1, "one campaign run recorded");
|
||
|
||
let campaigns_dir = cwd.join("runs").join("campaigns");
|
||
let campaign_doc_path = std::fs::read_dir(&campaigns_dir)
|
||
.expect("campaigns dir exists")
|
||
.next()
|
||
.expect("exactly one campaign document")
|
||
.expect("readable dir entry")
|
||
.path();
|
||
let campaign_doc_json = std::fs::read_to_string(&campaign_doc_path).expect("read campaign doc");
|
||
assert!(
|
||
campaign_doc_json.contains("\"name\":\"mc\""),
|
||
"the campaign document carries the authored mc name: {campaign_doc_json}"
|
||
);
|
||
assert!(
|
||
campaign_doc_json.contains("\"risk\":[")
|
||
&& campaign_doc_json.contains("\"length\":14")
|
||
&& campaign_doc_json.contains("\"k\":2.0"),
|
||
"the stop rides a non-empty risk regime: {campaign_doc_json}"
|
||
);
|
||
|
||
let (fams_out, fams_code) = run_code_in(&cwd, &["runs", "families"]);
|
||
assert_eq!(fams_code, Some(0), "families exit: {fams_out}");
|
||
assert_eq!(
|
||
fams_out.lines().filter(|l| l.contains("\"kind\":\"Sweep\"")).count(),
|
||
0,
|
||
"the enumerate-only std::grid leading stage persists no Sweep family: {fams_out}"
|
||
);
|
||
assert_eq!(
|
||
fams_out.lines().filter(|l| l.contains("\"kind\":\"WalkForward\"")).count(),
|
||
1,
|
||
"one WalkForward family holding the per-window OOS reports: {fams_out}"
|
||
);
|
||
assert_eq!(
|
||
fams_out.lines().filter(|l| l.contains("\"kind\":\"MonteCarlo\"")).count(),
|
||
0,
|
||
"the terminal monte_carlo stage is an annotator, not a family: {fams_out}"
|
||
);
|
||
}
|
||
|
||
/// Property (#239): a real-data `aura mc --real` invocation over a window SHORTER
|
||
/// than the fixed 90-day-in-sample / 30-day-out-of-sample roller runs the R-bootstrap
|
||
/// 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 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 std::grid stage only enumerates (nothing runs over the short window); the
|
||
/// refusal 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]
|
||
fn mc_real_fits_the_injected_roller_to_a_short_window() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
// Ported onto a `[std::grid, std::walk_forward, std::monte_carlo]`
|
||
// campaign document (#319 Task 8): the retired CLI's `fit_wf_ms_sizes`
|
||
// scaling (`aura_backtest::scaffold::fit_wf_ms_sizes`, still a real
|
||
// library function) is now invoked by the TEST itself, at document-
|
||
// construction time, to size the `std::walk_forward` stage — a
|
||
// hand-authored document has no in-executor auto-fit (that scaling was
|
||
// pure CLI-side preprocessing before generating the document); this
|
||
// reproduces the SAME fitted roller the retired sugar generated.
|
||
let (cwd, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&cwd, "mc-short-window-seed");
|
||
|
||
// ~30 days, far shorter than the fixed 90+30-day roller, so the roller
|
||
// must scale down to the window (the same fit that lets this complete).
|
||
const SUB_FROM_MS: i64 = 1744761600000;
|
||
const SUB_TO_MS: i64 = 1747353600000;
|
||
let (is_ms, oos_ms, step_ms) = aura_backtest::scaffold::fit_wf_ms_sizes(SUB_FROM_MS, SUB_TO_MS);
|
||
// Sanity: the fixed 90d+30d roller does NOT fit this ~30-day span
|
||
// unscaled, so `fit_wf_ms_sizes` must actually be exercising its
|
||
// scale-down branch here — else this test would be vacuous.
|
||
assert!(
|
||
7_776_000_000u64 + 2_592_000_000u64 > (SUB_TO_MS - SUB_FROM_MS) as u64,
|
||
"the fixed 90/30 roller must NOT fit this window unscaled, or the fit branch goes untested"
|
||
);
|
||
|
||
let mc_process_doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "process",
|
||
"name": "grid-then-walkforward-then-mc-shortwindow",
|
||
"pipeline": [
|
||
{{ "block": "std::grid" }},
|
||
{{ "block": "std::walk_forward", "in_sample_ms": {is_ms}, "out_of_sample_ms": {oos_ms},
|
||
"step_ms": {step_ms}, "mode": "rolling", "metric": "sqn_normalized", "select": "argmax" }},
|
||
{{ "block": "std::monte_carlo", "resamples": 100, "block_len": 5 }}
|
||
]
|
||
}}"#,
|
||
);
|
||
let proc_id = register_process_doc(&cwd, "mcshort.process.json", &mc_process_doc);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "mcshort",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [3, 5] }},
|
||
"slow.length": {{ "kind": "I64", "values": [12, 20] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 14, "k": 2.0 }} }} ],
|
||
"seed": 42,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
from = SUB_FROM_MS, to = SUB_TO_MS,
|
||
);
|
||
write_doc(&cwd, "mcshort.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "mcshort.campaign.json"]);
|
||
|
||
// Defensive: a genuine data refusal (never the late window-fit refusal this test
|
||
// pins) still means "no usable data on this host" — skip, don't fail.
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40 data");
|
||
return;
|
||
}
|
||
assert_eq!(
|
||
code, Some(0),
|
||
"a short real window must fit the injected roller and run to completion, \
|
||
not refuse late: {out}"
|
||
);
|
||
let record_line = out
|
||
.lines()
|
||
.find(|l| l.starts_with("{\"campaign_run\":"))
|
||
.expect("the always-on final campaign_run line");
|
||
let v: serde_json::Value = serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
|
||
assert!(
|
||
v["campaign_run"]["cells"][0]["stages"][2]["bootstrap"]["pooled_oos"].is_object(),
|
||
"the mc bootstrap grade must be emitted for the fitted short window: {record_line}"
|
||
);
|
||
}
|
||
|
||
|
||
|
||
/// Property: the one load-bearing decision new to the mc dissolution — `translate_mc`
|
||
/// maps `campaign.seed` to the mc `--seed` (unlike `translate_walkforward`, which
|
||
/// hardcodes `seed: 0`) — actually flows through the executor to the emitted grade,
|
||
/// not just into the generated document's content id (already unit-pinned by
|
||
/// `translate_mc_diverging_seeds_and_stops_do_not_collide_content_ids`). Two runs that
|
||
/// differ ONLY in `--seed` must emit a different `e_r` distribution (the bootstrap
|
||
/// resample draw is seeded) while pooling the IDENTICAL `n_trades` (the wf winners are
|
||
/// argmax-selected, hence deflation-seed-independent — the walkforward anchor already
|
||
/// proved this for the shared roller): the seed perturbs the resample, never the
|
||
/// underlying OOS trade-R series. Without this test, a dropped seed remap (e.g. mc
|
||
/// always bootstrapping at a fixed internal seed) would still pass every other mc
|
||
/// dissolution test, since none of them compare two distinct `--seed` runs. Gated on
|
||
/// the GER40 2025 archive; skips cleanly on a data refusal.
|
||
#[test]
|
||
fn mc_dissolved_seed_changes_the_bootstrap_draw_not_the_pooled_series() {
|
||
// Ported onto a `[std::grid, std::walk_forward, std::monte_carlo]`
|
||
// campaign document (#319 Task 8), same clipped window + fixed 90/30/30
|
||
// roller as `mc_r_bootstrap_real_e2e_pins_the_exact_current_grade` — only
|
||
// the document's own `seed` field varies (the campaign seed carries the
|
||
// retired sugar's `--seed`, `translate_mc`'s mapping).
|
||
let (cwd, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&cwd, "mc-seed-diff-seed");
|
||
let mc_process_doc = r#"{
|
||
"format_version": 1,
|
||
"kind": "process",
|
||
"name": "grid-then-walkforward-then-mc-seeddiff",
|
||
"pipeline": [
|
||
{ "block": "std::grid" },
|
||
{ "block": "std::walk_forward", "in_sample_ms": 7776000000, "out_of_sample_ms": 2592000000,
|
||
"step_ms": 2592000000, "mode": "rolling", "metric": "sqn_normalized", "select": "argmax" },
|
||
{ "block": "std::monte_carlo", "resamples": 1000, "block_len": 5 }
|
||
]
|
||
}"#;
|
||
let proc_id = register_process_doc(&cwd, "mcseeddiff.process.json", mc_process_doc);
|
||
let doc_for = |seed: u64| {
|
||
format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "mcseeddiff-{seed}",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1735776899999, "to_ms": 1767128220000 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [3, 5] }},
|
||
"slow.length": {{ "kind": "I64", "values": [12, 20] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 14, "k": 2.0 }} }} ],
|
||
"seed": {seed},
|
||
"presentation": {{ "persist_taps": [], "emit": [] }}
|
||
}}"#,
|
||
)
|
||
};
|
||
let run = |seed: u64| {
|
||
let file = format!("mcseeddiff-{seed}.campaign.json");
|
||
write_doc(&cwd, &file, &doc_for(seed));
|
||
run_code_in(&cwd, &["exec", &file])
|
||
};
|
||
let (out42, code42) = run(42);
|
||
if code42 == Some(3) && (out42.contains("no local data") || out42.contains("no recorded geometry") || out42.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40 data");
|
||
return;
|
||
}
|
||
let (out7, code7) = run(7);
|
||
assert_eq!(code42, Some(0), "exit (seed 42): {out42}");
|
||
assert_eq!(code7, Some(0), "exit (seed 7): {out7}");
|
||
|
||
let parse = |out: &str| -> serde_json::Value {
|
||
let line = out
|
||
.lines()
|
||
.find(|l| l.starts_with("{\"campaign_run\":"))
|
||
.unwrap_or_else(|| panic!("the always-on final campaign_run line: {out}"));
|
||
serde_json::from_str(line).expect("campaign_run line parses as JSON")
|
||
};
|
||
let v42 = parse(&out42);
|
||
let v7 = parse(&out7);
|
||
let mc42 = &v42["campaign_run"]["cells"][0]["stages"][2]["bootstrap"]["pooled_oos"];
|
||
let mc7 = &v7["campaign_run"]["cells"][0]["stages"][2]["bootstrap"]["pooled_oos"];
|
||
|
||
assert_eq!(
|
||
mc42["n_trades"], mc7["n_trades"],
|
||
"the pooled OOS trade-R series is seed-independent (argmax winners): {mc42} vs {mc7}"
|
||
);
|
||
assert_ne!(
|
||
mc42["e_r"]["mean"], mc7["e_r"]["mean"],
|
||
"a different seed must move the resampled E[R] mean: {mc42} vs {mc7}"
|
||
);
|
||
}
|
||
|
||
/// #220 weld-gone proof: the walkforward campaign path runs an arbitrary user
|
||
/// blueprint with axis names the CLI has never heard of — the r-sma weld
|
||
/// (hardcoded fast/slow) is structurally gone. Gated on the GER40 archive;
|
||
/// skips cleanly when absent.
|
||
#[test]
|
||
fn walkforward_campaign_runs_an_arbitrary_blueprint() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
// Ported onto a `[std::grid, std::walk_forward]` campaign document
|
||
// (#319 Task 8) over the SAME real GER40 window/axis the sugar used —
|
||
// the `{"walkforward":…}`/`param_stability` aggregate was the retired
|
||
// CLI verb's own post-processing (deleted Task 8 Step 2, no surviving
|
||
// reconstruction); the surviving property is that the campaign path
|
||
// realizes a `std::walk_forward` stage (>= 1 window, family persisted)
|
||
// over an arbitrary user blueprint with axis names the CLI has never
|
||
// hardcoded — the r-sma weld is structurally gone.
|
||
let (cwd, _g) = fresh_project();
|
||
let fixture = format!("{}/tests/fixtures/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
|
||
let bp_id = seed_blueprint_file(&cwd, &fixture, "breakout-wf-seed");
|
||
let proc_id = register_process_doc(&cwd, "breakoutwf.process.json", GRID_THEN_WF_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "breakout-wf",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1735689600000, "to_ms": 1767225599000 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "channel_length": {{ "kind": "I64", "values": [10, 20] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 3, "k": 2.0 }} }} ],
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "breakoutwf.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "breakoutwf.campaign.json"]);
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40 data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {out}");
|
||
let record_line = out
|
||
.lines()
|
||
.find(|l| l.starts_with("{\"campaign_run\":"))
|
||
.expect("the always-on final campaign_run line");
|
||
let v: serde_json::Value = serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
|
||
let stages = &v["campaign_run"]["cells"][0]["stages"];
|
||
assert_eq!(stages[0]["block"], "std::grid", "leading grid stage: {record_line}");
|
||
assert_eq!(stages[1]["block"], "std::walk_forward", "walk_forward realized: {record_line}");
|
||
assert!(stages[1]["family_id"].as_str().is_some(), "walk-forward persists its family: {record_line}");
|
||
}
|
||
|
||
/// #220 weld-gone proof for mc: the R-bootstrap pipeline runs the same
|
||
/// arbitrary breakout blueprint. Gated on the GER40 archive.
|
||
#[test]
|
||
fn mc_campaign_runs_an_arbitrary_blueprint() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
// Ported onto a `[std::grid, std::walk_forward, std::monte_carlo]`
|
||
// campaign document (#319 Task 8): the terminal stage's own native
|
||
// `bootstrap.pooled_oos` record (`aura_backtest::RBootstrap`, the SAME
|
||
// struct the retired CLI's `mc_r_bootstrap_json` wrapper printed) is read
|
||
// directly — no client-side reconstruction needed.
|
||
let (cwd, _g) = fresh_project();
|
||
let fixture = format!("{}/tests/fixtures/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
|
||
let bp_id = seed_blueprint_file(&cwd, &fixture, "breakout-mc-seed");
|
||
let proc_id = register_process_doc(&cwd, "breakoutmc.process.json", GRID_THEN_WF_THEN_MC_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "breakout-mc",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1735689600000, "to_ms": 1767225599000 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "channel_length": {{ "kind": "I64", "values": [10, 20] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 3, "k": 2.0 }} }} ],
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "breakoutmc.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "breakoutmc.campaign.json"]);
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40 data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {out}");
|
||
let record_line = out
|
||
.lines()
|
||
.find(|l| l.starts_with("{\"campaign_run\":"))
|
||
.expect("the always-on final campaign_run line");
|
||
let v: serde_json::Value = serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
|
||
let pooled = &v["campaign_run"]["cells"][0]["stages"][2]["bootstrap"]["pooled_oos"];
|
||
assert_eq!(pooled["n_resamples"].as_u64(), Some(100), "resamples flow: {record_line}");
|
||
assert!(pooled["e_r"]["mean"].is_number(), "bootstrap E[R] present: {record_line}");
|
||
}
|
||
|
||
/// #220 weld-gone proof for generalize: one mean-reversion candidate graded
|
||
/// across two instruments — two axes with names the CLI has never heard of —
|
||
/// one ganged I64 window and an independent F64 band. Gated on the GER40/USDJPY
|
||
/// Sept-2024 archive.
|
||
#[test]
|
||
fn generalize_campaign_runs_an_arbitrary_blueprint() {
|
||
if !local_data_present() {
|
||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||
return;
|
||
}
|
||
// Ported onto a `[std::sweep, std::generalize]` campaign document
|
||
// (#319 Task 8): the grade is the campaign's own native
|
||
// `generalizations[].generalization` record (`aura_registry::
|
||
// Generalization`, computed inside `aura-campaign::exec` — the SAME
|
||
// struct the retired `generalize_json` CLI wrapper printed), read
|
||
// directly off the final `{"campaign_run":…}` line.
|
||
let (cwd, _g) = fresh_project();
|
||
let fixture = format!("{}/tests/fixtures/r_meanrev_open.json", env!("CARGO_MANIFEST_DIR"));
|
||
let bp_id = seed_blueprint_file(&cwd, &fixture, "meanrev-gen-seed");
|
||
let proc_id = register_process_doc(&cwd, "meanrevgen.process.json", SWEEP_THEN_GENERALIZE_PROCESS_DOC);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "meanrev-gen",
|
||
"data": {{ "instruments": ["GER40", "USDJPY"],
|
||
"windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "window": {{ "kind": "I64", "values": [20] }},
|
||
"band.factor": {{ "kind": "F64", "values": [2.0] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"risk": [ {{ "vol": {{ "length": 3, "k": 2.0 }} }} ],
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
||
}}"#,
|
||
from = GER40_SEPT2024_FROM_MS.parse::<i64>().unwrap(),
|
||
to = GER40_SEPT2024_TO_MS.parse::<i64>().unwrap(),
|
||
);
|
||
write_doc(&cwd, "meanrevgen.campaign.json", &doc);
|
||
let (out, code) = run_code_in(&cwd, &["exec", "meanrevgen.campaign.json"]);
|
||
if code == Some(3) && (out.contains("no local data") || out.contains("no recorded geometry") || out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40/USDJPY data");
|
||
return;
|
||
}
|
||
assert_eq!(code, Some(0), "exit: {out}");
|
||
let record_line = out
|
||
.lines()
|
||
.find(|l| l.starts_with("{\"campaign_run\":"))
|
||
.expect("the always-on final campaign_run line");
|
||
let v: serde_json::Value = serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
|
||
let grade = &v["campaign_run"]["generalizations"][0]["generalization"];
|
||
assert!(grade.is_object(), "the aggregate object is present: {record_line}");
|
||
assert_eq!(grade["n_instruments"].as_u64(), Some(2), "two instruments graded: {record_line}");
|
||
assert!(grade["worst_case"].is_number(), "worst-case floor present: {record_line}");
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/// Property (#273, list reshape): `aura data list` enumerates the archive's
|
||
/// symbols as NDJSON, sorted — the discovery step a campaign author (or an
|
||
/// agent operating through the CLI alone, the deployment posture) needs
|
||
/// before `aura data info <symbol>`. The archive root resolves through the
|
||
/// same project path as `info` (`Env::data_path` — a project-local `[paths]
|
||
/// data` override when set). Over a hermetic synthetic archive holding two
|
||
/// symbols (`SYMA` 2024-01..08 and `SYMB` 2024-03..06), the verb exits 0 and
|
||
/// prints EXACTLY two lines, each a JSON string parseable via
|
||
/// `serde_json::from_str::<String>`, in ascending order — `"SYMA"` before
|
||
/// `"SYMB"`, nothing else.
|
||
#[test]
|
||
fn data_list_on_a_populated_archive_is_ndjson_one_symbol_per_line() {
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["data", "list"])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura data list");
|
||
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");
|
||
let lines: Vec<&str> = stdout.lines().collect();
|
||
assert_eq!(lines.len(), 2, "exactly the two archive symbols: {stdout:?}");
|
||
let parsed: Vec<String> = lines
|
||
.iter()
|
||
.map(|l| serde_json::from_str::<String>(l).unwrap_or_else(|e| panic!("line {l:?} is a JSON string: {e}")))
|
||
.collect();
|
||
assert_eq!(parsed, vec!["SYMA".to_string(), "SYMB".to_string()], "sorted NDJSON enumeration: {stdout:?}");
|
||
}
|
||
|
||
/// Property (#273 design minutes, 2026-07-25): an empty archive under the
|
||
/// all-JSON namespace prints ZERO stdout lines — the honest NDJSON of an
|
||
/// empty set, since a placeholder prose line would break `| jq`-style
|
||
/// consumption — with the human affordance carried on stderr instead (the
|
||
/// `aura: note:` class, #278).
|
||
#[test]
|
||
fn data_list_on_an_empty_archive_is_empty_stdout_with_a_stderr_notice() {
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let data_dir = cwd.join("data");
|
||
std::fs::remove_dir_all(&data_dir).expect("clear the synthetic archive");
|
||
std::fs::create_dir_all(&data_dir).expect("recreate an empty synthetic archive dir");
|
||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["data", "list"])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura data list");
|
||
assert_eq!(
|
||
out.status.code(),
|
||
Some(0),
|
||
"exit: {:?}, stderr: {}",
|
||
out.status,
|
||
String::from_utf8_lossy(&out.stderr)
|
||
);
|
||
assert_eq!(out.stdout, b"", "empty archive: zero stdout lines");
|
||
let stderr = String::from_utf8(out.stderr).expect("utf-8 stderr");
|
||
assert!(stderr.contains("aura: note:") && stderr.contains("no symbols"), "stderr carries the notice: {stderr}");
|
||
}
|
||
|
||
/// Property (#273): `aura data info <SYMBOL>` on a symbol the archive holds
|
||
/// bar files for but no `<SYMBOL>.meta.json` sidecar reports bare identity —
|
||
/// exit 0, stdout is exactly `{"symbol":"…"}`, no `null` geometry fields.
|
||
#[test]
|
||
fn data_info_on_a_symbol_without_sidecar_is_bare_symbol_json() {
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
std::fs::remove_file(cwd.join("data/SYMB.meta.json")).expect("drop SYMB's sidecar");
|
||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["data", "info", "SYMB"])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura data info");
|
||
assert_eq!(
|
||
out.status.code(),
|
||
Some(0),
|
||
"exit: {:?}, stderr: {}",
|
||
out.status,
|
||
String::from_utf8_lossy(&out.stderr)
|
||
);
|
||
assert_eq!(String::from_utf8(out.stdout).expect("utf-8 stdout"), "{\"symbol\":\"SYMB\"}\n");
|
||
}
|
||
|
||
/// Property (#273): `aura data info <SYMBOL>` on a symbol carrying a usable
|
||
/// geometry sidecar surfaces exactly the six neutral geometry fields
|
||
/// (`digits`, `pipSize`, `tickSize`, `lotSize`, `baseAsset`, `quoteAsset`)
|
||
/// alongside `symbol` — and NOTHING provider-specific (`description`,
|
||
/// `generator`, `schemaVersion`) leaks through, even though the sidecar
|
||
/// itself carries those fields: `InstrumentGeometry`'s reader never parses
|
||
/// them out of the sidecar JSON in the first place (#273 acceptance).
|
||
#[test]
|
||
fn data_info_on_a_symbol_with_a_sidecar_carries_the_six_geometry_fields() {
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
std::fs::write(
|
||
cwd.join("data/SYMA.meta.json"),
|
||
r#"{"schemaVersion":2,"generator":"MS_SimpleExport (cTrader.Automate)","description":"Euro vs US Dollar","digits":5,"pipSize":0.0001,"tickSize":1e-05,"lotSize":100000,"baseAsset":"EUR","quoteAsset":"USD"}"#,
|
||
)
|
||
.expect("overwrite SYMA's sidecar with the EURUSD-shaped fixture");
|
||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["data", "info", "SYMA"])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura data info");
|
||
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");
|
||
let obj: serde_json::Value = serde_json::from_str(stdout.trim()).expect("one JSON object");
|
||
let map = obj.as_object().expect("a flat object");
|
||
let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
|
||
keys.sort_unstable();
|
||
assert_eq!(
|
||
keys,
|
||
vec!["baseAsset", "digits", "lotSize", "pipSize", "quoteAsset", "symbol", "tickSize"],
|
||
"exactly symbol + the six geometry fields, no provider metadata: {stdout}"
|
||
);
|
||
assert_eq!(map["symbol"], "SYMA");
|
||
assert_eq!(map["digits"], 5);
|
||
assert_eq!(map["baseAsset"], "EUR");
|
||
assert_eq!(map["quoteAsset"], "USD");
|
||
}
|
||
|
||
/// Property (#273): a symbol carrying a usable geometry sidecar but NO
|
||
/// archive bar files is still KNOWN to `aura data info` — the `symbol_meta`
|
||
/// arm of the known-check's OR, independent of `has_symbol` — and its
|
||
/// geometry reports normally, exit 0. This is the real-world case of a
|
||
/// sidecar exported ahead of the bars themselves being ingested.
|
||
#[test]
|
||
fn data_info_on_a_sidecar_only_symbol_reports_geometry() {
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
std::fs::write(
|
||
cwd.join("data/METAONLY.meta.json"),
|
||
r#"{"schemaVersion":2,"generator":"MS_SimpleExport (cTrader.Automate)","description":"Euro vs US Dollar","digits":5,"pipSize":0.0001,"tickSize":1e-05,"lotSize":100000,"baseAsset":"EUR","quoteAsset":"USD"}"#,
|
||
)
|
||
.expect("write METAONLY's sidecar-only fixture (no archive bar files)");
|
||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["data", "info", "METAONLY"])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura data info");
|
||
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");
|
||
let obj: serde_json::Value = serde_json::from_str(stdout.trim()).expect("one JSON object");
|
||
let map = obj.as_object().expect("a flat object");
|
||
let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
|
||
keys.sort_unstable();
|
||
assert_eq!(
|
||
keys,
|
||
vec!["baseAsset", "digits", "lotSize", "pipSize", "quoteAsset", "symbol", "tickSize"],
|
||
"exactly symbol + the six geometry fields, no provider metadata: {stdout}"
|
||
);
|
||
assert_eq!(map["symbol"], "METAONLY");
|
||
}
|
||
|
||
/// Property (#273): a symbol with neither archive bar files nor a geometry
|
||
/// sidecar is unknown to `aura data info` — refuses on stderr, exit 1, no
|
||
/// panic, no bare Debug struct.
|
||
#[test]
|
||
fn data_info_on_an_unknown_symbol_refuses() {
|
||
let (cwd, _g) = fresh_project_with_data();
|
||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["data", "info", "GHOST"])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura data info");
|
||
assert_eq!(out.status.code(), Some(1), "stdout: {}", String::from_utf8_lossy(&out.stdout));
|
||
let stderr = String::from_utf8(out.stderr).expect("utf-8 stderr");
|
||
assert!(stderr.contains("no recorded data for symbol"), "{stderr}");
|
||
assert!(stderr.contains("GHOST"), "names the unknown symbol: {stderr}");
|
||
}
|
||
|
||
/// Property (#300): `aura campaign show <CID>` prints the registered
|
||
/// document's exact canonical bytes to stdout — round-tripping them back
|
||
/// through `aura campaign register` re-derives the SAME content id (canonical
|
||
/// bytes are content-id-identical), and the store stays deduplicated at one
|
||
/// document. Gated on the shared GER40 archive; skips cleanly on a data
|
||
/// refusal (same guard shape as
|
||
/// `walkforward_stopless_defaults_the_regime_byte_identically_to_explicit_3_2`).
|
||
#[test]
|
||
fn campaign_show_round_trips_the_registered_document() {
|
||
// Ported onto a one-cell campaign document run through `exec` (#319 Task
|
||
// 8) — the `campaign show`/`process show` verbs themselves are untouched
|
||
// by the retirement; only the SETUP that used to mint a registered
|
||
// document via `walkforward --real` is replaced with a hand-authored
|
||
// document over the same real GER40 window.
|
||
let (cwd, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&cwd, "campaign-show-seed");
|
||
let proc_id = register_process_doc(&cwd, "campaignshow.process.json", GRID_THEN_WF_PROCESS_DOC_90_30_30);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "campaignshow",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1735776899999, "to_ms": 1767128220000 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [3] }},
|
||
"slow.length": {{ "kind": "I64", "values": [12] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": [] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "campaignshow.campaign.json", &doc);
|
||
let (run_out, run_code) = run_code_in(&cwd, &["exec", "campaignshow.campaign.json"]);
|
||
if run_code == Some(3) && (run_out.contains("no local data") || run_out.contains("no recorded geometry") || run_out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40 data");
|
||
return;
|
||
}
|
||
assert_eq!(run_code, Some(0), "exec must succeed: {run_out}");
|
||
|
||
let campaigns_dir = cwd.join("runs").join("campaigns");
|
||
let cid = std::fs::read_dir(&campaigns_dir)
|
||
.expect("campaigns dir exists")
|
||
.next()
|
||
.expect("exactly one campaign document")
|
||
.expect("readable dir entry")
|
||
.path()
|
||
.file_stem()
|
||
.expect("campaign filename has a stem")
|
||
.to_string_lossy()
|
||
.into_owned();
|
||
|
||
let show = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["campaign", "show", &cid])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura campaign show");
|
||
assert_eq!(
|
||
show.status.code(),
|
||
Some(0),
|
||
"campaign show must succeed: {}",
|
||
String::from_utf8_lossy(&show.stderr)
|
||
);
|
||
|
||
// Directly pin the #164 no-framing invariant: `show` must print the
|
||
// stored canonical bytes byte-for-byte, with no added trailing newline
|
||
// (a `println!` regression in `show_campaign` would still pass the
|
||
// re-register check below via `campaign_to_json` -> `content_id_of`
|
||
// stripping it back out before hashing, so this comparison is the only
|
||
// thing that would catch it).
|
||
let stored_bytes = std::fs::read(campaigns_dir.join(format!("{cid}.json"))).expect("read stored campaign document");
|
||
assert_eq!(
|
||
show.stdout, stored_bytes,
|
||
"campaign show must print the stored canonical bytes byte-for-byte, no framing"
|
||
);
|
||
|
||
let roundtrip_path = cwd.join("roundtrip.campaign.json");
|
||
std::fs::write(&roundtrip_path, &show.stdout).expect("write roundtrip campaign doc");
|
||
|
||
let register = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["campaign", "register", roundtrip_path.to_str().expect("utf-8 path")])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura campaign register");
|
||
assert_eq!(
|
||
register.status.code(),
|
||
Some(0),
|
||
"re-registering the shown document must succeed: {}",
|
||
String::from_utf8_lossy(®ister.stderr)
|
||
);
|
||
let register_out = String::from_utf8_lossy(®ister.stdout).into_owned();
|
||
assert!(
|
||
register_out.contains(&format!("registered campaign {cid}")),
|
||
"canonical bytes must re-register content-id-identically to {cid}: {register_out}"
|
||
);
|
||
|
||
let count = std::fs::read_dir(&campaigns_dir).expect("campaigns dir exists").count();
|
||
assert_eq!(count, 1, "the shown bytes dedup onto the same one stored document");
|
||
}
|
||
|
||
/// Property (#300): identical to `campaign_show_round_trips_the_registered_document`
|
||
/// but over the generated process document — `aura process show <PID>` prints
|
||
/// canonical bytes that re-register content-id-identically through `aura
|
||
/// process register`, with the store staying deduplicated at one document.
|
||
#[test]
|
||
fn process_show_round_trips_the_registered_document() {
|
||
// Ported onto a one-cell campaign document run through `exec` (#319 Task
|
||
// 8) — same setup rationale as the sibling `campaign_show_round_trips_
|
||
// the_registered_document` above.
|
||
let (cwd, _g) = fresh_project();
|
||
let bp_id = seed_blueprint(&cwd, "process-show-seed");
|
||
let proc_id = register_process_doc(&cwd, "processshow.process.json", GRID_THEN_WF_PROCESS_DOC_90_30_30);
|
||
let doc = format!(
|
||
r#"{{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "processshow",
|
||
"data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1735776899999, "to_ms": 1767128220000 }} ] }},
|
||
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
||
"axes": {{ "fast.length": {{ "kind": "I64", "values": [3] }},
|
||
"slow.length": {{ "kind": "I64", "values": [12] }} }} }} ],
|
||
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
||
"seed": 7,
|
||
"presentation": {{ "persist_taps": [], "emit": [] }}
|
||
}}"#,
|
||
);
|
||
write_doc(&cwd, "processshow.campaign.json", &doc);
|
||
let (run_out, run_code) = run_code_in(&cwd, &["exec", "processshow.campaign.json"]);
|
||
if run_code == Some(3) && (run_out.contains("no local data") || run_out.contains("no recorded geometry") || run_out.contains("no data for instrument")) {
|
||
eprintln!("skip: no local GER40 data");
|
||
return;
|
||
}
|
||
assert_eq!(run_code, Some(0), "exec must succeed: {run_out}");
|
||
|
||
let processes_dir = cwd.join("runs").join("processes");
|
||
let pid = std::fs::read_dir(&processes_dir)
|
||
.expect("processes dir exists")
|
||
.next()
|
||
.expect("exactly one process document")
|
||
.expect("readable dir entry")
|
||
.path()
|
||
.file_stem()
|
||
.expect("process filename has a stem")
|
||
.to_string_lossy()
|
||
.into_owned();
|
||
|
||
let show = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["process", "show", &pid])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura process show");
|
||
assert_eq!(
|
||
show.status.code(),
|
||
Some(0),
|
||
"process show must succeed: {}",
|
||
String::from_utf8_lossy(&show.stderr)
|
||
);
|
||
|
||
// Directly pin the #164 no-framing invariant (see the sibling campaign
|
||
// test's comment): `show` must print the stored canonical bytes
|
||
// byte-for-byte, with no added trailing newline.
|
||
let stored_bytes = std::fs::read(processes_dir.join(format!("{pid}.json"))).expect("read stored process document");
|
||
assert_eq!(
|
||
show.stdout, stored_bytes,
|
||
"process show must print the stored canonical bytes byte-for-byte, no framing"
|
||
);
|
||
|
||
let roundtrip_path = cwd.join("roundtrip.process.json");
|
||
std::fs::write(&roundtrip_path, &show.stdout).expect("write roundtrip process doc");
|
||
|
||
let register = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["process", "register", roundtrip_path.to_str().expect("utf-8 path")])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura process register");
|
||
assert_eq!(
|
||
register.status.code(),
|
||
Some(0),
|
||
"re-registering the shown document must succeed: {}",
|
||
String::from_utf8_lossy(®ister.stderr)
|
||
);
|
||
let register_out = String::from_utf8_lossy(®ister.stdout).into_owned();
|
||
assert!(
|
||
register_out.contains(&format!("registered process {pid}")),
|
||
"canonical bytes must re-register content-id-identically to {pid}: {register_out}"
|
||
);
|
||
|
||
let count = std::fs::read_dir(&processes_dir).expect("processes dir exists").count();
|
||
assert_eq!(count, 1, "the shown bytes dedup onto the same one stored document");
|
||
}
|
||
|
||
/// Property (#300): `show` over an unknown id refuses with the same
|
||
/// project-store-not-found prose the referential tier already uses for a
|
||
/// missing `process.ref`/`strategy.ref` (`ref_fault_prose`'s
|
||
/// `ProcessNotFound`/campaign-equivalent hint shape) — no project run needed,
|
||
/// so this test needs no archive gate.
|
||
#[test]
|
||
fn show_refuses_an_unknown_id_with_the_store_hint() {
|
||
let (cwd, _g) = fresh_project();
|
||
|
||
let campaign_show = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["campaign", "show", "deadbeef"])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura campaign show");
|
||
assert_eq!(campaign_show.status.code(), Some(1), "unknown campaign id is a runtime refusal");
|
||
let campaign_stderr = String::from_utf8_lossy(&campaign_show.stderr);
|
||
assert!(
|
||
campaign_stderr.contains("campaign deadbeef not found in the project store"),
|
||
"campaign show stderr: {campaign_stderr}"
|
||
);
|
||
|
||
let process_show = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["process", "show", "deadbeef"])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura process show");
|
||
assert_eq!(process_show.status.code(), Some(1), "unknown process id is a runtime refusal");
|
||
let process_stderr = String::from_utf8_lossy(&process_show.stderr);
|
||
assert!(
|
||
process_stderr.contains("process deadbeef not found in the project store"),
|
||
"process show stderr: {process_stderr}"
|
||
);
|
||
}
|
||
|
||
/// Property (#300): `show`'s not-found prose reuses the existing `prefix_hint`
|
||
/// authoring-trap detector (#194) — an id pasted WITH the display-only
|
||
/// `content:` prefix (the form `ref_fault_prose` already prints elsewhere for
|
||
/// an unresolved `process.ref`/`strategy.ref`) still gets the "drop the
|
||
/// 'content:' prefix" hint through the new `show` code path, not a bare
|
||
/// not-found. This is the one authoring mistake the store can name outright,
|
||
/// and it must survive `show` reusing `ref_fault_prose`'s helper rather than
|
||
/// re-deriving its own prose.
|
||
#[test]
|
||
fn show_hints_the_content_prefix_authoring_mistake() {
|
||
let (cwd, _g) = fresh_project();
|
||
|
||
let campaign_show = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["campaign", "show", "content:deadbeef"])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura campaign show");
|
||
assert_eq!(campaign_show.status.code(), Some(1), "unresolvable id is a runtime refusal");
|
||
let campaign_stderr = String::from_utf8_lossy(&campaign_show.stderr);
|
||
assert!(
|
||
campaign_stderr.contains("drop the 'content:' prefix"),
|
||
"campaign show must hint the content: authoring mistake: {campaign_stderr}"
|
||
);
|
||
|
||
let process_show = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["process", "show", "content:deadbeef"])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura process show");
|
||
assert_eq!(process_show.status.code(), Some(1), "unresolvable id is a runtime refusal");
|
||
let process_stderr = String::from_utf8_lossy(&process_show.stderr);
|
||
assert!(
|
||
process_stderr.contains("drop the 'content:' prefix"),
|
||
"process show must hint the content: authoring mistake: {process_stderr}"
|
||
);
|
||
}
|
||
|
||
/// Property (#300): `show` is scoped to its own document-kind store — a
|
||
/// content id that IS a real, registered document in one store (here: a
|
||
/// minimal sweep-only process) does not resolve through the OTHER verb
|
||
/// (`campaign show`). `show_process`/`show_campaign` are two near-identical
|
||
/// functions written in the same change (both `registry.get_*(id) -> Option
|
||
/// -> print! the bytes or format! a not-found`); this pins that they read
|
||
/// from their own store method and neither accidentally shares the other's
|
||
/// namespace or falls back to a generic content-blob lookup.
|
||
#[test]
|
||
fn show_is_scoped_to_its_own_document_store() {
|
||
let (cwd, _g) = fresh_project();
|
||
const SWEEP_ONLY_PROCESS: &str = r#"{"format_version":1,"kind":"process","name":"knob-sweep-only","pipeline":[{"block":"std::sweep","metric":"sqn_normalized","select":"argmax"}]}"#;
|
||
std::fs::write(cwd.join("knob.process.json"), SWEEP_ONLY_PROCESS).expect("write process doc");
|
||
|
||
let register = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["process", "register", "knob.process.json"])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura process register");
|
||
assert_eq!(
|
||
register.status.code(),
|
||
Some(0),
|
||
"process register must succeed: {}",
|
||
String::from_utf8_lossy(®ister.stderr)
|
||
);
|
||
let register_out = String::from_utf8_lossy(®ister.stdout).into_owned();
|
||
let pid = register_out
|
||
.lines()
|
||
.find(|l| l.starts_with("registered process "))
|
||
.expect("register line")
|
||
.trim_start_matches("registered process ")
|
||
.split(' ')
|
||
.next()
|
||
.expect("process id")
|
||
.to_string();
|
||
|
||
let process_show = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["process", "show", &pid])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura process show");
|
||
assert_eq!(
|
||
process_show.status.code(),
|
||
Some(0),
|
||
"process show must find the just-registered process: {}",
|
||
String::from_utf8_lossy(&process_show.stderr)
|
||
);
|
||
|
||
let campaign_show = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||
.args(["campaign", "show", &pid])
|
||
.current_dir(&cwd)
|
||
.output()
|
||
.expect("spawn aura campaign show");
|
||
assert_eq!(
|
||
campaign_show.status.code(),
|
||
Some(1),
|
||
"a process id must NOT resolve through campaign show: {}",
|
||
String::from_utf8_lossy(&campaign_show.stdout)
|
||
);
|
||
let campaign_stderr = String::from_utf8_lossy(&campaign_show.stderr);
|
||
assert!(
|
||
campaign_stderr.contains(&format!("campaign {pid} not found in the project store")),
|
||
"campaign show stderr: {campaign_stderr}"
|
||
);
|
||
}
|
||
|
||
/// Property (#302): `show` mirrors the prefix resolution `reproduce` gained
|
||
/// with #298 — a unique prefix of a registered content id, in the 8-hex form
|
||
/// every surface prints (family ids, run records), resolves to the registered
|
||
/// document, and `show` prints EXACTLY the bytes the full-id invocation
|
||
/// prints (the #164 byte-discipline: the stored canonical bytes, no framing).
|
||
/// One document per store makes any prefix of its id unique by construction.
|
||
/// Deliberately NOT pinned here: the ambiguous-prefix refusal (listing the
|
||
/// candidate ids) — content ids are hash-derived, not choosable, so two
|
||
/// documents sharing a printed prefix cannot be constructed honestly at this
|
||
/// layer; that arm belongs beside the resolution seam, where candidate id
|
||
/// lists are plain strings.
|
||
#[test]
|
||
fn show_resolves_a_unique_prefix_to_the_full_id_bytes() {
|
||
let (cwd, _g) = fresh_project();
|
||
|
||
fn aura(cwd: &Path, args: &[&str]) -> std::process::Output {
|
||
Command::new(BIN).args(args).current_dir(cwd).output().expect("spawn aura")
|
||
}
|
||
|
||
const PREFIX_PROBE_PROCESS: &str = r#"{"format_version":1,"kind":"process","name":"prefix-probe","pipeline":[{"block":"std::sweep","metric":"sqn_normalized","select":"argmax"}]}"#;
|
||
const PREFIX_PROBE_CAMPAIGN: &str = r#"{
|
||
"format_version": 1,
|
||
"kind": "campaign",
|
||
"name": "prefix-probe",
|
||
"data": { "instruments": ["GER40"], "windows": [ { "from_ms": 1, "to_ms": 2 } ] },
|
||
"strategies": [ { "ref": { "content_id": "9f3a" },
|
||
"axes": { "fast": { "kind": "I64", "values": [8] } } } ],
|
||
"process": { "ref": { "content_id": "4e2d" } },
|
||
"seed": 1,
|
||
"presentation": { "persist_taps": [], "emit": ["family_table"] }
|
||
}"#;
|
||
|
||
for (verb, file, doc) in [
|
||
("process", "probe.process.json", PREFIX_PROBE_PROCESS),
|
||
("campaign", "probe.campaign.json", PREFIX_PROBE_CAMPAIGN),
|
||
] {
|
||
std::fs::write(cwd.join(file), doc).expect("write document");
|
||
let register = aura(&cwd, &[verb, "register", file]);
|
||
assert_eq!(
|
||
register.status.code(),
|
||
Some(0),
|
||
"{verb} register must succeed: {}",
|
||
String::from_utf8_lossy(®ister.stderr)
|
||
);
|
||
let register_out = String::from_utf8_lossy(®ister.stdout).into_owned();
|
||
let marker = format!("registered {verb} ");
|
||
let id = register_out
|
||
.lines()
|
||
.find(|l| l.starts_with(&marker))
|
||
.expect("register line")
|
||
.trim_start_matches(marker.as_str())
|
||
.split(' ')
|
||
.next()
|
||
.expect("content id")
|
||
.to_string();
|
||
assert!(id.len() > 8, "content id longer than the printed 8-hex prefix form: {id}");
|
||
|
||
let full = aura(&cwd, &[verb, "show", &id]);
|
||
assert_eq!(
|
||
full.status.code(),
|
||
Some(0),
|
||
"{verb} show <full id> must succeed: {}",
|
||
String::from_utf8_lossy(&full.stderr)
|
||
);
|
||
assert!(!full.stdout.is_empty(), "{verb} show <full id> prints the document bytes");
|
||
|
||
// The headline: the 8-hex prefix — unique in this one-document store —
|
||
// resolves, and the output is byte-identical to the full-id output.
|
||
let prefix = &id[..8];
|
||
let short = aura(&cwd, &[verb, "show", prefix]);
|
||
assert_eq!(
|
||
short.status.code(),
|
||
Some(0),
|
||
"{verb} show {prefix} (unique prefix of {id}) must resolve: {}",
|
||
String::from_utf8_lossy(&short.stderr)
|
||
);
|
||
assert_eq!(
|
||
short.stdout, full.stdout,
|
||
"{verb} show via unique prefix must print the identical document bytes"
|
||
);
|
||
}
|
||
}
|