Files
Aura/crates/aura-cli/tests/cli_run.rs
T
Brummel 5856cadadd refactor(cli): retire the r-breakout demo — topology now lives only as data (#159 cut 2)
Cut 2 of the hard-wired demo retirement, applying the r-sma template one level
down. The r-breakout signal leg was fused into r_breakout_graph (which also built
the whole pip/R harness inline), so this cut first carves the signal leg out as a
pure price->bias Composite, ships it as a proven blueprint example, then deletes
the fused builder and the built-in --strategy r-breakout surface. The r-breakout
topology now exists only as data (crates/aura-cli/examples/r_breakout{,_open}.json).

Carved: r_breakout_signal(channel) -> Composite, a verbatim lift of the fused
builder's signal leg (Delay/RollingMax/RollingMin/Gt/Latch/Sub) exposing the raw
exposure as "bias"; the generic wrap_r/run_signal_r path (unchanged) supplies the
pip/R harness. It is #[cfg(test)] — its only role is regenerating the examples and
pinning them to a faithful serialisation; production loads the shipped JSON, never
this builder (so no dead-code residue, no production topology constructor — #159's
invariant).

Proof: before the fused builder was deleted, an equivalence test proved the carved
signal, wrapped and run, reproduces the retiring r_breakout_graph grade byte-for-byte
on the synthetic stream at channel=3 with the shared default R-stop (metric
computation is identical on both paths). That gate is deleted with the builder it
references; the durable anchors that survive are the byte-identity of the shipped
examples to the carve, the loaded-example-grades-identically-to-the-carve proof, and
the closed-ness / open-axis introspect anchors (channel_hi.length, channel_lo.length
per-node — ganging is the separate #61).

Deleted: r_breakout_graph, r_breakout_sweep_family (aura sweep --strategy r-breakout),
Strategy::RBreakout + all four match arms, the r-breakout token from the --strategy
usage strings and docs, and the now-dead --channel flag + RGrid.channel field (its
sole reader was r_breakout_sweep_family). The data successors: aura run
examples/r_breakout.json, and aura sweep examples/r_breakout_open.json --axis
channel_hi.length --axis channel_lo.length.

Test surface: the three built-in-surface sweep tests drop; the walkforward
valid-but-unsupported-token test is re-pointed to momentum (the property is generic
and stays live until Cut 4); and a new negative pins that --strategy r-breakout on
both sweep and walkforward now falls into the generic usage error, not a
special-cased message — catching a half-finished retirement that left the token
parseable.

Verification (own, not the workflow's report): full `cargo test --workspace` green
(0 failed across every binary); `cargo clippy --workspace --all-targets -- -D warnings`
clean, no dead-code residue; the r-sma introspect + grade anchors and the
dissolved-verb real goldens stay green. The implement-loop ran both plan tasks in one
pass (task_range was not honoured); the tree was verified by hand — grep-confirmed the
retired surface is gone and the carve is #[cfg(test)]-scoped.

refs #159
2026-07-07 19:25:13 +02:00

4257 lines
210 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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::process::Command;
/// 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 = std::env::temp_dir().join(format!("aura-cli-{}-{}", std::process::id(), name));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create temp cwd");
dir
}
#[test]
fn run_prints_json_and_exits_zero() {
let out = Command::new(BIN).arg("run").output().expect("spawn aura run");
assert!(out.status.success(), "exit status: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
// exactly one line (the JSON object + a trailing newline from println!).
assert_eq!(stdout.lines().count(), 1, "stdout was: {stdout:?}");
let line = stdout.trim_end();
// canonical cycle-0009 JSON shape: nested manifest + metrics, stable keys.
// The manifest leads with a `commit` field; its value is the build's git
// identity (asserted by `run_manifest_commit_carries_real_git_head`), so
// this shape check pins only the surrounding structure, not the value.
assert!(line.starts_with("{\"manifest\":{\"commit\":\""), "got: {line}");
assert!(line.contains("\"broker\":\"sim-optimal(pip_size=0.0001)\""), "got: {line}");
assert!(line.contains("\"window\":[1,7]"), "got: {line}");
assert!(line.contains("\"metrics\":{\"total_pips\":"), "got: {line}");
// the integer sign-flip count is stable across float renderings.
assert!(line.ends_with("\"bias_sign_flips\":1}}"), "got: {line}");
}
/// Property: the RunManifest is self-identifying — its `commit` carries the
/// real code identity that produced the run, not a placeholder. C8/C18 audit
/// trail (this run = this commit): once runs are archived, `manifest.commit`
/// must point back at the source. The build captures the current git HEAD at
/// compile time; the `"unknown"` literal survives only when there is no git.
///
/// Structural assertion (not exact-equality) on purpose: the working tree may
/// be dirty when this runs (e.g. this very test file uncommitted), so a build
/// that appends a `-dirty` suffix would not equal `rev-parse HEAD` verbatim.
/// The honest contract is: the field CONTAINS the current HEAD sha AND is not
/// the bare `"unknown"` placeholder — true regardless of dirtiness/suffix.
#[test]
fn run_manifest_commit_carries_real_git_head() {
// The current HEAD sha, obtained the same way the build is expected to.
let head = Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.expect("spawn git rev-parse HEAD");
assert!(head.status.success(), "git rev-parse HEAD failed");
let head_sha = String::from_utf8(head.stdout).expect("utf-8 sha");
let head_sha = head_sha.trim();
assert!(!head_sha.is_empty(), "empty HEAD sha");
// Drive the binary and pull `manifest.commit` out of the single JSON line.
let out = Command::new(BIN).arg("run").output().expect("spawn aura run");
assert!(out.status.success(), "exit status: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let line = stdout.trim_end();
// Locate the `"commit":"<value>"` value without a JSON dependency (the
// sibling tests parse this output by substring too).
let key = "\"commit\":\"";
let start = line.find(key).expect("manifest has a commit field") + key.len();
let rest = &line[start..];
let end = rest.find('"').expect("commit field is a closed string");
let commit = &rest[..end];
assert_ne!(
commit, "unknown",
"manifest.commit is still the placeholder: {line}"
);
assert!(
commit.contains(head_sha),
"manifest.commit {commit:?} should contain the current HEAD sha {head_sha:?}; line: {line}"
);
}
/// Property: `aura run` is strict about its argument vector — a trailing
/// unexpected token (e.g. a typo'd flag like `aura run --sweep`) takes the
/// error path (usage on stderr, exit 2), never a silent full run. This is the
/// strict reading ratified in #16: keying only on the first token being `run`
/// and ignoring the rest would let a typo masquerade as a successful run.
/// The same test pins positive-preservation — bare `aura run` still emits the
/// JSON report on stdout and exits 0 — so the strict guard cannot regress the
/// happy path.
#[test]
fn run_with_trailing_token_is_strict_and_exits_two() {
// Negative: an unexpected trailing token is rejected like a bad-args call.
let out = Command::new(BIN)
.args(["run", "extra-garbage"])
.output()
.expect("spawn aura run extra-garbage");
assert_eq!(
out.status.code(),
Some(2),
"`aura run extra-garbage` must reject the trailing token: {:?}",
out.status
);
assert!(
out.stdout.is_empty(),
"stdout should be empty on the usage path, got: {:?}",
String::from_utf8_lossy(&out.stdout)
);
let stderr = String::from_utf8(out.stderr).expect("utf-8 stderr");
assert!(stderr.contains("Usage"), "stderr was: {stderr:?}");
// Positive-preservation: bare `aura run` still runs and prints the report.
let ok = Command::new(BIN).arg("run").output().expect("spawn aura run");
assert_eq!(
ok.status.code(),
Some(0),
"bare `aura run` must still succeed: {:?}",
ok.status
);
let ok_stdout = String::from_utf8(ok.stdout).expect("utf-8 stdout");
assert!(
ok_stdout.trim_start().starts_with("{\"manifest\":"),
"bare `aura run` should print the JSON report, got: {ok_stdout:?}"
);
}
#[test]
fn run_real_no_geometry_symbol_refuses_with_exit_1() {
// 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.
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--real", "NONEXISTENT"])
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(1), "expected exit 1");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no recorded geometry for symbol 'NONEXISTENT'"),
"expected the per-instrument-pip refusal message, got: {stderr}"
);
}
/// 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.
#[test]
fn run_real_refusal_names_no_authored_floor() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--real", "NONEXISTENT"])
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(1), "expected exit 1");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("instrument table"),
"refusal must not point at a removed authored table, got: {stderr}"
);
assert!(
!stderr.contains("vetted"),
"refusal must not use the removed vetted-floor vocabulary, got: {stderr}"
);
assert!(
!stderr.contains("instrument spec"),
"refusal must not reference the removed authored instrument floor, got: {stderr}"
);
}
/// Property: the un-specced-symbol refusal is a CLEAN refusal — it emits NOTHING
/// on stdout, never a partial `RunReport`. The #22 acceptance is "refuse INSTEAD
/// of emitting nonsense": the pip lookup precedes harness construction and data
/// access, so a `None` spec must short-circuit to `exit(1)` before any JSON line
/// can reach stdout. A regression that moved the lookup after a partial run would
/// leak a `{"manifest":...}` line here while still exiting non-zero; this pins
/// that it cannot. Archive-independent (the lookup never touches local data).
#[test]
fn run_real_no_geometry_symbol_emits_no_stdout_report() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--real", "NONEXISTENT"])
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(1), "expected exit 1: {:?}", out.status);
assert!(
out.stdout.is_empty(),
"the refusal must not leak a partial report to stdout, got: {:?}",
String::from_utf8_lossy(&out.stdout)
);
}
/// Property: the pip-honesty refusal keys on the SYMBOL having no recorded geometry,
/// not on `--real` being taken 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
/// usage path ("no local data for symbol"). Either way the "no recorded geometry"
/// string is absent. This distinguishes the per-instrument-pip honesty lever from
/// the unrelated arg-grammar / no-data error paths, and is archive-independent
/// (asserts only on the absence of the pip-refusal string, true whether or not
/// EURUSD data is on the machine).
#[test]
fn run_real_sidecar_symbol_never_hits_the_pip_refusal() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--real", "EURUSD"])
.output()
.expect("spawn aura");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("no recorded geometry"),
"a symbol with a recorded sidecar must clear the pip lookup, never the pip-refusal; stderr: {stderr}"
);
// It resolves to exactly one of the two legitimate outcomes: a clean report
// (exit 0, data present) or the distinct no-local-data refusal (exit 1).
match out.status.code() {
Some(0) => assert!(
String::from_utf8_lossy(&out.stdout)
.trim_start()
.starts_with("{\"manifest\":"),
"exit 0 must carry a JSON report, got: {:?}",
String::from_utf8_lossy(&out.stdout)
),
Some(1) => assert!(
stderr.contains("no local data for symbol 'EURUSD'"),
"exit 1 for a symbol with a recorded sidecar must be the no-data path, got: {stderr}"
),
other => panic!("unexpected exit for a real run with a recorded sidecar: {other:?}; stderr: {stderr}"),
}
}
/// Property: a recorded-sidecar symbol threads its looked-up pip all the way to the
/// binary's emitted manifest — `aura run --real 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 (criterion 2: GER40=1.0
/// vs EURUSD=0.0001 vs no-geometry→refuse): the metadata channel reaches stdout, so
/// equity is honestly scaled per instrument. Gated on local GER40 data — skip
/// cleanly when the archive is absent (the project's skip-on-no-data convention),
/// so it never fails on a data-less machine; the no-data path is the distinct
/// "no local data" refusal (exit 1), not the pip-refusal, asserted above.
#[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: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--real", "GER40", "--from", FROM_MS, "--to", TO_MS])
.output()
.expect("spawn aura");
// Skip on a data-less machine: a recorded-sidecar symbol with no local data takes
// the distinct no-data path (exit 1, "no local data"), never the pip-refusal.
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data for symbol 'GER40'"),
"exit 1 for GER40 must be the no-data path, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let line = stdout.trim_end();
// the looked-up index pip (1.0 -> "1") reaches the manifest, not the FX 0.0001.
assert!(
line.contains("\"broker\":\"sim-optimal(pip_size=1)\""),
"GER40 must render the index pip in the manifest, got: {line}"
);
}
/// Property: 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 when USDJPY has
/// no local geometry/data (no sidecar → "no recorded geometry"; sidecar but no bars
/// → "no local 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: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--real", "USDJPY", "--from", FROM_MS, "--to", TO_MS])
.output()
.expect("spawn aura");
// Skip on a host without USDJPY geometry/data — either refusal is a clean skip,
// sourced from recorded geometry, never an authored-table miss.
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
if stderr.contains("no recorded geometry") || stderr.contains("no local data") {
eprintln!("skip: no local USDJPY geometry/data");
return;
}
}
assert_eq!(out.status.code(), Some(0), "USDJPY should resolve and run; exit: {:?}, stderr: {}",
out.status, String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(
stdout.contains("pip_size=0.01"),
"USDJPY must render the JPY sidecar pip (0.01) in the manifest, got: {}",
stdout.trim_end()
);
}
/// `aura run <blueprint.json>` loads a serialized signal blueprint and runs it
/// end-to-end (#165): exit 0, a RunReport whose manifest carries the topology_hash.
#[test]
fn aura_run_loads_and_runs_a_blueprint_file() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "examples/r_sma.json"])
.output()
.expect("spawn aura run blueprint");
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");
assert!(stdout.contains("\"topology_hash\":\""), "manifest carries the topology hash: {stdout}");
}
/// A blueprint referencing a node type outside the closed vocabulary fails clean
/// at load (the #160 closed-set guard at the data-plane face; invariant 9), and
/// (#184) the failure is phrased as house-style prose — matching the `graph
/// build` presenter's wording — never the raw `UnknownNodeType` Debug leak.
#[test]
fn aura_run_rejects_an_unknown_node_blueprint() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "tests/fixtures/unknown_node.json"])
.output()
.expect("spawn aura run unknown");
assert_ne!(out.status.code(), Some(0), "an unknown node type must fail the run");
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains(r#"unknown node type "Nope""#),
"house-style prose names the type: {stderr}"
);
assert!(
!stderr.contains("UnknownNodeType"),
"does not leak the Debug variant name: {stderr}"
);
}
/// Property (#175, refuse-don't-guess): built-in-only flags on `aura run
/// <blueprint.json>` are REFUSED, not silently dropped. The loaded-blueprint
/// grammar accepts only `--params`/`--seed`/`--real`/`--from`/`--to`; the removed
/// hand-parser rejected any other flag (exit 2). clap's optional `[blueprint]`
/// positional makes `--trace` structurally parseable (it stays on `RunCmd` for
/// the built-in-harness grammar), so the dispatch guard must re-reject it —
/// mirroring the sweep/mc blueprint branches, which reject their non-branch
/// flags exhaustively. A regression that dropped the guard would silently
/// no-op a user-provided flag. (The cost flags this test used to also cover
/// were removed from `RunCmd` entirely in #159's builtin retirement; clap now
/// rejects them as unknown args before dispatch ever sees them, so they no
/// longer exercise this guard.)
#[test]
fn run_blueprint_rejects_builtin_only_flags_exit_two() {
let args = ["run", "examples/r_sma.json", "--trace", "foo"];
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(args)
.output()
.expect("spawn aura run blueprint + builtin-only flag");
assert_eq!(
out.status.code(),
Some(2),
"{args:?} must be refused (exit 2), got {:?}; stderr: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
assert!(
out.stdout.is_empty(),
"{args:?} must not emit a report on stdout: {:?}",
String::from_utf8_lossy(&out.stdout)
);
}
#[test]
fn run_trace_persists_taps_and_plain_run_writes_no_traces() {
let cwd = temp_cwd("run-trace");
// plain `aura run` (no --trace) persists nothing to disk.
let plain = Command::new(BIN).arg("run").current_dir(&cwd).output().expect("spawn aura run");
assert!(plain.status.success(), "plain run exit: {:?}", plain.status);
assert!(!cwd.join("runs/traces").exists(), "plain run must write no trace files");
// `aura run --trace demo` persists the two taps + index, and still prints the report.
let traced = Command::new(BIN)
.args(["run", "--trace", "demo"])
.current_dir(&cwd)
.output()
.expect("spawn aura run --trace");
assert!(traced.status.success(), "traced run exit: {:?}", traced.status);
let stdout = String::from_utf8(traced.stdout).expect("utf-8 stdout");
assert!(stdout.trim_end().starts_with("{\"manifest\":{"), "report still printed: {stdout:?}");
assert!(cwd.join("runs/traces/demo/index.json").exists(), "index.json written");
assert!(cwd.join("runs/traces/demo/equity.json").exists(), "equity tap written");
assert!(cwd.join("runs/traces/demo/exposure.json").exists(), "exposure tap written");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: a persisted `<tap>.json` is the **columnar SoA form** a downstream
/// chart can consume, not an opaque blob — it carries the four
/// `tap`/`kinds`/`ts`/`columns` keys, the equity tap is kind-tagged `["F64"]`
/// (C7: the base type survives on disk), and the SoA invariant holds: `ts` has one
/// entry per recorded cycle and EVERY column has exactly that many entries (parallel
/// arrays, one timestamp per row). This is acceptance criterion 2 at the built-binary
/// file boundary — the unit round-trip pins the in-memory transpose, but only an
/// end-to-end run pins that the bytes actually written to disk hold that shape.
/// Asserts on structure + array-length parity, never on the exact float values
/// (pip equity's low bits vary build-to-build), so it stays deterministic.
#[test]
fn run_trace_persists_columnar_soa_shape_on_disk() {
let cwd = temp_cwd("run-trace-shape");
let out = Command::new(BIN)
.args(["run", "--trace", "demo"])
.current_dir(&cwd)
.output()
.expect("spawn aura run --trace");
assert!(out.status.success(), "traced run exit: {:?}", out.status);
let equity = std::fs::read_to_string(cwd.join("runs/traces/demo/equity.json"))
.expect("read equity.json");
// The four SoA keys are present, equity is f64-kind-tagged, and the parallel
// arrays open as expected (the synthetic sample harness has a single column).
assert!(equity.contains("\"tap\":\"equity\""), "tap name missing: {equity}");
assert!(equity.contains("\"kinds\":[\"F64\"]"), "F64 kind tag missing: {equity}");
assert!(equity.contains("\"ts\":["), "ts axis missing: {equity}");
assert!(equity.contains("\"columns\":[["), "columns array missing: {equity}");
// SoA invariant: one timestamp per row, and every column has exactly that many
// entries. Count the ts entries (the synthetic window is 7 cycles) and the
// single column's entries by their comma-separated cardinality.
let ts_body = json_array_body(&equity, "\"ts\":[");
let col_body = json_array_body(&equity, "\"columns\":[[");
let ts_len = ts_body.split(',').count();
let col_len = col_body.split(',').count();
assert_eq!(ts_len, 7, "synthetic run records 7 cycles; ts was: {ts_body:?}");
assert_eq!(
col_len, ts_len,
"SoA parity broken: ts has {ts_len} entries but the column has {col_len}; file: {equity}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: `index.json` is the run's **table of contents** — it carries the run's
/// `RunManifest` (so a chart page can show run context: commit, broker, window) and
/// the tap names in a **deterministic order** (`["equity","exposure"]`). The serve
/// half (`aura chart`, iteration 2) reads taps in this index order; a regression that
/// dropped the manifest, or made the tap list order-unstable, would silently corrupt
/// the chart header / the per-series mapping. Asserts on the manifest's structural
/// presence (the commit value is the volatile git HEAD, pinned elsewhere) and the
/// exact deterministic tap order.
#[test]
fn run_trace_index_carries_manifest_and_ordered_tap_list() {
let cwd = temp_cwd("run-trace-index");
let out = Command::new(BIN)
.args(["run", "--trace", "demo"])
.current_dir(&cwd)
.output()
.expect("spawn aura run --trace");
assert!(out.status.success(), "traced run exit: {:?}", out.status);
let index = std::fs::read_to_string(cwd.join("runs/traces/demo/index.json"))
.expect("read index.json");
// The manifest is embedded with its identifying context for the chart header.
assert!(index.contains("\"manifest\":{\"commit\":\""), "manifest missing: {index}");
assert!(index.contains("\"window\":[1,7]"), "window missing from manifest: {index}");
assert!(
index.contains("\"broker\":\"sim-optimal(pip_size=0.0001)\""),
"broker label missing: {index}"
);
// The tap list is the deterministic read order the serve path depends on.
assert!(
index.contains("\"taps\":[\"equity\",\"exposure\"]"),
"tap order must be deterministic [equity, exposure]: {index}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: `--trace` is an **orthogonal modifier** — it composes with `aura run
/// --real <SYM>`, not only plain `run`, and is strictly **additive**: the looked-up
/// instrument pip still reaches stdout byte-for-byte while the same two taps land on
/// disk. The spec's headline is "one shared persist helper, every run form"; the
/// existing E2E only exercised the synthetic `run` path. Gated on local GER40 data
/// (skip cleanly when absent, the project convention) so it never fails on a
/// data-less machine; the no-data path is the distinct "no local data" refusal.
#[test]
fn run_real_with_trace_persists_taps_and_keeps_stdout_unchanged() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("run-real-trace");
let out = Command::new(BIN)
.args(["run", "--real", "GER40", "--from", FROM_MS, "--to", TO_MS, "--trace", "ger"])
.current_dir(&cwd)
.output()
.expect("spawn aura run --real --trace");
// Skip on a data-less machine: GER40 with no local data takes the
// distinct no-data path (exit 1, runtime), never the pip-refusal, and writes no traces.
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data for symbol 'GER40'"),
"exit 1 for GER40 --trace must be the no-data path, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
let _ = std::fs::remove_dir_all(&cwd);
return;
}
assert_eq!(out.status.code(), Some(0), "real --trace exit: {:?}", out.status);
// Additive: the index-pip report still reaches stdout unchanged.
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let line = stdout.trim_end();
assert!(line.starts_with("{\"manifest\":{"), "report still printed: {line}");
assert!(
line.contains("\"broker\":\"sim-optimal(pip_size=1)\""),
"real --trace must keep the GER40 index pip on stdout: {line}"
);
// And the same two taps landed under the named run dir.
assert!(cwd.join("runs/traces/ger/index.json").exists(), "index.json written");
assert!(cwd.join("runs/traces/ger/equity.json").exists(), "equity tap written");
assert!(cwd.join("runs/traces/ger/exposure.json").exists(), "exposure tap written");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Extract the body of a JSON array opened by `marker` (e.g. `"ts":[`) up to its
/// closing `]`, by substring — no serde dependency, mirroring the sibling tests'
/// substring parsing. Used only to count comma-separated entries for the SoA
/// length-parity check.
fn json_array_body<'a>(json: &'a str, marker: &str) -> &'a str {
let start = json.find(marker).expect("array marker present") + marker.len();
let rest = &json[start..];
let end = rest.find(']').expect("array is closed");
&rest[..end]
}
#[test]
fn chart_emits_self_contained_html_for_a_persisted_run() {
let cwd = temp_cwd("chart-serve");
// persist a run first (iteration 1), then chart it.
let traced = Command::new(BIN).args(["run", "--trace", "demo"]).current_dir(&cwd).output().expect("spawn run --trace");
assert!(traced.status.success(), "run --trace exit: {:?}", traced.status);
let chart = Command::new(BIN).args(["chart", "demo"]).current_dir(&cwd).output().expect("spawn chart");
assert!(chart.status.success(), "chart exit: {:?}", chart.status);
let html = String::from_utf8(chart.stdout).expect("utf-8 html");
assert!(html.starts_with("<!doctype html>"), "not an HTML doc: {:?}", &html[..html.len().min(40)]);
assert!(html.contains("window.AURA_TRACES = {"), "AURA_TRACES not injected");
assert!(html.contains("\"equity\""), "equity series missing from the chart");
assert!(html.contains("uPlot=function"), "vendored uPlot not inlined");
// a missing run is a runtime failure (stderr + exit 1), not a panic.
let missing = Command::new(BIN).args(["chart", "nope"]).current_dir(&cwd).output().expect("spawn chart nope");
assert_eq!(missing.status.code(), Some(1), "missing run must exit 1");
assert!(!missing.status.success());
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: the `--panels` flag is an honest CLI dispatch axis — `chart <name>`
/// serves the overlay page and `chart <name> --panels` serves the panels page over
/// the SAME persisted run. The mode the viewer reads (`AURA_TRACES.mode`) reflects
/// which arm dispatched: a `--panels`→Overlay miswiring would leave every shape and
/// render test green while silently ignoring the flag, so this pins it at the
/// binary's observable stdout.
#[test]
fn chart_panels_flag_selects_panels_mode() {
let cwd = temp_cwd("chart-panels");
let traced = Command::new(BIN).args(["run", "--trace", "demo"]).current_dir(&cwd).output().expect("spawn run --trace");
assert!(traced.status.success(), "run --trace exit: {:?}", traced.status);
// default (no flag) -> overlay.
let overlay = Command::new(BIN).args(["chart", "demo"]).current_dir(&cwd).output().expect("spawn chart");
assert!(overlay.status.success(), "chart exit: {:?}", overlay.status);
let overlay_html = String::from_utf8(overlay.stdout).expect("utf-8 html");
assert!(overlay_html.contains("\"mode\":\"overlay\""), "default chart must serve overlay mode");
assert!(!overlay_html.contains("\"mode\":\"panels\""), "default chart must not serve panels mode");
// --panels -> panels, over the very same persisted run.
let panels = Command::new(BIN).args(["chart", "demo", "--panels"]).current_dir(&cwd).output().expect("spawn chart --panels");
assert!(panels.status.success(), "chart --panels exit: {:?}", panels.status);
let panels_html = String::from_utf8(panels.stdout).expect("utf-8 html");
assert!(panels_html.contains("\"mode\":\"panels\""), "--panels must serve panels mode");
assert!(!panels_html.contains("\"mode\":\"overlay\""), "--panels must not fall back to overlay");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: `--tap <nonexistent>` on a single-run chart is a CLEAN refusal at the
/// CLI seam — refuse-don't-guess (C10). The builder's no-tap `Err` is unit-tested,
/// but this pins the user-facing wiring: `filter_to_tap` returning `Err` must reach
/// `eprintln!` + `exit(1)`, never a panic and never a chart of zero series. A
/// regression that swallowed the `Err` (e.g. charting an empty `ChartData`) would
/// exit 0 with an empty page; this fails it. Archive-local (persists its own run).
#[test]
fn chart_tap_nonexistent_on_run_refuses_with_exit_1() {
let cwd = temp_cwd("chart-tap-missing");
// persist a single run; its taps are `equity` / `exposure`, never `nope`.
let traced = Command::new(BIN).args(["run", "--trace", "demo"]).current_dir(&cwd).output().expect("spawn run --trace");
assert!(traced.status.success(), "run --trace exit: {:?}", traced.status);
let out = Command::new(BIN).args(["chart", "demo", "--tap", "nope"]).current_dir(&cwd).output().expect("spawn chart --tap");
assert_eq!(out.status.code(), Some(1), "nonexistent tap 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("run has no tap named 'nope'"),
"expected the no-such-tap refusal message, got: {stderr}"
);
// #131: the refusal lists the valid taps so the user can correct the typo
// (the demo run's taps are `equity` and `exposure`).
assert!(
stderr.contains("available:") && stderr.contains("equity") && stderr.contains("exposure"),
"the refusal should enumerate the available taps, got: {stderr}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// 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);
}
#[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: `--help`/`-h` is the success-path help affordance, not the error
/// path. A newcomer's reflex first command (C22 first-contact ergonomics, #20)
/// prints the usage to stdout and exits 0; the conventional `-h` alias behaves
/// identically. An *unknown* subcommand keeps the error path (exit 2) so the
/// help fix does not regress bad-args handling.
#[test]
fn help_flag_prints_usage_to_stdout_and_exits_zero() {
for flag in ["--help", "-h"] {
let out = Command::new(BIN).arg(flag).output().expect("spawn aura --help");
assert_eq!(out.status.code(), Some(0), "`aura {flag}` exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(
!stdout.trim().is_empty(),
"`aura {flag}` should print help to stdout, got: {stdout:?}"
);
// The usage names the only subcommand a newcomer can run.
assert!(
stdout.contains("run"),
"`aura {flag}` help should mention the `run` subcommand, got: {stdout:?}"
);
}
// Negative-preservation: an unknown subcommand is still the error path.
let out = Command::new(BIN).arg("frobnicate").output().expect("spawn aura frobnicate");
assert_eq!(
out.status.code(),
Some(2),
"unknown subcommand must stay exit 2: {:?}",
out.status
);
}
/// Property (post-clap migration, #175): subcommand help is SCOPED — `aura <sub>
/// --help` (and `-h`) prints that subcommand's own options to stdout and exits 0,
/// with nothing on stderr, for every subcommand. This supersedes the retired #131
/// "uniform (byte-identical) help" surface: clap gives each subcommand its own
/// scoped Options section rather than one shared global usage blob.
#[test]
fn per_subcommand_help_is_scoped_stdout_exit_zero() {
for sub in ["run", "chart", "sweep", "walkforward", "mc", "graph", "runs"] {
for help in ["--help", "-h"] {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([sub, help]).output().expect("spawn");
assert_eq!(out.status.code(), Some(0), "{sub} {help} exit");
assert!(!out.stdout.is_empty(), "{sub} {help} stdout empty");
assert!(out.stderr.is_empty(), "{sub} {help} stderr not empty: {:?}",
String::from_utf8_lossy(&out.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}");
}
#[test]
fn sweep_prints_four_family_json_lines_and_exits_zero() {
let cwd = temp_cwd("sweep4");
let out = Command::new(BIN).arg("sweep").current_dir(&cwd).output().expect("spawn aura sweep");
assert!(out.status.success(), "exit status: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert_eq!(stdout.lines().count(), 4, "stdout was: {stdout:?}");
let first = stdout.lines().next().unwrap();
// each line is now a family member: the assigned `family_id` plus the nested
// RunReport (C18/C21 lineage). The first run assigns `sweep-0`; the nested
// report is serialized via `serde_json`, so the manifest's fields lead with
// `broker` (the commit value is the volatile git HEAD, pinned by params below).
assert!(first.starts_with("{\"family_id\":\"sweep-0\",\"report\":{\"manifest\":{"), "got: {stdout}");
assert!(
first.contains("\"params\":[[\"signals.trend.fast.length\",{\"I64\":2}],[\"signals.trend.slow.length\",{\"I64\":4}],[\"signals.momentum.fast.length\",{\"I64\":2}],[\"signals.momentum.slow.length\",{\"I64\":4}],[\"signals.momentum.signal.length\",{\"I64\":3}],[\"signals.blend.weights[0]\",{\"F64\":1.0}],[\"signals.blend.weights[1]\",{\"F64\":1.0}],[\"bias.scale\",{\"F64\":0.5}]]"),
"got: {stdout}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
#[test]
fn sweep_with_trailing_token_is_strict_and_exits_two() {
// strict-arg reading (#16): an unexpected trailing token is a usage error.
let out = Command::new(BIN).args(["sweep", "extra"]).output().expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "exit status: {:?}", out.status);
}
/// Property: `aura mc` runs the built-in Monte-Carlo family end-to-end through the
/// binary — it prints one member line per seed (carrying the assigned `family_id`)
/// plus one `mc_aggregate` line, exits 0, and persists the draws as a discoverable
/// `MonteCarlo` family (C12 axis 4 / C18/C21). Driven through the built binary so
/// it pins the observable CLI contract, not just the in-crate render helper.
#[test]
fn mc_runs_persists_a_monte_carlo_family_and_lists_it() {
let cwd = temp_cwd("mc-family");
let out = Command::new(BIN).arg("mc").current_dir(&cwd).output().expect("spawn aura mc");
assert!(out.status.success(), "mc exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let lines: Vec<&str> = stdout.lines().collect();
// three built-in seeds -> three member lines + one aggregate line.
assert_eq!(lines.len(), 4, "mc stdout: {stdout:?}");
for line in &lines[..3] {
assert!(line.contains("\"family_id\":\"mc-0\""), "member missing family_id: {line}");
assert!(line.contains("\"seed\":"), "member missing seed: {line}");
}
assert!(lines[3].contains("\"mc_aggregate\":"), "missing aggregate line: {}", lines[3]);
// the run persisted the draws as a discoverable MonteCarlo family.
let fams = Command::new(BIN).args(["runs", "families"]).current_dir(&cwd).output().expect("spawn families");
assert!(fams.status.success(), "families exit: {:?}", fams.status);
let fams_out = String::from_utf8(fams.stdout).expect("utf-8");
assert_eq!(fams_out.lines().count(), 1, "families: {fams_out:?}");
assert!(fams_out.contains("\"family_id\":\"mc-0\""), "families: {fams_out:?}");
assert!(fams_out.contains("\"kind\":\"MonteCarlo\""), "families: {fams_out:?}");
assert!(fams_out.contains("\"members\":3"), "families: {fams_out:?}");
let _ = std::fs::remove_dir_all(&cwd);
}
#[test]
fn runs_families_list_and_per_family_rank_across_invocations() {
let cwd = temp_cwd("runs-flow");
// two sweeps accumulate two families (sweep-0, sweep-1) over time, each a
// related set of 4 records (C18/C21 lineage), in the sibling family store.
for _ in 0..2 {
let out = Command::new(BIN).arg("sweep").current_dir(&cwd).output().expect("spawn sweep");
assert!(out.status.success(), "sweep exit: {:?}", out.status);
}
// `runs families` shows both stored families in first-seen order, each with
// its kind + member count.
let fams = Command::new(BIN).args(["runs", "families"]).current_dir(&cwd).output().expect("spawn families");
assert!(fams.status.success(), "families exit: {:?}", fams.status);
let fams_out = String::from_utf8(fams.stdout).expect("utf-8");
let fam_lines: Vec<&str> = fams_out.lines().collect();
assert_eq!(fam_lines.len(), 2, "families: {fams_out:?}");
assert!(fam_lines[0].contains("\"family_id\":\"sweep-0\""), "fam0: {fams_out:?}");
assert!(fam_lines[1].contains("\"family_id\":\"sweep-1\""), "fam1: {fams_out:?}");
assert!(fam_lines.iter().all(|l| l.contains("\"members\":4")), "members: {fams_out:?}");
// `runs family sweep-0 rank total_pips`: the family's 4 members, highest
// total_pips first.
let rank = Command::new(BIN)
.args(["runs", "family", "sweep-0", "rank", "total_pips"])
.current_dir(&cwd)
.output()
.expect("spawn rank");
assert!(rank.status.success(), "rank exit: {:?}", rank.status);
let rank_out = String::from_utf8(rank.stdout).expect("utf-8");
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 = Command::new(BIN)
.args(["runs", "family", "sweep-0", "rank", "bogus"])
.current_dir(&cwd)
.output()
.expect("spawn bogus");
assert_eq!(bogus.status.code(), Some(2), "bogus exit: {:?}", bogus.status);
// an unknown family id is an empty family: zero lines, exit 0.
let unknown = Command::new(BIN)
.args(["runs", "family", "nope-9"])
.current_dir(&cwd)
.output()
.expect("spawn unknown");
assert!(unknown.status.success(), "unknown family exit: {:?}", unknown.status);
assert_eq!(unknown.stdout.len(), 0, "unknown family stdout: {:?}", unknown.stdout);
let _ = std::fs::remove_dir_all(&cwd);
}
#[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);
}
/// Property: `aura mc --trace <name>` persists one standalone, round-tripping
/// run-dir per draw seed (the built-in MC family draws seeds 1,2,3), and a plain
/// `aura mc` (no --trace) writes no trace tree (opt-in; byte-unchanged path).
#[test]
fn mc_trace_persists_a_member_dir_per_seed() {
let cwd = temp_cwd("mc-trace");
// opt-in OFF: plain `aura mc` persists no per-member trace dirs.
let plain = Command::new(BIN).arg("mc").current_dir(&cwd).output().expect("spawn aura mc");
assert!(plain.status.success(), "plain mc exit: {:?}", plain.status);
assert!(!cwd.join("runs/traces").exists(), "plain mc must write no trace files");
// opt-in ON: one standalone run-dir per draw seed.
let traced = Command::new(BIN)
.args(["mc", "--trace", "mc1"])
.current_dir(&cwd)
.output()
.expect("spawn aura mc --trace");
assert!(traced.status.success(), "traced mc exit: {:?}", traced.status);
for seed in [1, 2, 3] {
let dir = cwd.join(format!("runs/traces/mc1/seed{seed}"));
assert!(dir.join("index.json").exists(), "seed{seed} index.json missing");
assert!(dir.join("equity.json").exists(), "seed{seed} equity tap missing");
assert!(dir.join("exposure.json").exists(), "seed{seed} exposure tap missing");
// the persisted equity tap is columnar SoA (C7): kind tag + ts/column parity.
let equity = std::fs::read_to_string(dir.join("equity.json")).expect("read equity.json");
assert!(equity.contains("\"kinds\":[\"F64\"]"), "seed{seed} F64 tag: {equity}");
let ts_len = json_array_body(&equity, "\"ts\":[").split(',').count();
let col_len = json_array_body(&equity, "\"columns\":[[").split(',').count();
assert_eq!(col_len, ts_len, "seed{seed} SoA parity: ts={ts_len} col={col_len}; {equity}");
}
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: `aura sweep --trace <name>` persists one standalone, round-tripping
/// run-dir per grid point. The built-in grid is fast∈{2,3} × slow∈{4,5} = 4
/// points, content-keyed by the varying axes (signals.trend.fast.length /
/// signals.trend.slow.length); a plain `aura sweep` writes nothing.
#[test]
fn sweep_trace_persists_a_member_dir_per_grid_point() {
let cwd = temp_cwd("sweep-trace");
let plain = Command::new(BIN).arg("sweep").current_dir(&cwd).output().expect("spawn aura sweep");
assert!(plain.status.success(), "plain sweep exit: {:?}", plain.status);
assert!(!cwd.join("runs/traces").exists(), "plain sweep must write no trace files");
let traced = Command::new(BIN)
.args(["sweep", "--trace", "swp"])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --trace");
assert!(traced.status.success(), "traced sweep exit: {:?}", traced.status);
for key in [
"signals.trend.fast.length-2_signals.trend.slow.length-4",
"signals.trend.fast.length-2_signals.trend.slow.length-5",
"signals.trend.fast.length-3_signals.trend.slow.length-4",
"signals.trend.fast.length-3_signals.trend.slow.length-5",
] {
let dir = cwd.join(format!("runs/traces/swp/{key}"));
assert!(dir.join("index.json").exists(), "{key} index.json missing");
assert!(dir.join("equity.json").exists(), "{key} equity tap missing");
assert!(dir.join("exposure.json").exists(), "{key} exposure tap missing");
let equity = std::fs::read_to_string(dir.join("equity.json")).expect("read equity.json");
assert!(equity.contains("\"kinds\":[\"F64\"]"), "{key} F64 tag: {equity}");
let ts_len = json_array_body(&equity, "\"ts\":[").split(',').count();
let col_len = json_array_body(&equity, "\"columns\":[[").split(',').count();
assert_eq!(col_len, ts_len, "{key} SoA parity: ts={ts_len} col={col_len}; {equity}");
}
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: `aura walkforward --trace <name>` persists one standalone,
/// round-tripping run-dir per OOS window (the built-in roll = 3 windows), each
/// keyed `oos<ns>`; a plain `aura walkforward` writes nothing.
#[test]
fn walkforward_trace_persists_a_member_dir_per_oos_window() {
let cwd = temp_cwd("wf-trace");
let plain = Command::new(BIN).arg("walkforward").current_dir(&cwd).output().expect("spawn aura walkforward");
assert!(plain.status.success(), "plain walkforward exit: {:?}", plain.status);
assert!(!cwd.join("runs/traces").exists(), "plain walkforward must write no trace files");
let traced = Command::new(BIN)
.args(["walkforward", "--trace", "wf1"])
.current_dir(&cwd)
.output()
.expect("spawn aura walkforward --trace");
assert!(traced.status.success(), "traced walkforward exit: {:?}", traced.status);
let base = cwd.join("runs/traces/wf1");
let mut members: Vec<String> = std::fs::read_dir(&base)
.expect("read wf1 trace dir")
.map(|e| e.expect("dir entry").file_name().to_string_lossy().into_owned())
.collect();
members.sort();
assert_eq!(members.len(), 3, "built-in roll = 3 OOS windows -> 3 member dirs; got {members:?}");
for key in &members {
assert!(key.starts_with("oos"), "member key must be oos<ns>: {key}");
let dir = base.join(key);
assert!(dir.join("index.json").exists(), "{key} index.json missing");
let equity = std::fs::read_to_string(dir.join("equity.json")).expect("read equity.json");
assert!(equity.contains("\"kinds\":[\"F64\"]"), "{key} F64 tag: {equity}");
let ts_len = json_array_body(&equity, "\"ts\":[").split(',').count();
let col_len = json_array_body(&equity, "\"columns\":[[").split(',').count();
assert_eq!(col_len, ts_len, "{key} SoA parity: ts={ts_len} col={col_len}; {equity}");
}
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (C1 / Fork-C content-derived keys): `aura sweep --trace` is
/// reproducible — re-running it produces the SAME member-dir set and
/// BYTE-IDENTICAL tap files. The whole reason member keys are derived from the
/// grid point's content (not a runtime ordinal counter) is that members run in
/// parallel across threads (C1: parallelism *across* sims); a thread-race in the
/// keying or the persisted bytes would silently regress determinism. A runtime
/// counter would shuffle which member lands in which content-keyed dir run-to-run; this
/// test fails loudly if that drift is ever reintroduced.
#[test]
fn sweep_trace_is_byte_deterministic_across_runs() {
let collect = |tag: &str| -> std::collections::BTreeMap<String, String> {
let cwd = temp_cwd(tag);
let out = Command::new(BIN)
.args(["sweep", "--trace", "det"])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --trace");
assert!(out.status.success(), "{tag} sweep exit: {:?}", out.status);
// Map each member dir's tap files to their bytes (member_key/file -> content).
let base = cwd.join("runs/traces/det");
let mut taps = std::collections::BTreeMap::new();
for member in std::fs::read_dir(&base).expect("read det trace dir") {
let member = member.expect("dir entry").file_name().to_string_lossy().into_owned();
for file in ["index.json", "equity.json", "exposure.json"] {
let body = std::fs::read_to_string(base.join(&member).join(file))
.unwrap_or_else(|e| panic!("{tag} read {member}/{file}: {e}"));
taps.insert(format!("{member}/{file}"), body);
}
}
let _ = std::fs::remove_dir_all(&cwd);
taps
};
let first = collect("sweep-det-1");
let second = collect("sweep-det-2");
// Same member-dir set, run-to-run (content-keyed, not order-keyed).
let first_keys: Vec<&String> = first.keys().collect();
let second_keys: Vec<&String> = second.keys().collect();
assert_eq!(first_keys, second_keys, "member-dir set drifted across runs");
// Every tap byte-identical across the two runs (the index.json git-commit
// field is build-identity, constant within one build, so it compares equal
// here too — this pins run-to-run determinism, C1).
assert_eq!(first, second, "a traced member's bytes drifted across two runs (C1 violation)");
}
/// Property (#105 generic key, the bool-param proof): `aura sweep --strategy
/// momentum --trace <name>` sweeps the momentum strategy — a totally different
/// param surface incl. a bool — and persists one portable member dir per grid
/// point. Every member-dir name is filesystem-conformant (`[A-Za-z0-9._-]`), the
/// bool axis shows up (both `longonly.enabled-true` and `-false`), and one member
/// is chartable.
#[test]
fn momentum_sweep_trace_persists_portable_member_dirs_with_the_bool() {
let cwd = temp_cwd("momentum-trace");
let traced = Command::new(BIN)
.args(["sweep", "--strategy", "momentum", "--trace", "mom"])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --strategy momentum --trace");
assert!(
traced.status.success(),
"momentum sweep exit: {:?}; stderr: {}",
traced.status,
String::from_utf8_lossy(&traced.stderr)
);
let base = cwd.join("runs/traces/mom");
let members: Vec<String> = std::fs::read_dir(&base)
.expect("read mom trace dir")
.map(|e| e.expect("dir entry").file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(members.len(), 8, "2x2x2 momentum grid = 8 member dirs; got {members:?}");
for m in &members {
assert!(
m.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-')),
"non-portable member dir name: {m}"
);
}
assert!(members.iter().any(|m| m.contains("longonly.enabled-true")), "missing bool=true: {members:?}");
assert!(members.iter().any(|m| m.contains("longonly.enabled-false")), "missing bool=false: {members:?}");
// one member is chartable (the bool sits inside the dir name passed through).
let one = &members[0];
let chart = Command::new(BIN)
.args(["chart", &format!("mom/{one}")])
.current_dir(&cwd)
.output()
.expect("spawn aura chart mom/<member>");
assert!(chart.status.success(), "chart exit: {:?}", chart.status);
assert!(
String::from_utf8_lossy(&chart.stdout).contains("uPlot"),
"expected a self-contained uPlot page"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#105, the `--strategy` selector at the stdout boundary): `aura sweep
/// --strategy momentum` SWITCHES the swept surface — it prints exactly the
/// 8-point momentum family (vs the default SMA arm's 4 points), and every line is
/// a `RunReport` over the momentum param surface, carrying the bool axis as a
/// TYPED `{"Bool":...}` param (both `true` and `false` appear across the grid).
/// The disk-trace test pins the member-dir names; this pins the binary's actual
/// stdout. A selector miswiring (`momentum`→`SmaCross`) would leave that test
/// green — the dirs would still differ — while silently printing SMA reports
/// here; nothing else catches it. Asserts on structure + the typed bool token,
/// never the volatile commit value or the metric floats, so it stays
/// deterministic. Run in a fresh temp cwd so the family-store ordinal is stable.
#[test]
fn sweep_strategy_momentum_prints_eight_typed_bool_member_lines() {
let cwd = temp_cwd("sweep-strategy-momentum");
let out = Command::new(BIN)
.args(["sweep", "--strategy", "momentum"])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --strategy momentum");
assert!(
out.status.success(),
"momentum sweep 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();
// 2x2x2 momentum grid = 8 member lines (vs the default SMA arm's 4): the
// selector switched the swept surface, observable at stdout.
assert_eq!(lines.len(), 8, "momentum sweep must print 8 family lines: {stdout:?}");
// Every line is a family member: assigned id + a nested RunReport over the
// MOMENTUM param surface (the SMA arm carries `signals.trend.*`, never
// `ema.length`/`longonly.enabled`).
for line in &lines {
assert!(line.starts_with("{\"family_id\":\"sweep-0\",\"report\":{\"manifest\":{"), "got: {line}");
assert!(line.contains("[\"ema.length\","), "momentum surface missing ema.length: {line}");
assert!(!line.contains("signals.trend.fast.length"), "must not be the SMA surface: {line}");
}
// The bool axis is carried as a TYPED `{"Bool":...}` param, both values
// present across the 8-point grid — the bool-param proof at the stdout edge.
assert!(
stdout.contains("[\"longonly.enabled\",{\"Bool\":true}]"),
"missing typed Bool=true param: {stdout}"
);
assert!(
stdout.contains("[\"longonly.enabled\",{\"Bool\":false}]"),
"missing typed Bool=false param: {stdout}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#105, the bool actually gates — observable on disk): the
/// `longonly.enabled` param is genuinely WIRED into the momentum harness, not a
/// no-op knob. For one grid point's two bool members (same `ema.length` +
/// `bias.scale`, differing only in the bool), the `enabled=true` member
/// records a long-only exposure stream — every recorded value `>= 0` — while the
/// otherwise-identical `enabled=false` member records at least one NEGATIVE
/// exposure over the same deterministic showcase stream. This pins the gate's
/// effect on the persisted C7 exposure tap end-to-end: the LongOnly unit test
/// proves `max(x,0)` in isolation, the dir-name test proves the bool reaches the
/// key, but only this proves the bool changes the OBSERVED run output. A
/// regression that dropped the gate from the wiring (passing exposure straight to
/// the broker) would make the two members' exposure taps identical — this fails
/// loudly. Asserts on the sign (>=0 / <0), never exact floats, so it is
/// deterministic across builds.
#[test]
fn momentum_longonly_true_clamps_exposure_nonnegative_false_keeps_negatives() {
let cwd = temp_cwd("momentum-longonly-gate");
let traced = Command::new(BIN)
.args(["sweep", "--strategy", "momentum", "--trace", "gate"])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --strategy momentum --trace");
assert!(
traced.status.success(),
"momentum --trace exit: {:?}; stderr: {}",
traced.status,
String::from_utf8_lossy(&traced.stderr)
);
// One grid point, its two bool members (identical but for the bool).
let base = cwd.join("runs/traces/gate");
let enabled = base.join("ema.length-5_bias.scale-1_longonly.enabled-true");
let disabled = base.join("ema.length-5_bias.scale-1_longonly.enabled-false");
let exposures = |dir: &std::path::Path| -> Vec<f64> {
let body = std::fs::read_to_string(dir.join("exposure.json")).expect("read exposure.json");
// the single f64 column's comma-separated values (substring parse, no serde).
json_array_body(&body, "\"columns\":[[")
.split(',')
.map(|t| t.trim().parse::<f64>().expect("exposure value is an f64"))
.collect()
};
let enabled_vals = exposures(&enabled);
let disabled_vals = exposures(&disabled);
// enabled=true is a long-only gate: NO recorded exposure is negative.
assert!(
enabled_vals.iter().all(|&x| x >= 0.0),
"long-only (enabled=true) must clamp short exposure to >= 0; got: {enabled_vals:?}"
);
// enabled=false passes through: at least one negative survives over the same
// showcase stream — proof the bool genuinely changes the run, not a no-op.
assert!(
disabled_vals.iter().any(|&x| x < 0.0),
"pass-through (enabled=false) must keep a short exposure over this stream; got: {disabled_vals:?}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
// --- 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;
// EURUSD 2024 full year (UTC inclusive ms): 2024-01-01 00:00:00.000 ..
// 2024-12-31 23:59:59.999 — a span wide enough for several 90/30/30-day
// walk-forward windows.
const EURUSD_2024_FROM_MS: i64 = 1_704_067_200_000;
const EURUSD_2024_TO_MS: i64 = 1_735_689_599_999;
fn local_data_present() -> bool {
std::path::Path::new(data_server::DEFAULT_DATA_PATH).is_dir()
}
/// Property: `aura sweep --real EURUSD` runs the whole built-in grid over REAL M1
/// close bars (not the synthetic VecSource), persisting one filesystem-portable
/// member dir per grid point — the four-point SMA grid lands as four
/// `[A-Za-z0-9._-]` dirs under `runs/traces/<name>/`, and one of them charts as a
/// self-contained uPlot page. This pins the real source actually flowing through
/// the family builders (Tasks 2-4) at the built-binary boundary, gated on local
/// data so it never fails on a data-less machine.
#[test]
fn sweep_real_persists_portable_member_dirs_over_real_bars() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let dir = temp_cwd("sweep_real");
let out = std::process::Command::new(BIN)
.args(["sweep", "--real", "EURUSD",
"--from", &EURUSD_JUN2024_FROM_MS.to_string(),
"--to", &EURUSD_JUN2024_TO_MS.to_string(),
"--trace", "swpr"])
.current_dir(&dir).output().unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let members: Vec<_> = std::fs::read_dir(dir.join("runs/traces/swpr")).unwrap()
.map(|e| e.unwrap().file_name().into_string().unwrap()).collect();
assert_eq!(members.len(), 4, "four sweep members: {members:?}");
assert!(members.iter().all(|k| k.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))),
"all keys filesystem-portable: {members:?}");
// one member charts as a uPlot page
let key = &members[0];
let chart = std::process::Command::new(BIN)
.args(["chart", &format!("swpr/{key}")]).current_dir(&dir).output().unwrap();
assert!(chart.status.success(), "stderr: {}", String::from_utf8_lossy(&chart.stderr));
assert!(String::from_utf8_lossy(&chart.stdout).contains("uPlot"));
let _ = std::fs::remove_dir_all(&dir);
}
/// Property: `aura walkforward --real EURUSD` over a full year of real bars rolls
/// the real-data window roller (90/30/30-day, ns-stamped) into SEVERAL OOS
/// windows, persisting one `oos<ns>`-keyed member dir per window. Pins that the
/// windowed real path draws its span from the probed `--from..--to` (not the
/// synthetic showcase span) and that the roller produces a multi-window family
/// over real data, gated on the local archive.
#[test]
fn walkforward_real_persists_one_oos_member_per_window() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let dir = temp_cwd("wf_real");
// a full year so 90/30/30-day rolling fits several windows
let out = std::process::Command::new(BIN)
.args(["walkforward", "--real", "EURUSD",
"--from", &EURUSD_2024_FROM_MS.to_string(),
"--to", &EURUSD_2024_TO_MS.to_string(),
"--trace", "wfr"])
.current_dir(&dir).output().unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let members: Vec<_> = std::fs::read_dir(dir.join("runs/traces/wfr")).unwrap()
.map(|e| e.unwrap().file_name().into_string().unwrap()).collect();
assert!(members.iter().all(|k| k.starts_with("oos")), "oos<ns> member dirs: {members:?}");
assert!(members.len() >= 2, "several OOS windows over a year: {members:?}");
let _ = std::fs::remove_dir_all(&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.
#[test]
fn sweep_real_no_geometry_symbol_refuses_with_exit_1() {
// not gated: the pip refusal happens before any data access.
let dir = temp_cwd("sweep_real_refuse");
let out = std::process::Command::new(BIN)
.args(["sweep", "--real", "NONEXISTENT"]).current_dir(&dir).output().unwrap();
assert_eq!(out.status.code(), Some(1));
assert!(String::from_utf8_lossy(&out.stderr).contains("no recorded geometry"));
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (C1): the same real sweep run twice is byte-identical — re-running
/// `aura sweep --real EURUSD` over the same bounded window produces an identical
/// report body. The whole determinism contract rests on a real backtest being
/// reproducible; a thread-race in the real source ingestion or member fold would
/// surface as a drift here. Gated on the local archive.
#[test]
fn sweep_real_is_byte_deterministic_across_runs() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let run = || {
// a fresh wiped cwd per call -> an empty registry each run, so the
// `family_id` ordinal is `sweep-0` for both; the raw stdout bodies are
// directly comparable with no ordinal normalisation needed.
let dir = temp_cwd("sweep_real_det");
let o = std::process::Command::new(BIN)
.args(["sweep", "--real", "EURUSD",
"--from", &EURUSD_JUN2024_FROM_MS.to_string(),
"--to", &EURUSD_JUN2024_TO_MS.to_string()])
.current_dir(&dir).output().unwrap();
let body = String::from_utf8_lossy(&o.stdout).into_owned();
let _ = std::fs::remove_dir_all(&dir);
body
};
assert_eq!(run(), run(), "same real sweep twice must be byte-identical (C1)");
}
// 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";
/// 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 = temp_cwd("sweep_real_blueprint_no_stop");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8",
"--name", "brp_nostop",
])
.current_dir(&dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2, "one member line per grid point: {stdout}");
for line in &lines {
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
let 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: {line}");
assert_eq!(get("stop_k").and_then(|p| p[1]["F64"].as_f64()), Some(2.0),
"the resolved default stop k is stamped: {line}");
}
}
/// Property: the real-data blueprint sweep path (`aura sweep <blueprint.json>
/// --real SYM --axis …`, dispatched as sugar over the one campaign executor)
/// prints one member line per grid point, IN AXIS ORDER (odometer: the first
/// `--axis` varies slowest), each line JSON with a `family_id` and a `report`
/// whose `manifest.params` carries every swept param binding — and, the
/// sanctioned additive delta over the synthetic path, **`report.manifest`
/// carries a stamped `instrument` key** (the campaign substrate stamps every
/// member's manifest with the instrument under test, exactly like the sibling
/// `campaign_run_real_e2e_sweep_gate_walkforward` in `research_docs.rs`). Also
/// pins that the run persists exactly one new `FamilyKind::Sweep` family with
/// the grid's member count, and that the underlying generated process/campaign
/// documents and campaign-run record are durably auto-registered. 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 = temp_cwd("sweep_real_blueprint");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
"--name", "brp",
])
.current_dir(&dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 4, "one member line per 2x2 grid point: {stdout}");
// Odometer order: `sma_signal.fast.length` (first `--axis`, outer/slowest)
// x `sma_signal.slow.length` (second `--axis`, inner) -> (2,8),(2,16),(4,8),(4,16).
let expected: [[(&str, i64); 2]; 4] = [
[("sma_signal.fast.length", 2), ("sma_signal.slow.length", 8)],
[("sma_signal.fast.length", 2), ("sma_signal.slow.length", 16)],
[("sma_signal.fast.length", 4), ("sma_signal.slow.length", 8)],
[("sma_signal.fast.length", 4), ("sma_signal.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}");
}
// the sanctioned additive delta: the campaign substrate stamps every
// member's manifest with the instrument.
assert_eq!(
manifest["instrument"].as_str(),
Some("GER40"),
"the dissolved sweep 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 dissolved sweep stamps the resolved stop: {line}"
);
}
// exactly one new Sweep family persisted, with the grid's four members.
let fams = std::process::Command::new(BIN)
.args(["runs", "families"])
.current_dir(&dir)
.output()
.unwrap();
assert!(fams.status.success(), "families exit: {:?}", fams.status);
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
assert_eq!(fams_out.lines().count(), 1, "exactly one family persisted: {fams_out}");
assert!(fams_out.contains("\"kind\":\"Sweep\""), "families: {fams_out}");
assert!(fams_out.contains("\"members\":4"), "2x2 grid -> four members: {fams_out}");
// Auto-registration: generated documents are durable intent — exactly
// one process doc, one campaign doc, one campaign run record.
let count = |sub: &str| {
std::fs::read_dir(dir.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(dir.join("runs").join("campaign_runs.jsonl"))
.expect("campaign_runs.jsonl exists after a sugar run");
assert_eq!(runs_log.lines().count(), 1, "one campaign run recorded");
// `CampaignRunRecord` (aura-registry's lineage.rs)
// carries no name field at all (campaign id/process id/run/seed/cells/
// generalizations/trace_name only) — the `--name` handle is NOT part of
// the run-log line. It IS part of the generated CAMPAIGN DOCUMENT
// (`translate_sweep` sets `campaign.name = name`), so the handle is
// checked on that persisted document instead.
let campaigns_dir = dir.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\":\"brp\""),
"the --name handle lands on the generated campaign document: {campaign_doc_json}"
);
// Property (dispatch-rewire seam, docs/specs/sweep-dissolution.md Task 4
// Step 3): the CLI's `--axis` names are typed against the WRAPPED probe
// namespace (`sma_signal.fast.length`), but a campaign document's
// `strategies[].axes` speak the RAW `param_space` namespace
// (`fast.length`) — `bind_axes`'s convention (#203). `dispatch_sweep`
// strips the wrapper's one leading node segment before generating the
// document; a regression that stopped stripping (or stripped one segment
// too many/few) would persist the wrong axis keys and
// `validate_campaign_refs` would refuse every axis as unknown before any
// member ran — this pin catches that at the artifact level, directly.
let campaign_doc: serde_json::Value =
serde_json::from_str(&campaign_doc_json).expect("generated campaign doc parses as JSON");
let axes = campaign_doc["strategies"][0]["axes"]
.as_object()
.expect("strategies[0].axes is an object");
assert!(
axes.contains_key("fast.length"),
"the raw (unwrapped) axis name is the document key: {campaign_doc_json}"
);
assert!(
axes.contains_key("slow.length"),
"the raw (unwrapped) axis name is the document key: {campaign_doc_json}"
);
assert!(
!axes.contains_key("sma_signal.fast.length") && !axes.contains_key("sma_signal.slow.length"),
"the wrapped CLI probe name must never leak into the stored document: {campaign_doc_json}"
);
// Property (dispatch-rewire seam: the ms<->ns crossing, C3): the probed
// archive window is carried through `DataSource::full_window` (aura's
// native epoch-ns `Timestamp`, clamped to the archive's actual first/last
// bar inside the requested range) and converted back to Unix-ms via
// `aura_ingest::epoch_ns_to_unix_ms` exactly once before landing in the
// generated document's `data.windows[0]`. A regression that skipped the
// conversion (or applied it twice, or swapped ms<->ns) would persist a
// window off by a factor of ~1e6 from the requested `--from`/`--to` —
// silently, since the executor would still run (over the wrong or an
// empty range) rather than refuse outright. Pin the sane invariant that
// survives the archive's exact clamp: the stored window is a
// (from <= to) sub-range of the requested Unix-ms bounds, not a value
// orders of magnitude away.
let from_req = GER40_SEPT2024_FROM_MS.parse::<i64>().unwrap();
let to_req = GER40_SEPT2024_TO_MS.parse::<i64>().unwrap();
let window = &campaign_doc["data"]["windows"][0];
let from_stored = window["from_ms"].as_i64().expect("from_ms is an integer");
let to_stored = window["to_ms"].as_i64().expect("to_ms is an integer");
assert!(
from_req <= from_stored && from_stored <= to_stored && to_stored <= to_req,
"the stored window is a ms sub-range of the requested [{from_req}, {to_req}]: {campaign_doc_json}"
);
// Dedup: a second identical invocation reuses both documents (content-
// addressed) and appends a second run record.
let out2 = std::process::Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
"--name", "brp",
])
.current_dir(&dir)
.output()
.unwrap();
assert!(out2.status.success(), "second run stderr: {}", String::from_utf8_lossy(&out2.stderr));
// `family_id` carries a per-execution uniqueness
// suffix ("the '-{run}' suffix is appended by the registry",
// aura-campaign/src/exec.rs `run_cell`) — a fresh family record is
// written on every execution even when the campaign/process documents
// themselves dedupe, so two identical invocations are NOT literally
// 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_stdout = String::from_utf8_lossy(&out2.stdout).into_owned();
let lines1: Vec<&str> = stdout.lines().collect();
let lines2: Vec<&str> = out2_stdout.lines().collect();
assert_eq!(lines2.len(), lines1.len(), "same member count on the dedup run: {out2_stdout}");
for (l1, l2) in lines1.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}");
}
assert_eq!(count("processes"), 1, "second identical run dedupes the process doc");
assert_eq!(count("campaigns"), 1, "second identical run dedupes the campaign doc");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (#210 c0110 fieldtest, "the wrapped-name refusal mangles the axis
/// the user typed"): the dissolved real-data blueprint sweep types `--axis`
/// against the WRAPPED probe namespace (`sma_signal.fast.length`, what
/// `--list-axes` prints), not the raw campaign-document namespace
/// (`fast.length`). A user who types the raw form must be refused up front,
/// echoing exactly what they typed — never a stripped-then-mangled fragment
/// (the old failure mode: `fast.length` -> strip one segment -> the
/// nonsensical bare `"length"`). NOT gated: the preflight is data-free (it
/// only reloads the blueprint to probe its param space), so the refusal
/// fires before the archive is ever touched — this test needs no local data.
#[test]
fn aura_sweep_real_blueprint_refuses_a_raw_form_axis_name_before_data_access() {
let dir = temp_cwd("sweep_real_rawname_refusal");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "fast.length=2,4",
"--name", "c0110-rawname",
])
.current_dir(&dir)
.output()
.expect("spawn aura sweep (raw-form axis)");
assert_eq!(out.status.code(), Some(2), "a raw-form axis name must be refused, not run: status={:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(
stderr.contains("axis \"fast.length\""),
"the refusal echoes exactly the name the user typed: {stderr}"
);
assert!(
!stderr.contains("axis \"length\""),
"must never mangle the typed axis into a stripped fragment: {stderr}"
);
// Data-free: no archive/store access at all.
assert!(!dir.join("runs").exists(), "a refused axis name must not start a real run");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (#210 c0110 fieldtest, "refused dissolved sweeps still register
/// their generated campaign document" — store litter, probe P3): binding only
/// a SUBSET of the blueprint's open axes (here: `fast.length`, leaving
/// `slow.length` unbound) is a referential refusal at member-run time — but
/// the sugar must validate the generated documents (including the
/// axis-binding shape) BEFORE `register_generated`, so the refusal leaves
/// `runs/processes/` and `runs/campaigns/` untouched and appends no
/// `campaign_runs.jsonl` record. Gated on the local GER40 archive (the
/// referential refusal is reached only after the archive is probed for the
/// window, same as the sibling dissolved-sweep pin above).
#[test]
fn aura_sweep_real_blueprint_subset_axes_refuses_without_store_litter() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let dir = temp_cwd("sweep_real_subset_refusal");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "sma_signal.fast.length=2,4",
"--name", "c0110-subset",
])
.current_dir(&dir)
.output()
.expect("spawn aura sweep (subset axes)");
assert_ne!(
out.status.code(),
Some(0),
"leaving slow.length unbound must refuse, not run: stdout={}",
String::from_utf8_lossy(&out.stdout)
);
let count = |sub: &str| {
std::fs::read_dir(dir.join("runs").join(sub)).map(|d| d.count()).unwrap_or(0)
};
assert_eq!(count("processes"), 0, "a refused sweep must not register a generated process document");
assert_eq!(count("campaigns"), 0, "a refused sweep must not register a generated campaign document");
let runs_log = dir.join("runs").join("campaign_runs.jsonl");
assert!(
!runs_log.exists() || std::fs::read_to_string(&runs_log).unwrap().is_empty(),
"no realization recorded for a refused sweep"
);
let _ = std::fs::remove_dir_all(&dir);
}
/// The synthetic seed-resweep `mc` is undefined over real bars (one realization ->
/// identical members), so a bare `aura mc --real EURUSD` — with no R candidate — is
/// REJECTED (usage on stderr, exit 2) at the binary boundary. The real path is
/// reachable ONLY via `--strategy r-sma` (the R-bootstrap over the pooled OOS R
/// series, `mc_strategy_r_sma_prints_a_bootstrap_line_deterministically`); a
/// `--real` without it stays a usage error. NOT gated — the refusal precedes any
/// data access, so it is CI-safe on every machine.
#[test]
fn mc_rejects_real_flag_with_usage_exit_2() {
let dir = temp_cwd("mc_no_real");
let out = Command::new(BIN)
.args(["mc", "--real", "EURUSD"])
.current_dir(&dir)
.output()
.expect("spawn aura mc --real");
assert_eq!(out.status.code(), Some(2), "mc --real must exit 2; status: {:?}", out.status);
assert!(out.stdout.is_empty(), "mc --real must not emit a run on stdout: {:?}", out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("Usage"), "mc --real stderr must carry usage: {stderr:?}");
// the exclusion is total: no `runs/` registry write either.
assert!(!dir.join("runs").exists(), "mc --real must not start a real run");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (window-flags-require-`--real`, at the binary boundary): a family
/// command given a `--from`/`--to` window WITHOUT `--real` is a usage error —
/// there is no synthetic window knob, so `aura sweep --from 100` and
/// `aura walkforward --to 200` exit 2 (usage on stderr), never a silent synthetic
/// run over the whole built-in stream. The pure grammar is unit-tested in
/// `main.rs`; this pins that the `Err` arm actually wires through to `exit(2)` at
/// the dispatch for BOTH family commands. NOT gated — the refusal is pure
/// argument parsing, before any data access.
#[test]
fn family_window_flag_without_real_refuses_with_usage_exit_2() {
// (subcommand args, the subcommand token the usage message must name)
for (args, cmd) in [
(&["sweep", "--from", "100"][..], "sweep"),
(&["sweep", "--to", "200"][..], "sweep"),
(&["walkforward", "--from", "100"][..], "walkforward"),
(&["walkforward", "--to", "200"][..], "walkforward"),
] {
let dir = temp_cwd("family_window_no_real");
let out = Command::new(BIN)
.args(args)
.current_dir(&dir)
.output()
.unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
assert!(out.stdout.is_empty(), "`aura {args:?}` must emit no run on stdout: {:?}", out.stdout);
// the refusal carries the subcommand's usage, which names `--real` — the
// flag the window needs and was given without.
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains(cmd), "`aura {args:?}` stderr must name `{cmd}`: {stderr:?}");
assert!(stderr.contains("--real"), "`aura {args:?}` stderr must name `--real`: {stderr:?}");
let _ = std::fs::remove_dir_all(&dir);
}
}
/// Property (house style, #179): every hand-rolled usage line reads
/// `Usage: aura <verb> …` — capital `Usage:` plus the program name inside the
/// grammar. `sweep`/`walkforward`'s built-in-branch usage closures were bare
/// (`"sweep [--strategy …]"`, no prefix, no program name) before cycle 0101; a
/// regression that drops the prefix again would NOT be caught by the older
/// `family_window_flag_without_real_refuses_with_usage_exit_2` pin above, which
/// only checks for the verb token and `--real` — not the `Usage: aura` lead-in.
/// An unknown `--strategy` name reaches this same closure on both commands, so
/// it is the smallest trigger for each.
#[test]
fn builtin_branch_usage_lines_lead_with_the_house_style_prefix() {
for (args, verb) in [(&["sweep", "--strategy", "bogus"][..], "sweep"), (&["walkforward", "--strategy", "bogus"][..], "walkforward")] {
let dir = temp_cwd("builtin_usage_prefix");
let out = Command::new(BIN)
.args(args)
.current_dir(&dir)
.output()
.unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr);
let want = format!("aura: Usage: aura {verb} ");
assert!(stderr.starts_with(&want), "`aura {args:?}` stderr must lead with {want:?}: {stderr:?}");
let _ = std::fs::remove_dir_all(&dir);
}
}
/// Property (house style, #179): the built-in `mc` usage line carries TWO
/// `|`-separated grammar alternatives, and BOTH name the program (`aura mc …`),
/// not just the first. This is the one lockstep-pinned site
/// (`mc_rejects_real_flag_with_usage_exit_2` above), but that pin only checks
/// for a single `"Usage"` occurrence — a regression that fixed only the first
/// alternative (leaving `… | mc --strategy r-sma …` unprefixed) would still
/// pass it. `--seeds` with no blueprint file is the built-in branch's own
/// grammar violation, reaching this exact literal.
#[test]
fn mc_builtin_usage_names_the_program_in_both_alternatives() {
let dir = temp_cwd("mc_usage_both_alts");
let out = Command::new(BIN)
.args(["mc", "--seeds", "3"])
.current_dir(&dir)
.output()
.expect("spawn aura mc --seeds");
assert_eq!(out.status.code(), Some(2), "mc --seeds without a blueprint must exit 2; status: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("Usage: aura mc [--name"),
"first alternative must name the program: {stderr:?}"
);
assert!(
stderr.contains("| aura mc --strategy r-sma"),
"second alternative must ALSO name the program: {stderr:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (house style, #179): `mc`'s loaded-blueprint branch usage line
/// also normalizes to `Usage: aura mc <blueprint.json> …` — before cycle 0101
/// it was fully lowercase and carried no program name (`"usage: mc
/// <blueprint.json> …"`). Reusing `sma_signal.json` (an existing closed
/// blueprint fixture) only to reach the dispatch branch; omitting `--seeds`
/// is this branch's own grammar violation.
#[test]
fn mc_over_a_blueprint_usage_names_the_program() {
let dir = temp_cwd("mc_blueprint_usage_prefix");
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args(["mc", &fixture])
.current_dir(&dir)
.output()
.expect("spawn aura mc <blueprint.json>");
assert_eq!(out.status.code(), Some(2), "mc <blueprint.json> without --seeds must exit 2; status: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("aura: Usage: aura mc <blueprint.json>"),
"stderr must lead with the house-style, program-named usage: {stderr:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
/// Property: `aura chart <family>` overlays one series per family member, labelled
/// by member key. The built-in sweep grid is fast∈{2,3} × slow∈{4,5} = 4 members;
/// each member key appears as a series name in the injected AURA_TRACES.
#[test]
fn chart_of_a_family_overlays_one_series_per_member() {
let cwd = temp_cwd("chart-family");
let swept = Command::new(BIN)
.args(["sweep", "--trace", "fam"])
.current_dir(&cwd)
.output()
.expect("spawn sweep --trace");
assert!(swept.status.success(), "sweep --trace exit: {:?}", swept.status);
let chart = Command::new(BIN).args(["chart", "fam"]).current_dir(&cwd).output().expect("spawn chart fam");
assert!(chart.status.success(), "chart family exit: {:?}", chart.status);
let html = String::from_utf8(chart.stdout).expect("utf-8 html");
assert!(html.contains("window.AURA_TRACES = {"), "AURA_TRACES not injected");
for key in [
"signals.trend.fast.length-2_signals.trend.slow.length-4",
"signals.trend.fast.length-2_signals.trend.slow.length-5",
"signals.trend.fast.length-3_signals.trend.slow.length-4",
"signals.trend.fast.length-3_signals.trend.slow.length-5",
] {
assert!(html.contains(key), "member series '{key}' missing from the family chart");
}
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: the single-run chart path is unchanged — no `--tap` shows ALL taps
/// (equity + exposure); `--tap equity` filters to one. A regression net for the
/// "single-run byte-unchanged when no --tap" acceptance bullet.
#[test]
fn chart_single_run_tap_filter_keeps_only_the_named_tap() {
let cwd = temp_cwd("chart-tap");
let traced = Command::new(BIN).args(["run", "--trace", "solo"]).current_dir(&cwd).output().expect("spawn run --trace");
assert!(traced.status.success(), "run --trace exit: {:?}", traced.status);
// no --tap: both taps present (unchanged behaviour).
let all = Command::new(BIN).args(["chart", "solo"]).current_dir(&cwd).output().expect("spawn chart");
assert!(all.status.success());
let all_html = String::from_utf8(all.stdout).expect("utf-8");
assert!(all_html.contains("\"equity\""), "equity series missing");
assert!(all_html.contains("\"exposure\""), "exposure series missing");
// --tap equity: only equity remains.
let one = Command::new(BIN).args(["chart", "solo", "--tap", "equity"]).current_dir(&cwd).output().expect("spawn chart --tap");
assert!(one.status.success(), "chart --tap exit: {:?}", one.status);
let one_html = String::from_utf8(one.stdout).expect("utf-8");
assert!(one_html.contains("\"equity\""), "equity series missing under --tap");
assert!(!one_html.contains("\"exposure\""), "--tap equity must drop exposure");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: a trace name cannot be reused across kinds — the write-guard refuses
/// it (exit 2), keeping `aura chart <name>` resolution a total function.
#[test]
fn trace_name_collision_across_kinds_is_refused() {
let cwd = temp_cwd("collision");
// run then sweep on the same name -> the sweep is refused.
let run = Command::new(BIN).args(["run", "--trace", "t"]).current_dir(&cwd).output().expect("spawn run --trace");
assert!(run.status.success(), "run --trace exit: {:?}", run.status);
let sweep = Command::new(BIN).args(["sweep", "--trace", "t"]).current_dir(&cwd).output().expect("spawn sweep --trace");
assert_eq!(sweep.status.code(), Some(1), "sweep onto a run name must exit 1");
// reverse: sweep then run on a fresh name -> the run is refused.
let sweep2 = Command::new(BIN).args(["sweep", "--trace", "u"]).current_dir(&cwd).output().expect("spawn sweep --trace u");
assert!(sweep2.status.success(), "sweep --trace u exit: {:?}", sweep2.status);
let run2 = Command::new(BIN).args(["run", "--trace", "u"]).current_dir(&cwd).output().expect("spawn run --trace u");
assert_eq!(run2.status.code(), Some(1), "run onto a family name must exit 1");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: the write-guard refuses ONLY cross-kind name reuse — a SAME-kind
/// overwrite stays Ok. Re-running `aura sweep --trace <name>` onto a name already
/// held by a family succeeds (exit 0), so the guard does not over-broadly forbid
/// all reuse. Without this the collision test alone would pass even if the guard
/// degenerated into "refuse every existing name", silently breaking re-tracing.
#[test]
fn same_kind_overwrite_is_allowed_by_the_write_guard() {
let cwd = temp_cwd("same-kind-overwrite");
let first = Command::new(BIN).args(["sweep", "--trace", "fam"]).current_dir(&cwd).output().expect("spawn sweep --trace fam");
assert!(first.status.success(), "first sweep --trace exit: {:?}", first.status);
// Same name, same kind (family -> family): the guard must NOT refuse it.
let again = Command::new(BIN).args(["sweep", "--trace", "fam"]).current_dir(&cwd).output().expect("spawn sweep --trace fam again");
assert_eq!(again.status.code(), Some(0), "same-kind family overwrite must stay Ok, got: {:?}; stderr: {}", again.status, String::from_utf8_lossy(&again.stderr));
// And the re-traced family is still chartable (name resolution unbroken).
let chart = Command::new(BIN).args(["chart", "fam"]).current_dir(&cwd).output().expect("spawn chart fam");
assert!(chart.status.success(), "chart after overwrite exit: {:?}", chart.status);
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: the family overlay honours a non-default `--tap` — `chart <family>
/// --tap exposure` overlays the exposure tap of every member, not the default
/// equity. Pins the family branch's `tap.unwrap_or("equity")` selection: a
/// regression that ignored `--tap` on families (always equity) would still pass
/// the default-equity family test, so the explicit non-default tap is its own net.
#[test]
fn chart_family_with_tap_overlays_the_named_tap_per_member() {
let cwd = temp_cwd("chart-family-tap");
let swept = Command::new(BIN).args(["sweep", "--trace", "fam"]).current_dir(&cwd).output().expect("spawn sweep --trace");
assert!(swept.status.success(), "sweep --trace exit: {:?}", swept.status);
let chart = Command::new(BIN).args(["chart", "fam", "--tap", "exposure"]).current_dir(&cwd).output().expect("spawn chart fam --tap exposure");
assert!(chart.status.success(), "chart family --tap exposure exit: {:?}", chart.status);
let html = String::from_utf8(chart.stdout).expect("utf-8 html");
assert!(html.contains("window.AURA_TRACES = {"), "AURA_TRACES not injected");
// One series per member, keyed by member key — the exposure overlay still
// spans all four grid points (the tap selection changes the column, not the
// member set).
for key in [
"signals.trend.fast.length-2_signals.trend.slow.length-4",
"signals.trend.fast.length-2_signals.trend.slow.length-5",
"signals.trend.fast.length-3_signals.trend.slow.length-4",
"signals.trend.fast.length-3_signals.trend.slow.length-5",
] {
assert!(html.contains(key), "member series '{key}' missing from the --tap exposure family chart");
}
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#102, single-run run-context reaches the served page): `aura chart
/// <run>` injects the run's `RunManifest` context into `window.AURA_TRACES.meta`
/// at the binary boundary — `kind:"run"`, the chart name, the manifest broker and
/// data window, the charted taps, and `members:null` (a single run carries no
/// member count). The render-layer unit (`render_chart_html_injects_run_context`)
/// proves the struct serializes, but only an end-to-end run pins that the
/// *built-and-read-back* manifest threads through `build_chart_data` into the
/// served HTML; a regression dropping the `meta` build (charting bare series with a
/// `ChartMeta::default()`) would leave every render/unit test green while serving a
/// header-less page. Asserts on the stable manifest fields (the synthetic window is
/// `[1,7]`, the broker is the FX sim-optimal label, kind/taps/members are fixed),
/// NEVER on the volatile `commit` (the dirty git HEAD), so it stays deterministic.
#[test]
fn chart_run_serves_manifest_run_context_in_meta() {
let cwd = temp_cwd("chart-run-meta");
let traced = Command::new(BIN).args(["run", "--trace", "demo"]).current_dir(&cwd).output().expect("spawn run --trace");
assert!(traced.status.success(), "run --trace exit: {:?}", traced.status);
let chart = Command::new(BIN).args(["chart", "demo"]).current_dir(&cwd).output().expect("spawn chart");
assert!(chart.status.success(), "chart exit: {:?}", chart.status);
let html = String::from_utf8(chart.stdout).expect("utf-8 html");
// The run-context meta is injected, tagged as a single run named `demo`.
assert!(html.contains("\"meta\":{"), "run-context meta not injected into the served page: {html:?}");
assert!(html.contains("\"kind\":\"run\""), "served meta must be kind run");
assert!(html.contains("\"name\":\"demo\""), "served meta must carry the run name");
// The manifest's broker + data window thread through to the served page.
assert!(html.contains("\"broker\":\"sim-optimal(pip_size=0.0001)\""), "manifest broker missing from served meta");
assert!(html.contains("\"window\":[1,7]"), "manifest window missing from served meta");
// The charted taps + the single-run sentinel.
assert!(html.contains("\"taps\":[\"equity\",\"exposure\"]"), "charted taps missing from served meta");
assert!(html.contains("\"members\":null"), "a single run must carry no member count");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#102, family run-context + SPANNING window at the served page): `aura
/// chart <family>` injects family-shaped `meta` — `kind:"family"`, a `members`
/// count, the one compared `taps`, and crucially a `window` that is the SPAN across
/// all members, NOT a single member's window. The built-in sweep grid has 4
/// members, each over the synthetic `[1,7]` window, but the family's recorded
/// timestamps union to a wider `[1,18]` span; the served `window` must report that
/// span (the ledger's families-comparison invariant: only the span labels a
/// walk-forward family's true OOS coverage, and for sweep/MC it is still the union
/// of member windows). A regression that took the first member's window, or dropped
/// the family `members` count, would still pass the family-overlay series test
/// (which only checks series labels); this pins the header context. Deterministic:
/// asserts the fixed kind/members/taps/span, never the volatile commit.
#[test]
fn chart_family_serves_member_count_and_spanning_window_in_meta() {
let cwd = temp_cwd("chart-family-meta");
let swept = Command::new(BIN).args(["sweep", "--trace", "fam"]).current_dir(&cwd).output().expect("spawn sweep --trace");
assert!(swept.status.success(), "sweep --trace exit: {:?}", swept.status);
let chart = Command::new(BIN).args(["chart", "fam"]).current_dir(&cwd).output().expect("spawn chart fam");
assert!(chart.status.success(), "chart family exit: {:?}", chart.status);
let html = String::from_utf8(chart.stdout).expect("utf-8 html");
assert!(html.contains("\"meta\":{"), "family run-context meta not injected: {html:?}");
assert!(html.contains("\"kind\":\"family\""), "served meta must be kind family");
assert!(html.contains("\"name\":\"fam\""), "served meta must carry the family name");
// The 4-point built-in grid -> a member count of 4 (the single-run sentinel is gone).
assert!(html.contains("\"members\":4"), "family meta must carry the member count, got: {html}");
assert!(!html.contains("\"members\":null"), "a family must not carry the single-run null sentinel");
// The default compared tap (equity), one entry — not the per-member labels.
assert!(html.contains("\"taps\":[\"equity\"]"), "family meta must carry the single compared tap");
// The SPANNING window across members ([1,18]), wider than any single member's [1,7].
assert!(html.contains("\"window\":[1,18]"), "family meta must carry the SPAN across members, not one member's window; got: {html}");
let _ = std::fs::remove_dir_all(&cwd);
}
// --- `aura run --harness <name>` selector (iter-3 Task 3) --------------------
/// Property: `aura run --harness sma` is the pip-only default — its `RunReport`
/// OMITS the `r` key entirely (the `skip_serializing_if` keeps a pip run byte-clean).
/// The companion of the r-sma test: it proves the selector switches the surface,
/// not that every run sprouts an `r` block.
#[test]
fn run_harness_sma_is_pip_only_no_r_block() {
let out = std::process::Command::new(BIN).args(["run", "--harness", "sma"]).output().unwrap();
assert!(out.status.success());
let s = String::from_utf8(out.stdout).unwrap();
assert!(!s.contains("\"r\":"), "a pip run must omit the r key: {s}");
}
/// Property: an unknown `--harness` name is a usage error at the binary boundary —
/// exit 2 (never a silent default run), the `Err` arm wiring through to `exit(2)`.
#[test]
fn run_unknown_harness_exits_two() {
let out = std::process::Command::new(BIN).args(["run", "--harness", "nope"]).output().unwrap();
assert_eq!(out.status.code(), Some(2));
}
/// Extract the balanced `"metrics":{...}` object substring from one stdout member
/// line. The `family_id` prefix legitimately differs between a no-trace sweep
/// (`sweep-0`) and a `--trace n` sweep (`n-0`), so an equivalence check must
/// compare only the `metrics` block (the reduced/raw output under test), not the
/// whole line. Brace-balanced scan, not a JSON parser (no serde dep in this test
/// crate); the member lines are flat enough for this to be exact.
fn metrics_object(line: &str) -> &str {
let key = "\"metrics\":";
let start = line.find(key).expect("member line has a metrics block") + key.len();
let bytes = line.as_bytes();
assert_eq!(bytes[start], b'{', "metrics value must be an object: {line}");
let mut depth = 0usize;
for (i, &b) in bytes[start..].iter().enumerate() {
match b {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
return &line[start..start + i + 1];
}
}
_ => {}
}
}
panic!("unbalanced metrics object: {line}")
}
/// The mean-reversion strategy reaches the CLI seam: `aura sweep --strategy
/// r-meanrev` emits an R-bearing member, and the folded no-trace path equals
/// the raw --trace path byte-for-byte (parity with the r-sma / breakout
/// fold-vs-raw guard). window 3 warms up on the synthetic stream; one window ×
/// one band_k × the single default stop = one member.
#[test]
fn sweep_strategy_r_meanrev_folded_no_trace_metrics_equal_raw_trace_metrics() {
let cwd = temp_cwd("sweep-r-meanrev-fold-vs-raw");
let folded = Command::new(BIN)
.args(["sweep", "--strategy", "r-meanrev", "--window", "3"])
.current_dir(&cwd)
.output()
.expect("spawn folded (no-trace) r-meanrev sweep");
assert!(
folded.status.success(),
"folded no-trace meanrev sweep exit: {:?}; stderr: {}",
folded.status,
String::from_utf8_lossy(&folded.stderr)
);
let folded_out = String::from_utf8(folded.stdout).expect("utf-8");
let raw = Command::new(BIN)
.args(["sweep", "--strategy", "r-meanrev", "--window", "3", "--trace", "m1"])
.current_dir(&cwd)
.output()
.expect("spawn raw (--trace) r-meanrev sweep");
assert!(
raw.status.success(),
"raw --trace meanrev sweep exit: {:?}; stderr: {}",
raw.status,
String::from_utf8_lossy(&raw.stderr)
);
let raw_out = String::from_utf8(raw.stdout).expect("utf-8");
let folded_lines: Vec<&str> = folded_out.lines().collect();
let raw_lines: Vec<&str> = raw_out.lines().collect();
assert_eq!(folded_lines.len(), 1, "folded meanrev sweep must print 1 member: {folded_out:?}");
assert_eq!(
raw_lines.len(),
folded_lines.len(),
"member count must match: folded {} vs raw {}",
folded_lines.len(),
raw_lines.len()
);
for (i, (f, r)) in folded_lines.iter().zip(raw_lines.iter()).enumerate() {
let fm = metrics_object(f);
let rm = metrics_object(r);
assert!(fm.contains("\"r\":{"), "folded meanrev member {i} must carry an r block: {fm}");
assert_eq!(
fm, rm,
"folded vs raw meanrev metrics diverge at member {i}\n folded: {fm}\n raw: {rm}"
);
}
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: the meanrev family is the cartesian product of its grid axes — a
/// multi-value `--window` list yields exactly one member per window value, each
/// carrying its own bound window length in the manifest params. Guards the manual
/// cartesian loop in `r_meanrev_sweep_family`. windows 3 and 4 both warm up
/// on the 18-bar synthetic stream, so two members must appear, in grid order.
#[test]
fn sweep_strategy_r_meanrev_grids_one_member_per_window() {
let cwd = temp_cwd("sweep-r-meanrev-window-grid");
let out = Command::new(BIN)
.args(["sweep", "--strategy", "r-meanrev", "--window", "3,4"])
.current_dir(&cwd)
.output()
.expect("spawn 2-window r-meanrev sweep");
assert!(
out.status.success(),
"window-grid meanrev sweep exit: {:?}; stderr: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2, "2-window grid must print 2 members: {stdout:?}");
assert!(
lines[0].contains("[\"window\",{\"I64\":3}]"),
"first member must bind window=3: {}",
lines[0]
);
assert!(
lines[1].contains("[\"window\",{\"I64\":4}]"),
"second member must bind window=4: {}",
lines[1]
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: the bare `aura walkforward` (default SMA-cross) summary is preserved
/// byte-for-byte by the new `--strategy`/grid plumbing — it still carries exactly
/// `windows`, `stitched_total_pips`, `param_stability` and NOT the r-sma-only
/// `oos_r` block. The spec promised the bare path's golden is unchanged; the
/// signature widening defaults to SmaCross and the summary gates `oos_r` on
/// `metrics.r.is_some()`, so a regression that leaked R-reporting (or any other
/// key) into the SMA summary would be a silent contract break. This pins the
/// negative: the SMA arm produces no `metrics.r`, hence no `oos_r`.
#[test]
fn walkforward_bare_sma_summary_has_no_oos_r() {
let cwd = temp_cwd("wf-bare-sma");
let out = Command::new(BIN)
.arg("walkforward")
.current_dir(&cwd)
.output()
.expect("spawn aura walkforward");
assert!(out.status.success(), "bare walkforward exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let lines: Vec<&str> = stdout.trim().lines().collect();
let summary: serde_json::Value = serde_json::from_str(lines.last().unwrap()).unwrap();
let wf = &summary["walkforward"];
assert!(wf["oos_r"].is_null(), "bare SMA summary must NOT carry oos_r: {wf}");
assert!(wf["windows"].is_number(), "bare SMA summary keeps windows: {wf}");
assert!(wf["stitched_total_pips"].is_number(), "bare SMA summary keeps stitched_total_pips: {wf}");
assert!(wf["param_stability"].is_array(), "bare SMA summary keeps param_stability: {wf}");
// no per-window member line carries a metrics.r block (R-reporting is r-sma-only)
for l in &lines[..lines.len() - 1] {
let v: serde_json::Value = serde_json::from_str(l).unwrap();
assert!(v["report"]["metrics"]["r"].is_null(), "bare SMA per-window line must NOT carry metrics.r: {l}");
}
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: a strategy that parses but has no walk-forward form (e.g.
/// `momentum`) is rejected with exit 2 and a stderr message that names the
/// OFFENDING strategy by its CLI token — not silently run as SMA, not crashed.
/// The dispatch's catch-all arm routes through `Strategy::cli_token()`, so the
/// diagnostic must echo the token the user typed (`momentum`), proving the
/// inverse map is wired. Distinct from a bad `--strategy bogus` (a usage parse
/// error): here the token is a valid strategy with no walk-forward arm.
#[test]
fn walkforward_unsupported_strategy_exits_2_naming_the_token() {
let cwd = temp_cwd("wf-unsupported");
let out = Command::new(BIN)
.args(["walkforward", "--strategy", "momentum"])
.current_dir(&cwd)
.output()
.expect("spawn aura walkforward --strategy momentum");
assert_eq!(out.status.code(), Some(2), "unsupported walkforward strategy must exit 2");
let stderr = String::from_utf8(out.stderr).expect("utf-8 stderr");
assert!(
stderr.contains("momentum"),
"stderr must name the offending strategy token: {stderr:?}"
);
assert!(out.stdout.is_empty(), "no family summary on the rejected path: {:?}", out.stdout);
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#159 cut 2): `--strategy r-breakout` is a fully retired CLI token, on
/// BOTH `sweep` and `walkforward` — indistinguishable from an unrecognized token
/// (`bogus`), i.e. it falls through `strategy_from`'s catch-all into the plain
/// usage error, not into a "valid-but-unsupported" branch (contrast
/// `walkforward_unsupported_strategy_exits_2_naming_the_token`, which pins that
/// shape for `momentum`). A regression that left `Strategy::RBreakout` parseable
/// while only deleting its family builder would make this test's stderr start
/// naming "r-breakout" specially instead of reading the generic `Usage: aura
/// <verb> …` line — this is the negative that catches a half-finished retirement.
#[test]
fn sweep_and_walkforward_reject_retired_r_breakout_token_as_unrecognized() {
for (args, verb) in [
(&["sweep", "--strategy", "r-breakout"][..], "sweep"),
(&["walkforward", "--strategy", "r-breakout"][..], "walkforward"),
] {
let cwd = temp_cwd("retired-r-breakout-token");
let out = Command::new(BIN)
.args(args)
.current_dir(&cwd)
.output()
.unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr);
let want = format!("aura: Usage: aura {verb} ");
assert!(
stderr.starts_with(&want),
"`aura {args:?}` must fall into the generic usage error, not a special-cased \
message: {stderr:?}"
);
assert!(out.stdout.is_empty(), "no family summary on the rejected path: {:?}", out.stdout);
let _ = std::fs::remove_dir_all(&cwd);
}
}
/// Property (#159 cut 2): `--channel` is genuinely removed from the sweep grammar,
/// not merely unused — clap rejects it as a structurally unknown argument (exit 2)
/// rather than silently accepting and dropping it. A regression that left the flag
/// on `SweepCmd` while deleting only its plumbing would parse successfully here and
/// this test would fail on the exit code.
#[test]
fn sweep_channel_flag_is_removed_from_the_grammar() {
let cwd = temp_cwd("sweep-channel-flag-retired");
let out = Command::new(BIN)
.args(["sweep", "--strategy", "r-meanrev", "--channel", "3"])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --channel");
assert_eq!(out.status.code(), Some(2), "--channel must be an unrecognized clap argument: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--channel") || stderr.to_lowercase().contains("unexpected argument"),
"clap must name the unknown flag: {stderr:?}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// 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() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-two");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(stdout.contains("\"generalize\":"), "aggregate object: {stdout}");
assert!(stdout.contains("\"n_instruments\":2"), "two instruments: {stdout}");
assert!(stdout.contains("\"worst_case\":"), "worst_case present: {stdout}");
assert!(stdout.contains("\"sign_agreement\":"), "sign_agreement present: {stdout}");
assert!(stdout.contains("GER40") && stdout.contains("USDJPY"), "per-instrument breakdown: {stdout}");
assert!(stdout.contains("\"family_id\":\"generalize-0\""), "family handle: {stdout}");
}
/// Characterization pin (byte-identity anchor for the generalize dissolution,
/// #210). The current `aura generalize` binds its protective stop as a grid axis
/// (`r_sma_sweep_family` with `stop_open=true`); the dissolution rebinds the same
/// stop through the risk-regime seam (`RiskRegime::Vol -> StopRule::Vol`). The
/// sibling `generalize_grades_a_candidate_across_two_instruments` asserts shape
/// only — `worst_case` and the per-instrument R floats are never checked — so a
/// stop-mechanism divergence between the two bindings would ship green. This pins
/// the EXACT current R grade of the identical invocation; after the dissolution
/// the same command must reproduce these bytes (the acceptance gate), and any
/// drift fails here loudly. 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() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-exact-grade");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let grade_line = stdout
.lines()
.find(|l| l.starts_with("{\"generalize\":"))
.unwrap_or_else(|| panic!("the generalize grade line: {stdout}"));
let v: serde_json::Value = serde_json::from_str(grade_line).expect("grade line parses as JSON");
let g = &v["generalize"];
assert_eq!(g["metric"].as_str(), Some("expectancy_r"), "metric: {grade_line}");
assert_eq!(g["n_instruments"].as_u64(), Some(2), "two instruments: {grade_line}");
assert_eq!(g["sign_agreement"].as_u64(), Some(2), "both agree in sign: {grade_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: {grade_line}");
let per = g["per_instrument"].as_array().expect("per_instrument is an array");
assert_eq!(per.len(), 2, "two per-instrument grades: {grade_line}");
assert_eq!(per[0][0].as_str(), Some("GER40"), "first instrument: {grade_line}");
assert_eq!(per[0][1].as_f64(), Some(0.01056371324510624), "GER40 expectancy R: {grade_line}");
assert_eq!(per[1][0].as_str(), Some("USDJPY"), "second instrument: {grade_line}");
assert_eq!(per[1][1].as_f64(), Some(0.005795903617609842), "USDJPY expectancy R: {grade_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() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("walkforward-exact-grade");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", "--strategy", "r-sma", "--real", "GER40",
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let grade_line = stdout
.lines()
.find(|l| l.starts_with("{\"walkforward\":"))
.unwrap_or_else(|| panic!("the walkforward summary line: {stdout}"));
let v: serde_json::Value = serde_json::from_str(grade_line).expect("summary line parses as JSON");
let wf = &v["walkforward"];
assert_eq!(wf["windows"].as_u64(), Some(9), "window count: {grade_line}");
// EXACT floats -- the byte-identity anchor the dissolution must preserve.
// stitched_total_pips is retained only if the campaign runner keeps the OOS curve.
assert_eq!(wf["stitched_total_pips"].as_f64(), Some(-10398606.666650848), "stitched OOS pips: {grade_line}");
let r = &wf["oos_r"];
assert_eq!(r["n_trades"].as_u64(), Some(20681), "pooled OOS trade count: {grade_line}");
assert_eq!(r["expectancy_r"].as_f64(), Some(-0.002397100685333715), "pooled OOS expectancy R: {grade_line}");
// param_stability varies across windows -> the per-window IS-refit picks
// different winners; pinning the means locks the winner-selection equivalence.
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: {grade_line}");
assert_eq!(ps[1]["mean"].as_f64(), Some(17.333333333333332), "slow-MA refit mean: {grade_line}");
}
/// Fork A: the dissolved walkforward binds the stop as a single risk regime, so a
/// multi-value --stop-length/--stop-k is refused (exit 2) before any run.
#[test]
fn walkforward_dissolved_refuses_a_multi_value_stop() {
let cwd = temp_cwd("walkforward-multi-stop");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", "--strategy", "r-sma", "--real", "GER40",
"--fast", "3", "--slow", "12", "--stop-length", "14,20", "--stop-k", "2.0",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "multi-value stop is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("single risk regime"), "stop-regime refusal: {stderr}");
}
/// Fork B: --select plateau is refused on the dissolved path (exit 2) with a
/// pointer to #215 until the wf-stage plateau relaxation ships.
#[test]
fn walkforward_dissolved_refuses_select_plateau() {
let cwd = temp_cwd("walkforward-plateau");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", "--strategy", "r-sma", "--real", "GER40",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
"--select", "plateau:worst",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "plateau is refused on the dissolved path");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("#215"), "forward pointer to the plateau follow-up: {stderr}");
}
/// `walkforward_args_from` requires all four grid knobs on the dissolved r-sma-real
/// path (unlike the inline path, which defaults an absent flag via `RGrid::default()`)
/// — a missing knob is a usage refusal (exit 2), not a silent default.
#[test]
fn walkforward_dissolved_refuses_missing_knobs() {
let cwd = temp_cwd("walkforward-missing-knobs");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", "--strategy", "r-sma", "--real", "GER40", "--fast", "3", "--slow", "12"])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "missing --stop-length/--stop-k is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("requires --fast --slow --stop-length --stop-k"), "missing-knob refusal: {stderr}");
}
/// An empty `--real ""` still satisfies clap's `Option::is_some()` dispatch guard, so
/// `walkforward_args_from`'s own empty-string check is what refuses it (exit 2) —
/// distinct from omitting `--real` entirely, which never enters the dissolved branch.
#[test]
fn walkforward_dissolved_refuses_an_empty_real_symbol() {
let cwd = temp_cwd("walkforward-empty-real");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", "--strategy", "r-sma", "--real", "",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "empty --real is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("dissolves only over --real"), "empty-real refusal: {stderr}");
}
/// Front-end parity: an unknown `--select` token is refused (exit 2) on the
/// dissolved path exactly as it is on the inline path — it must not be silently
/// swallowed by `select_is_plateau`'s plateau-only check and fall through to argmax.
#[test]
fn walkforward_dissolved_refuses_an_unknown_select_token() {
let cwd = temp_cwd("walkforward-bad-select");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", "--strategy", "r-sma", "--real", "GER40",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
"--select", "bogus",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "unknown --select token is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("Usage: aura walkforward"), "unknown-select refusal: {stderr}");
}
/// `--trace`'s own help text ("mutually exclusive with --name") is a contract the
/// inline path already enforces via `name_persist`; the dissolved r-sma-real path
/// must refuse the same combination (exit 2) rather than silently letting --name
/// win over --trace.
#[test]
fn walkforward_dissolved_refuses_name_and_trace_together() {
let cwd = temp_cwd("walkforward-name-and-trace");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", "--strategy", "r-sma", "--real", "GER40",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
"--name", "a", "--trace", "b",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "--name and --trace together is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("mutually exclusive"), "name/trace refusal: {stderr}");
}
/// Property (#210 T3, dispatch split): a successful `aura walkforward --strategy
/// r-sma --real` durably auto-registers exactly one generated process document, one
/// generated campaign document (carrying the `--name` handle and the stop as a
/// non-empty single risk regime), and one campaign-run record — mirroring the
/// generalize dissolution's audit trail. The persisted family set is exactly one
/// `Sweep` family (the pipeline's leading `std::sweep(argmax)` stage) plus one
/// `WalkForward` family (the per-window OOS reports) — the observable proof that
/// `dispatch_walkforward`'s r-sma-real branch now runs through the one campaign
/// executor rather than the deleted inline roller. Gated on the shared GER40
/// archive; skips cleanly on a data refusal.
#[test]
fn walkforward_dissolves_through_the_campaign_path() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("walkforward-dissolves");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", "--strategy", "r-sma", "--real", "GER40",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
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 sugar 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\":\"walkforward\""),
"the generated campaign document carries the --name handle: {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 = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(&cwd)
.output()
.expect("spawn families");
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
assert_eq!(
fams_out.lines().filter(|l| l.contains("\"kind\":\"Sweep\"")).count(),
1,
"one Sweep family from the pipeline's leading argmax stage: {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}"
);
}
/// Property: `aura generalize` is now thin sugar over the one campaign path — a
/// successful run durably auto-registers exactly one generated process document,
/// one generated campaign document (carrying the `--name` handle and the stop as
/// a non-empty single risk regime), and one campaign-run record. This is the
/// observable proof the inline path is gone and the dissolution runs through the
/// executor. Gated on the shared GER40/USDJPY Sept-2024 archive; skips cleanly on
/// a data refusal.
#[test]
fn generalize_dissolves_through_the_campaign_path() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-dissolves");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let 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 sugar 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\":\"generalize\""),
"the generated campaign document carries the --name handle: {campaign_doc_json}"
);
// The structural delta vs the sweep translator: the stop is a non-empty
// single risk regime, not empty risk.
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}"
);
// The campaign path's family set (#210 Q4): exactly one CrossInstrument grade
// family (the generalize result, appended by the sugar) plus one
// per-instrument Sweep family per cell (persisted by the executor).
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(&cwd)
.output()
.expect("spawn families");
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
assert_eq!(
fams_out.lines().filter(|l| l.contains("\"kind\":\"CrossInstrument\"")).count(),
1,
"one CrossInstrument grade family: {fams_out}"
);
assert_eq!(
fams_out.lines().filter(|l| l.contains("\"kind\":\"Sweep\"")).count(),
2,
"one per-instrument Sweep family per cell: {fams_out}"
);
}
/// Property (#210, dissolution — content-addressed idempotency): re-running the
/// IDENTICAL `aura generalize` invocation in the same project does not litter the
/// store with a second generated process/campaign document — `register_generated_g`
/// writes under a content-id filename, so two runs of the same candidate collapse
/// onto the same one process doc and one campaign doc, while each invocation still
/// records its OWN campaign-run line (a run is an event, a document is content).
/// Were the sugar to salt its generated documents with anything non-deterministic
/// (e.g. a timestamp), this would regress from 1 to 2 stored documents per repeat
/// invocation — the store-litter regression the campaign-path dissolution must not
/// introduce. Gated on the shared GER40/USDJPY Sept-2024 archive; skips cleanly on
/// a data refusal.
#[test]
fn generalize_repeated_identical_invocation_does_not_litter_the_store() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-repeat-idempotent");
let invoke = || {
std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.output()
.expect("spawn aura")
};
let first = invoke();
if first.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&first.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
return;
}
assert_eq!(first.status.code(), Some(0), "first run exit: {:?}", first.status);
let second = invoke();
assert_eq!(second.status.code(), Some(0), "second run exit: {:?}", second.status);
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, "two identical invocations share one process document");
assert_eq!(count("campaigns"), 1, "two identical invocations share one campaign document");
let runs_log = std::fs::read_to_string(cwd.join("runs").join("campaign_runs.jsonl"))
.expect("campaign_runs.jsonl exists");
assert_eq!(runs_log.lines().count(), 2, "each invocation still records its own campaign run");
}
/// Property (#210, dissolution — content-addressed distinctness): the idempotency
/// above is not a bug masquerading as a feature — two DIFFERENT `--fast`
/// invocations (differing candidate content) generate and persist TWO distinct
/// campaign documents (and two distinct process documents, since the `--metric`
/// text is unchanged so the process content matches — pinned via the campaign
/// count only, the axis differs on the campaign, not the process). Together with
/// `generalize_repeated_identical_invocation_does_not_litter_the_store`, this rules
/// out a store that always overwrites one fixed filename regardless of content —
/// the failure mode that would make the idempotency test above pass for the wrong
/// reason. Gated on the shared GER40/USDJPY Sept-2024 archive; skips cleanly on a
/// data refusal.
#[test]
fn generalize_distinct_invocations_persist_distinct_campaign_documents() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-distinct-content");
let invoke = |fast: &str| {
std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
"--fast", fast, "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.output()
.expect("spawn aura")
};
let first = invoke("3");
if first.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&first.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
return;
}
assert_eq!(first.status.code(), Some(0), "first run exit: {:?}", first.status);
let second = invoke("4");
assert_eq!(second.status.code(), Some(0), "second run exit: {:?}", second.status);
let count = |sub: &str| {
std::fs::read_dir(cwd.join("runs").join(sub))
.map(|d| d.count())
.unwrap_or(0)
};
assert_eq!(count("campaigns"), 2, "two distinct candidates persist two distinct campaign documents");
}
/// Property (#210 T3, dissolution): omitting `--from`/`--to` still completes —
/// the dispatch rewrite's window-resolution fallback (`dispatch_generalize` in
/// main.rs) resolves ONE shared campaign window from the FIRST listed symbol's
/// full archive geometry and applies it to every instrument in the campaign
/// document (a `CampaignDoc` carries a single shared window, unlike the old
/// inline `run_generalize`, which let each instrument resolve its OWN
/// independent full window). A wrong index or an empty-`symbols` panic in that
/// new fallback would only surface when `--from`/`--to` are absent — every
/// other generalize e2e pins the explicit-window path, leaving this one
/// unexercised. Gated on local GER40/USDJPY data.
#[test]
fn generalize_without_explicit_window_falls_back_to_the_first_symbols_full_window() {
let cwd = temp_cwd("generalize-default-window");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
let _ = std::fs::remove_dir_all(&cwd);
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(stdout.contains("\"generalize\":"), "aggregate object: {stdout}");
assert!(stdout.contains("\"n_instruments\":2"), "two instruments graded: {stdout}");
assert!(stdout.contains("\"family_id\":\"generalize-0\""), "family handle: {stdout}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: a single-instrument generalize is refused at parse time (exit 2),
/// before any data access — so it asserts the arity refusal on any machine.
#[test]
fn generalize_refuses_a_single_instrument() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", "--strategy", "r-sma", "--real", "GER40",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
])
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "single instrument must exit 2");
}
/// Property: a non-R metric is refused before any instrument runs (the data-free
/// `check_r_metric` pre-check), so this asserts the R-only refusal on any machine.
#[test]
fn generalize_refuses_a_non_r_metric() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
"--metric", "total_pips",
])
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "a non-R metric must exit 2");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("R-only"), "must name the R-only refusal, got: {stderr}");
}
/// Property: a duplicate instrument in the `--real` list is refused at the binary
/// boundary (exit 2, before any data access) — `GER40,GER40` never runs. The spec's
/// double-count hazard: a repeated symbol would be evaluated twice and so counted
/// twice in both the worst-case floor and the sign-agreement count, silently
/// corrupting the generalization grade. The parser unit test pins the grammar; this
/// pins that the parse error actually reaches `exit(2)` through the dispatch arm
/// (a swallowed Err would let a duplicated run proceed). Data-free — the duplicate
/// check precedes any geometry/archive lookup, so it asserts on any machine.
#[test]
fn generalize_refuses_a_duplicate_instrument() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", "--strategy", "r-sma", "--real", "GER40,GER40",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
])
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "a duplicate instrument must exit 2");
assert!(
out.stdout.is_empty(),
"the refusal must not leak an aggregate to stdout, got: {:?}",
String::from_utf8_lossy(&out.stdout)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("generalize"), "must surface the generalize usage, got: {stderr}");
}
/// Property: a non-`r-sma` strategy is refused at the binary boundary (exit 2,
/// no run, data-free). The candidate must produce R (C10) — the cross-instrument
/// reduction is R-only — so only the r-sma grid is an admissible candidate. The
/// parser unit test pins the grammar refusal; this pins that `--strategy sma`
/// reaches `exit(2)` through the dispatch arm rather than silently defaulting to a
/// running candidate. Asserts on any machine (the strategy check precedes any run).
#[test]
fn generalize_refuses_a_non_r_sma_strategy() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", "--strategy", "sma", "--real", "GER40,USDJPY",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
])
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "a non-r-sma strategy must exit 2");
assert!(
out.stdout.is_empty(),
"the refusal must not leak an aggregate to stdout, got: {:?}",
String::from_utf8_lossy(&out.stdout)
);
}
/// Property: a multi-value candidate flag is refused at the binary boundary (exit 2,
/// no run, data-free). A *candidate* is a single grid cell, not a sweep — so any of
/// `--fast/--slow/--stop-length/--stop-k` carrying more than one value (`--fast 2,3`)
/// is refused, never silently widened into a multi-cell run whose generalization
/// grade would be ill-defined (the reduction grades one candidate, not a family).
/// The parser unit test pins the grammar; this pins the wiring to `exit(2)`.
#[test]
fn generalize_refuses_a_multi_value_candidate_flag() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
"--fast", "2,3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
])
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "a multi-value candidate flag must exit 2");
assert!(
out.stdout.is_empty(),
"the refusal must not leak an aggregate to stdout, got: {:?}",
String::from_utf8_lossy(&out.stdout)
);
}
/// Property: a successful `aura generalize` persists its per-instrument runs as a
/// discoverable `CrossInstrument` family — the C12 comparison axis / C18 lineage at
/// the built-binary boundary. The unit `cli_families_persist_and_round_trip_per_kind`
/// covers Sweep/MonteCarlo/WalkForward but not CrossInstrument; this is the
/// cross-instrument sibling of `mc_runs_persists_a_monte_carlo_family_and_lists_it`:
/// after the run, `aura runs families` lists `generalize-0` with
/// `"kind":"CrossInstrument"` and `"members":2` (one member per instrument). A
/// regression that mis-tagged the family kind, or persisted the wrong member count,
/// would slip past the stdout-only happy-path assertion above but fail here. Gated on
/// local GER40/USDJPY data (the shared Sept-2024 window); skips cleanly when absent.
#[test]
fn generalize_persists_a_discoverable_cross_instrument_family() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-family");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
let _ = std::fs::remove_dir_all(&cwd);
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
// the run persisted the per-instrument members as a discoverable CrossInstrument family.
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(&cwd)
.output()
.expect("spawn families");
assert!(fams.status.success(), "families exit: {:?}", fams.status);
let fams_out = String::from_utf8(fams.stdout).expect("utf-8");
// The dissolved path routes through the campaign executor, which persists a
// per-instrument Sweep family per cell (each instrument's candidate run, now
// a durable audit artifact) alongside the one CrossInstrument grade family
// the sugar appends. Pin exactly one CrossInstrument family (the generalize
// result) — not the total family count, which now includes those per-cell
// Sweep families.
let cross: Vec<&str> = fams_out
.lines()
.filter(|l| l.contains("\"kind\":\"CrossInstrument\""))
.collect();
assert_eq!(cross.len(), 1, "exactly one CrossInstrument family: {fams_out:?}");
assert!(cross[0].contains("\"family_id\":\"generalize-0\""), "families: {fams_out:?}");
assert!(cross[0].contains("\"members\":2"), "one member per instrument: {fams_out:?}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#210 T3, dissolution): the persisted `CrossInstrument` family's
/// members are attributed to the RIGHT instrument in the RIGHT cell order —
/// not merely correct in count. The dissolved path builds the family via
/// `cross_instrument_members`, which walks `outcome.cells` and trusts their
/// order/labelling, replacing the old `run_generalize`'s explicit
/// `zip(symbols, members)`. A cell-ordering or index bug in the new
/// extraction would still print the right aggregate (the campaign's own
/// `Generalize` stage computes that independently) while silently persisting
/// swapped or mislabelled members — a regression this test, not the exact-grade
/// anchor, would catch. Asserts `aura runs family generalize-0` lists GER40
/// first with the exact-grade anchor's GER40 `expectancy_r`, then USDJPY with
/// its own. Gated on the local GER40/USDJPY data (the shared Sept-2024 window).
#[test]
fn generalize_family_members_are_attributed_to_the_right_instrument() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let cwd = temp_cwd("generalize-member-attribution");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
let _ = std::fs::remove_dir_all(&cwd);
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let fam = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "family", "generalize-0"])
.current_dir(&cwd)
.output()
.expect("spawn runs family");
assert!(fam.status.success(), "runs family exit: {:?}", fam.status);
let fam_out = String::from_utf8(fam.stdout).expect("utf-8");
let members: Vec<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();
assert_eq!(members.len(), 2, "one member per instrument: {fam_out:?}");
assert_eq!(
members[0]["manifest"]["instrument"].as_str(), Some("GER40"),
"first member is GER40: {fam_out:?}"
);
assert_eq!(
members[0]["metrics"]["r"]["expectancy_r"].as_f64(), Some(0.01056371324510624),
"GER40's own expectancy_r, matching the exact-grade anchor: {fam_out:?}"
);
assert_eq!(
members[1]["manifest"]["instrument"].as_str(), Some("USDJPY"),
"second member is USDJPY: {fam_out:?}"
);
assert_eq!(
members[1]["metrics"]["r"]["expectancy_r"].as_f64(), Some(0.005795903617609842),
"USDJPY's own expectancy_r, matching the exact-grade anchor: {fam_out:?}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// `aura sweep <blueprint.json> --axis <name>=<csv> …` (#166): sweeps a loaded OPEN
/// signal over its named param-space axes and persists a discoverable Sweep family.
/// Both open knobs (fast/slow) must be bound by axes; the 2×2 grid yields 4 members.
#[test]
fn aura_sweep_loads_a_blueprint_and_persists_a_sweep_family() {
let cwd = temp_cwd("blueprint-sweep");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args([
"sweep", &fixture,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
"--trace", "f",
])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep blueprint");
assert!(out.status.success(), "exit: {:?} stderr={}", out.status, String::from_utf8_lossy(&out.stderr));
let fams = Command::new(BIN).args(["runs", "families"]).current_dir(&cwd).output().expect("spawn families");
let fo = String::from_utf8(fams.stdout).expect("utf-8");
assert!(fo.contains("\"kind\":\"Sweep\""), "families: {fo}");
assert!(fo.contains("\"members\":4"), "2×2 grid -> four members: {fo}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// 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_std::std_vocabulary;
use sha2::{Digest, Sha256};
let cwd = temp_cwd("blueprint-sweep-store");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args([
"sweep", &fixture,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
"--trace", "f",
])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep blueprint");
assert!(out.status.success(), "exit: {:?} stderr={}", out.status, String::from_utf8_lossy(&out.stderr));
// 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 OPEN fixture has
/// sweepable knobs, so a nonsense axis name (`nope`) is genuinely unknown — distinct
/// from a fully-bound blueprint, which is refused earlier as "nothing to sweep".
#[test]
fn aura_sweep_rejects_an_unknown_axis() {
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args(["sweep", &fixture, "--axis", "nope=1,2", "--name", "f"])
.output()
.expect("spawn aura sweep unknown-axis");
assert_ne!(out.status.code(), Some(0), "an unknown axis must fail the sweep");
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("Knob") || stderr.to_lowercase().contains("nope"),
"names the unresolved axis: {stderr}"
);
}
/// 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() {
let cwd = temp_cwd("blueprint-reproduce");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let sweep = Command::new(BIN)
.args([
"sweep", &fixture,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
"--trace", "f",
])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep blueprint");
assert!(sweep.status.success(), "sweep exit: {:?} stderr={}", sweep.status, String::from_utf8_lossy(&sweep.stderr));
// `--trace f` names the family `f-0`; reproduce it from disk.
let out = Command::new(BIN)
.args(["reproduce", "f-0"])
.current_dir(&cwd)
.output()
.expect("spawn aura reproduce");
assert!(out.status.success(), "reproduce exit: {:?} stderr={}", out.status, String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
// 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!(
stdout.lines().filter(|l| l.contains("reproduced: bit-identical")).count(),
4,
"every member re-derives bit-identically: {stdout}"
);
assert!(stdout.contains("sma_signal.fast.length=2"), "label renders values portably: {stdout}");
assert!(!stdout.contains("I64("), "label must not leak the Scalar Debug form: {stdout}");
assert!(stdout.contains("reproduced 4/4 members bit-identically"), "summary line: {stdout}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// 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() {
let cwd = temp_cwd("blueprint-reproduce-diverge");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let sweep = Command::new(BIN)
.args([
"sweep", &fixture,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
"--trace", "f",
])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep blueprint");
assert!(sweep.status.success(), "sweep exit: {:?} stderr={}", sweep.status, String::from_utf8_lossy(&sweep.stderr));
// 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 out = Command::new(BIN)
.args(["reproduce", "f-0"])
.current_dir(&cwd)
.output()
.expect("spawn aura reproduce");
assert_eq!(out.status.code(), Some(1), "a diverged member exits 1: stderr={}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(stdout.contains("reproduced: DIVERGED"), "the tampered member is reported DIVERGED: {stdout}");
assert!(stdout.contains("reproduced 3/4 members bit-identically"), "summary counts the diverged member: {stdout}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// 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() {
let cwd = temp_cwd("reproduce_roundtrip");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let sweep = std::process::Command::new(BIN)
.args([
"sweep",
&fixture,
"--axis",
"sma_signal.fast.length=2",
"--axis",
"sma_signal.slow.length=4,6",
"--name",
"smacross",
])
.current_dir(&cwd)
.output()
.expect("run aura sweep");
assert!(sweep.status.success(), "sweep ok: {}", String::from_utf8_lossy(&sweep.stderr));
let repro = std::process::Command::new(BIN)
.args(["reproduce", "smacross-0"])
.current_dir(&cwd)
.output()
.expect("run aura reproduce");
let stdout = String::from_utf8_lossy(&repro.stdout);
assert!(repro.status.success(), "reproduce exits 0: {}", String::from_utf8_lossy(&repro.stderr));
assert!(
stdout.contains("reproduced 2/2 members bit-identically"),
"every member re-derives: {stdout}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (acc 1+3+4): `aura mc <closed blueprint.json> --seeds N` builds a MonteCarlo
/// family (one member per seed), writes exactly one content-addressed blueprint, and
/// `aura reproduce <id>` re-derives every member bit-identically (exit 0).
#[test]
fn aura_mc_over_a_blueprint_reproduces_bit_identically() {
let cwd = temp_cwd("mc-blueprint-reproduce");
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let mc = Command::new(BIN)
.args(["mc", &fixture, "--seeds", "4", "--name", "mcx"])
.current_dir(&cwd)
.output()
.expect("spawn aura mc blueprint");
assert!(mc.status.success(), "mc exit: {:?} stderr={}", mc.status, String::from_utf8_lossy(&mc.stderr));
let mc_out = String::from_utf8(mc.stdout).expect("utf-8 stdout");
// one member line per seed (each carries "seed":n) + one aggregate line.
assert_eq!(
mc_out.lines().filter(|l| l.contains("\"seed\":")).count(),
4,
"one member line per seed: {mc_out}"
);
assert!(mc_out.contains("mc_aggregate"), "prints the aggregate line: {mc_out}");
// exactly one blueprint stored for the whole family (one topology per family).
let store = cwd.join("runs/blueprints");
let entries: Vec<_> = std::fs::read_dir(&store)
.unwrap_or_else(|e| panic!("mc must create the blueprint store {store:?}: {e}"))
.map(|e| e.expect("dir entry").path())
.collect();
assert_eq!(entries.len(), 1, "one stored topology per family: {entries:?}");
let repro = Command::new(BIN)
.args(["reproduce", "mcx-0"])
.current_dir(&cwd)
.output()
.expect("spawn aura reproduce");
assert!(repro.status.success(), "reproduce exit: {:?} stderr={}", repro.status, String::from_utf8_lossy(&repro.stderr));
let repro_out = String::from_utf8(repro.stdout).expect("utf-8 stdout");
assert!(
repro_out.contains("reproduced 4/4 members bit-identically"),
"every MC member re-derives: {repro_out}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// 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.
#[test]
fn aura_walkforward_over_a_blueprint_reproduces_bit_identically() {
let cwd = temp_cwd("blueprint-walkforward-reproduce");
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let run = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", &bp, "--axis", "sma_signal.fast.length=2,3",
"--axis", "sma_signal.slow.length=4,6", "--name", "wfr"])
.current_dir(&cwd)
.output()
.expect("spawn aura walkforward");
assert_eq!(run.status.code(), Some(0), "stderr: {}", String::from_utf8_lossy(&run.stderr));
let repro = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["reproduce", "wfr-0"])
.current_dir(&cwd)
.output()
.expect("spawn aura reproduce");
assert_eq!(repro.status.code(), Some(0));
let out = String::from_utf8(repro.stdout).expect("utf-8");
assert!(out.contains("reproduced 3/3 members bit-identically"), "stdout: {out}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (#173): the loaded-blueprint walk-forward runs + persists a WalkForward
/// family (3 OOS members) with a {"walkforward":…} summary carrying oos_r, and
/// stores exactly one content-addressed blueprint.
#[test]
fn aura_walkforward_over_a_blueprint_persists_a_family() {
let cwd = temp_cwd("blueprint-walkforward-persist");
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", &bp, "--axis", "sma_signal.fast.length=2,3",
"--axis", "sma_signal.slow.length=4,6", "--name", "wfx"])
.current_dir(&cwd)
.output()
.expect("spawn aura walkforward");
assert_eq!(out.status.code(), Some(0), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8");
assert_eq!(stdout.matches("wfx-").count(), 3, "3 OOS member lines: {stdout}");
assert!(stdout.contains("\"walkforward\""), "summary line: {stdout}");
assert!(stdout.contains("\"oos_r\""), "R-measured summary: {stdout}");
let families = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"]).current_dir(&cwd).output().expect("spawn families");
let fo = String::from_utf8(families.stdout).expect("utf-8");
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");
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (#173): an OPEN blueprint with no --axis (nothing to re-fit) is a usage
/// error (exit 2), naming that >= 1 --axis is required.
#[test]
fn aura_walkforward_over_a_blueprint_rejects_no_axis() {
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", &bp])
.output()
.expect("spawn aura walkforward (no axis)");
assert_eq!(out.status.code(), Some(2));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(stderr.contains("requires >= 1 --axis"), "stderr: {stderr}");
}
/// E2E (#173/#184, safety): a malformed blueprint is rejected at the dispatch
/// boundary (exit 2) rather than panicking, phrased house-style (#184's
/// `blueprint_load_prose` convention) — never the raw `UnknownNodeType` Debug
/// leak (c0110 fieldtest finding, mirroring `aura_run_rejects_an_unknown_node_blueprint`).
#[test]
fn aura_walkforward_over_a_blueprint_rejects_malformed() {
let bp = format!("{}/tests/fixtures/unknown_node.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", &bp, "--axis", "sma_signal.fast.length=2,3"])
.output()
.expect("spawn aura walkforward (malformed)");
assert_eq!(out.status.code(), Some(2), "malformed must be rejected, not panic");
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(stderr.contains(r#"unknown node type "Nope""#), "house-style prose names the type: {stderr}");
assert!(!stderr.contains("UnknownNodeType"), "does not leak the Debug variant name: {stderr}");
assert!(!stderr.contains("panicked"), "must reject cleanly: {stderr}");
}
/// E2E (#173, panic-safety): a `--axis` naming a knob that is not in the loaded
/// blueprint's param_space fails clean (named BindError, exit 2, no panic). This
/// pins the in-closure fallible-bind path — `blueprint_sweep_over` returns a
/// `BindError` from inside the `walk_forward` window closure — which is a DISTINCT
/// branch from the dispatch-boundary blueprint-parse rejection (the malformed test):
/// the blueprint here is well-formed and passes dispatch, then the per-window
/// re-fit's bind fails. A regression to `.expect()` on that bind would panic here
/// while leaving the malformed test green.
#[test]
fn aura_walkforward_over_a_blueprint_rejects_an_unknown_axis() {
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", &bp, "--axis", "nope=1,2"])
.output()
.expect("spawn aura walkforward (unknown axis)");
assert_eq!(out.status.code(), Some(2), "an unknown axis must fail clean, not panic");
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("Knob") && stderr.contains("nope"),
"names the unresolved axis via BindError: {stderr}"
);
assert!(!stderr.contains("panicked"), "must reject cleanly, never panic: {stderr}");
}
/// E2E (#177): a rejected `--axis` on a loaded walk-forward blueprint is reported
/// EXACTLY ONCE, however many IS/OOS windows the roll would have spanned — mirroring
/// the sibling `aura sweep` / `aura mc` paths, which validate the axis ONCE up front
/// and emit their rejection a single time. The property this protects: axis
/// resolution is a single pre-flight, not a per-window re-raise.
///
/// The CLOSED fixture has an empty param_space, so ANY `--axis` name is an
/// `UnknownKnob`. The pre-fix code validates the axis INSIDE the per-window closure
/// that `walk_forward` fans out in parallel across the windows, so more than one
/// window `eprintln!`s the same rejection before the racing `exit(2)` tears the
/// process down — a race whose emit count is 1 or 2 per run. A single invocation
/// would therefore be a flaky assertion (it emits once ~1/3 of the time by luck), so
/// this drives the verb repeatedly: observing the double-emit at least once is then
/// near-certain, and the assertion is that EVERY run emits the rejection exactly
/// once. The fix (hoist axis resolution to one pre-flight before the windowed
/// fan-out) makes every run deterministically single.
#[test]
fn aura_walkforward_emits_an_unknown_axis_rejection_once() {
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
for attempt in 1..=20 {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", &bp, "--axis", "graph.fast.length=2,3"])
.output()
.expect("spawn aura walkforward (unknown axis)");
assert_eq!(out.status.code(), Some(2), "an unknown axis must fail clean, not panic");
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(!stderr.contains("panicked"), "must reject cleanly, never panic: {stderr}");
let emits = stderr.matches("UnknownKnob").count();
assert_eq!(
emits, 1,
"attempt {attempt}: the axis rejection must be emitted exactly once, got {emits}: {stderr:?}",
);
}
}
/// E2E (acc 2): `aura mc <open blueprint.json>` is rejected with a named error + exit 2
/// (NOT a panic / exit 101) — MC binds no axis, so a free knob has no binder.
#[test]
fn aura_mc_rejects_an_open_blueprint() {
let cwd = temp_cwd("mc-blueprint-open-reject");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args(["mc", &fixture, "--seeds", "4"])
.current_dir(&cwd)
.output()
.expect("spawn aura mc open-blueprint");
assert_eq!(out.status.code(), Some(2), "an open blueprint fails clean (exit 2, not a panic): stderr={}", String::from_utf8_lossy(&out.stderr));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(stderr.contains("closed blueprint"), "names the closed-blueprint requirement: {stderr}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (#176): `aura run <open blueprint.json>` refuses an open blueprint the same way the
/// sibling `aura mc` does — a clean named error + exit 2, NEVER a `compile_with_params`
/// arity panic (exit 101). The `run` dispatch bootstraps over the EMPTY point (a
/// closed-blueprint assumption), so a free knob must be rejected at the dispatch boundary
/// before that bootstrap, mirroring `blueprint_mc_family`'s closed-guard.
#[test]
fn aura_run_rejects_an_open_blueprint_without_panicking() {
let cwd = temp_cwd("run-blueprint-open-reject");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args(["run", &fixture])
.current_dir(&cwd)
.output()
.expect("spawn aura run open-blueprint");
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert_eq!(
out.status.code(),
Some(2),
"an open blueprint fails clean (exit 2, not a panic/exit 101): stderr={stderr}"
);
assert!(
stderr.contains("closed blueprint"),
"names the closed-blueprint requirement (mirroring `aura mc`): {stderr}"
);
assert!(
!stderr.contains("panicked"),
"must refuse at the dispatch boundary, never panic: {stderr}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// 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);
}
/// E2E (refuse-don't-guess): `aura reproduce` on a HARD-WIRED sweep family — its members
/// carry no `topology_hash` (not built from a blueprint) — exits non-zero, naming that the
/// member is not a generated run. Content-addressed reproduction covers only generated runs.
#[test]
fn aura_reproduce_rejects_a_non_generated_family() {
let cwd = temp_cwd("reproduce_non_generated");
let sweep = std::process::Command::new(BIN)
.args(["sweep", "--name", "hw"])
.current_dir(&cwd)
.output()
.expect("run aura sweep");
assert!(sweep.status.success(), "hard-wired sweep ok: {}", String::from_utf8_lossy(&sweep.stderr));
let out = std::process::Command::new(BIN)
.args(["reproduce", "hw-0"])
.current_dir(&cwd)
.output()
.expect("run aura reproduce");
assert!(!out.status.success(), "a non-generated family exits non-zero");
assert!(
String::from_utf8_lossy(&out.stderr).contains("not a generated run"),
"names the cause: {}",
String::from_utf8_lossy(&out.stderr)
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// 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() {
let cwd = temp_cwd("reproduce_missing_store");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let sweep = std::process::Command::new(BIN)
.args([
"sweep",
&fixture,
"--axis",
"sma_signal.fast.length=2",
"--axis",
"sma_signal.slow.length=4",
"--name",
"bp",
])
.current_dir(&cwd)
.output()
.expect("run aura sweep");
assert!(sweep.status.success(), "blueprint sweep ok: {}", String::from_utf8_lossy(&sweep.stderr));
std::fs::remove_dir_all(cwd.join("runs").join("blueprints")).expect("remove the blueprint store");
let out = std::process::Command::new(BIN)
.args(["reproduce", "bp-0"])
.current_dir(&cwd)
.output()
.expect("run aura reproduce");
assert!(!out.status.success(), "a missing stored blueprint exits non-zero");
assert!(
String::from_utf8_lossy(&out.stderr).contains("missing from store"),
"names the cause: {}",
String::from_utf8_lossy(&out.stderr)
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (#169): `aura sweep <open blueprint.json> --list-axes` prints one
/// `<name>:<kind>` line per open sweepable knob, in `param_space()` order, and
/// exits 0. The names are prefixed by the wrapping (sma_signal.*) — exactly
/// the strings `--axis` binds — so a user can discover then sweep without guessing.
#[test]
fn aura_sweep_list_axes_prints_prefixed_names() {
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["sweep", &bp, "--list-axes"])
.output()
.expect("spawn aura sweep --list-axes");
assert_eq!(out.status.code(), Some(0));
let stdout = String::from_utf8(out.stdout).expect("utf-8");
assert_eq!(stdout, "sma_signal.fast.length:I64\nsma_signal.slow.length:I64\n");
}
/// E2E (#169): `--list-axes` on a CLOSED blueprint (all knobs bound) prints
/// nothing and exits 0 — an empty axis set is the honest answer, not an error.
#[test]
fn aura_sweep_list_axes_closed_is_empty() {
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["sweep", &bp, "--list-axes"])
.output()
.expect("spawn aura sweep --list-axes (closed)");
assert_eq!(out.status.code(), Some(0));
assert!(out.stdout.is_empty(), "a closed blueprint has no open axes");
}
/// E2E (#169): `--list-axes` is a standalone query — combining it with a real
/// sweep flag (`--axis`) is a usage error (exit 2), naming that it lists axes
/// and takes no other flags. A query cannot also seed a sweep.
#[test]
fn aura_sweep_list_axes_rejects_other_flags() {
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["sweep", &bp, "--list-axes", "--axis", "sma_signal.fast.length=2,3"])
.output()
.expect("spawn aura sweep --list-axes + --axis");
assert_eq!(out.status.code(), Some(2));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(stderr.contains("--list-axes lists axes"), "stderr was: {stderr}");
}
/// E2E (#169/#184, safety): `--list-axes` over a malformed blueprint rejects
/// cleanly (exit 2), phrased house-style — never the raw `UnknownNodeType`
/// Debug leak (c0110 fieldtest finding) — rather than panicking; the axis
/// probe reloads the doc, so the dispatch-boundary validation must run first,
/// never a raw `.expect`.
#[test]
fn aura_sweep_list_axes_rejects_malformed_blueprint() {
let bp = format!("{}/tests/fixtures/unknown_node.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["sweep", &bp, "--list-axes"])
.output()
.expect("spawn aura sweep --list-axes (malformed)");
assert_eq!(out.status.code(), Some(2), "malformed blueprint must be rejected, not panic");
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(stderr.contains(r#"unknown node type "Nope""#), "house-style prose names the type: {stderr}");
assert!(!stderr.contains("UnknownNodeType"), "does not leak the Debug variant name: {stderr}");
assert!(!stderr.contains("panicked"), "must reject cleanly, not panic: {stderr}");
}
/// E2E (#210 c0110 fieldtest, "aura sweep <op-script> rejects the op-script
/// array with a raw serde error"): `aura sweep`'s blueprint slot accepts only
/// the built blueprint envelope; an un-built op-script (a JSON array) is
/// shape-discriminated and refused with a targeted, house-style hint — never
/// the leaked `Json(Error("invalid type: map, expected u32", ...))`.
#[test]
fn aura_sweep_rejects_an_op_script_array_with_a_build_first_hint() {
let dir = temp_cwd("sweep_rejects_op_script");
let op_script = dir.join("ops.json");
std::fs::write(&op_script, r#"[{"op":"source","role":"price","kind":"F64"}]"#)
.expect("write op-script fixture");
let out = Command::new(BIN)
.args(["sweep", op_script.to_str().unwrap(), "--list-axes"])
.output()
.expect("spawn aura sweep (op-script)");
assert_eq!(out.status.code(), Some(2), "an op-script array must be refused, not panic");
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(
stderr.contains("op-script") && stderr.contains("aura graph build"),
"names the op-script cause + the fix: {stderr}"
);
assert!(!stderr.contains("Json(Error"), "does not leak the raw serde error: {stderr}");
let _ = std::fs::remove_dir_all(&dir);
}
/// E2E (#210 c0110 fieldtest, house-style loader prose): a genuinely malformed
/// (syntactically invalid) JSON document in the sweep blueprint slot is
/// refused with `blueprint_load_prose`'s wording, never the raw `{e:?}` Debug
/// form the dispatch used to print.
#[test]
fn aura_sweep_rejects_malformed_json_house_style() {
let dir = temp_cwd("sweep_rejects_malformed_json");
let bad = dir.join("bad.json");
std::fs::write(&bad, "{not valid json").expect("write malformed fixture");
let out = Command::new(BIN)
.args(["sweep", bad.to_str().unwrap(), "--list-axes"])
.output()
.expect("spawn aura sweep (malformed json)");
assert_eq!(out.status.code(), Some(2), "malformed JSON must be refused, not panic");
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(
stderr.contains("blueprint document is not valid JSON"),
"house-style prose, not a raw Debug leak: {stderr}"
);
assert!(!stderr.contains("Json(Error"), "does not leak the raw serde Debug form: {stderr}");
let _ = std::fs::remove_dir_all(&dir);
}
/// E2E (#169, the load-bearing "listed == swept" invariant): a name emitted by
/// `--list-axes` is accepted VERBATIM by `--axis` — the discovered axis namespace
/// and the swept axis namespace are the same set (shared-probe construction). The
/// axis names here are DERIVED from the `--list-axes` output, never hardcoded, then
/// fed straight back into a real sweep: the sweep exits 0 and persists a `Sweep`
/// family, proving every listed name resolved (no UnknownKnob). This is the
/// discovery-removes-guessing property #169 exists to close — were the list and
/// sweep surfaces ever to diverge, a discovered name would be rejected here and this
/// round-trip would fail, whereas the literal-pinned per-surface tests could not see it.
#[test]
fn aura_sweep_list_axes_names_round_trip_into_a_working_sweep() {
let cwd = temp_cwd("blueprint-list-axes-roundtrip");
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
// 1) discover the axis names (do NOT hardcode them) — this is what a user reads.
let list = Command::new(BIN)
.args(["sweep", &bp, "--list-axes"])
.output()
.expect("spawn aura sweep --list-axes");
assert_eq!(list.status.code(), Some(0));
let listed = String::from_utf8(list.stdout).expect("utf-8");
let names: Vec<String> = listed
.lines()
.map(|l| l.split(':').next().expect("a `name:kind` line").to_string())
.collect();
assert!(!names.is_empty(), "the open fixture must list >= 1 axis");
// 2) feed the DISCOVERED names straight back into a real sweep. Values are
// position-assigned (first axis small, rest large) to stay in a non-degenerate
// SMA regime without re-hardcoding the names; each axis takes two values.
let mut args: Vec<String> = vec!["sweep".into(), bp.clone()];
for (i, name) in names.iter().enumerate() {
args.push("--axis".into());
args.push(format!("{name}={}", if i == 0 { "2,4" } else { "8,16" }));
}
let sweep = Command::new(BIN)
.args(&args)
.current_dir(&cwd)
.output()
.expect("spawn aura sweep over discovered axes");
assert!(
sweep.status.success(),
"every listed name must be a valid --axis; exit={:?} stderr={}",
sweep.status,
String::from_utf8_lossy(&sweep.stderr)
);
// 3) the sweep persisted a Sweep family whose grid spans exactly the discovered
// axes (each bound to two values -> 2^k members) — the names resolved end-to-end.
let fams = Command::new(BIN).args(["runs", "families"]).current_dir(&cwd).output().expect("spawn families");
let fo = String::from_utf8(fams.stdout).expect("utf-8");
assert!(fo.contains("\"kind\":\"Sweep\""), "families: {fo}");
let expected = 1usize << names.len(); // two values per discovered axis
assert!(fo.contains(&format!("\"members\":{expected}")), "grid over the discovered axes: {fo}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// `--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:?}");
}
}
/// Subcommand help is SCOPED: `aura sweep --help` names sweep's own flags and is
/// NOT byte-identical to `aura run --help` — each subcommand documents its own
/// options rather than reprinting one shared global blob (the #131 uniform-help
/// surface retired).
#[test]
fn subcommand_help_is_scoped_not_uniform() {
let sweep = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["sweep", "--help"]).output().expect("spawn");
let run = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--help"]).output().expect("spawn");
assert_eq!(sweep.status.code(), Some(0), "sweep --help exit");
assert_eq!(run.status.code(), Some(0), "run --help exit");
let sweep_out = String::from_utf8_lossy(&sweep.stdout);
let run_out = String::from_utf8_lossy(&run.stdout);
assert!(sweep_out.contains("--strategy"), "sweep help names its flags: {sweep_out:?}");
assert_ne!(sweep_out, run_out, "per-subcommand help must be scoped, not uniform");
}
/// The GNU `--flag=value` equals form is accepted, equivalent to the
/// space-separated `--flag value`.
#[test]
fn gnu_equals_form_is_accepted() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--harness=sma"]).output().expect("spawn");
assert_eq!(out.status.code(), Some(0),
"run --harness=sma should run; stderr: {}", String::from_utf8_lossy(&out.stderr));
}
/// The `--` end-of-options terminator is recognized (a bare `--` after the flags
/// is a no-op that still runs the default harness).
#[test]
fn double_dash_terminator_is_recognized() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--harness", "sma", "--"]).output().expect("spawn");
assert_eq!(out.status.code(), Some(0),
"run ... -- should run; stderr: {}", String::from_utf8_lossy(&out.stderr));
}
/// GNU long-option abbreviation (spec acceptance criterion 4): an unambiguous prefix
/// of a long flag is accepted (`--harn` → `--harness`), via clap `infer_long_args`
/// on the root command (which propagates to subcommands). An LLM/automation caller
/// always writes the full flag, but GNU compliance (the cycle's ratified purpose)
/// includes abbreviation, and clap delivers it with one root attribute.
#[test]
fn long_option_abbreviation_is_accepted() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--harn", "sma"]).output().expect("spawn");
assert_eq!(out.status.code(), Some(0),
"run --harn sma (abbrev of --harness) should run; stderr: {}",
String::from_utf8_lossy(&out.stderr));
}
/// Property (#175, dual-grammar built-in branch stays strict): a leading positional
/// that is NOT an existing `.json` file is a usage error (exit 2), never silently
/// swallowed — for every dual-grammar subcommand (run/sweep/walkforward/mc). The
/// clap migration models the loaded-blueprint `[BLUEPRINT]` as an OPTIONAL positional
/// on one shared args struct; that makes a typo'd leading token (`aura run bogus`,
/// meant as `--harness bogus`) structurally parseable where the old hand-parser
/// rejected any stray token. Each built-in handler therefore re-asserts the guard
/// (`if a.blueprint.is_some() { usage; exit 2 }`). Dropping any one guard would
/// silently run the default harness on a mistyped invocation — the exact regression
/// clap's optional positional invites and this pins shut. Exit 2 preserved (the
/// usage/runtime split is iteration 2); nothing on stdout on the refusal path.
#[test]
fn dual_grammar_stray_positional_is_a_usage_error_not_swallowed() {
// A trailing flag is added where the built-in grammar needs one to reach the
// guard rather than a bare-run default; `bogus` never ends in `.json`, so the
// is_file() discriminator sends every case down the built-in branch.
for argv in [
&["run", "bogus"][..],
&["sweep", "bogus", "--fast", "3"][..],
&["walkforward", "bogus", "--strategy", "sma"][..],
&["mc", "bogus"][..],
] {
let out = Command::new(BIN).args(argv).output().expect("spawn aura <sub> <stray-positional>");
assert_eq!(
out.status.code(),
Some(2),
"{argv:?} must be refused (exit 2), got {:?}; stderr: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
assert!(
out.stdout.is_empty(),
"{argv:?} must not emit a report on stdout: {:?}",
String::from_utf8_lossy(&out.stdout)
);
}
}
/// Property (#175, dual-grammar discriminator keys on `is_file()`, not the suffix):
/// `aura run <name>.json` where the file does NOT exist is treated as the built-in
/// grammar with a stray positional (refused, exit 2, built-in usage naming
/// `--harness`), NOT as a loaded blueprint. The discriminator is
/// `ends_with(".json") && Path::is_file()`; a regression that dropped the `is_file()`
/// conjunct (suffix-only) would send this down the blueprint-read branch and fail
/// with a file-read error instead — a different, misleading refusal. The
/// `--harness`-in-stderr assertion pins that the built-in branch was chosen; the
/// positive `.json`-file path stays covered by `aura_run_loads_and_runs_a_blueprint_file`.
#[test]
fn dual_grammar_discriminator_requires_an_existing_file_not_just_json_suffix() {
let out = Command::new(BIN)
.args(["run", "definitely-not-a-real-file.json"])
.output()
.expect("spawn aura run <nonexistent>.json");
assert_eq!(
out.status.code(),
Some(2),
"a .json name that is not a file must be refused (exit 2), got {:?}; stderr: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
assert!(out.stdout.is_empty(), "no report on stdout: {:?}", String::from_utf8_lossy(&out.stdout));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--harness"),
"the built-in run grammar (not the blueprint-read path) must handle it: {stderr:?}"
);
}
/// The exit-code partition (#175 iteration 2, deviation #8): a USAGE error (a
/// malformed command line) exits 2; a RUNTIME failure (a well-formed command
/// whose needed data/state is missing) exits 1. Pins the partition as a property.
#[test]
fn exit_codes_partition_usage_two_from_runtime_one() {
// usage: an unknown flag is a command-line error → exit 2
let usage = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--bogus"]).output().expect("spawn");
assert_eq!(usage.status.code(), Some(2),
"unknown flag is a usage error → 2; stderr: {}", String::from_utf8_lossy(&usage.stderr));
// runtime: a well-formed command whose symbol has no recorded data → exit 1
let runtime = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--real", "NONEXISTENT"]).output().expect("spawn");
assert_eq!(runtime.status.code(), Some(1),
"no data for a valid command is a runtime failure → 1; stderr: {}",
String::from_utf8_lossy(&runtime.stderr));
}
/// Characterization pin (byte-identity anchor for the mc R-bootstrap dissolution,
/// #210, the last verb). The current `aura mc --strategy r-sma --real` runs the
/// inline rolling walk-forward, pools the per-window OOS trade-R series, and
/// r-bootstraps E[R] (`mc_r_bootstrap_report`). The dissolution reroutes this
/// through the campaign `[std::sweep(argmax), std::walk_forward, std::monte_carlo]`
/// process, whose terminal monte_carlo stage does the identical
/// `StageBootstrap::PooledOos(r_bootstrap(...))` seeded from the campaign seed -- so
/// the campaign seed must carry 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. This pins the EXACT current bootstrap grade of
/// a fixed 2025 GER40 invocation; after the dissolution the same command must
/// reproduce these bytes (the acceptance gate). Gated on the GER40 archive; skips
/// cleanly on a data refusal.
#[test]
fn mc_r_bootstrap_real_e2e_pins_the_exact_current_grade() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("mc-r-bootstrap-exact-grade");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"mc", "--strategy", "r-sma", "--real", "GER40",
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0",
"--block-len", "5", "--resamples", "1000", "--seed", "42",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let grade_line = stdout
.lines()
.find(|l| l.starts_with("{\"mc_r_bootstrap\":"))
.unwrap_or_else(|| panic!("the mc_r_bootstrap grade line: {stdout}"));
let v: serde_json::Value = serde_json::from_str(grade_line).expect("grade line parses as JSON");
let mc = &v["mc_r_bootstrap"];
assert_eq!(mc["block_len"].as_u64(), Some(5), "block_len: {grade_line}");
assert_eq!(mc["n_resamples"].as_u64(), Some(1000), "n_resamples: {grade_line}");
assert_eq!(mc["n_trades"].as_u64(), Some(20681), "pooled OOS trade count: {grade_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: {grade_line}");
assert_eq!(mc["e_r"]["p50"].as_f64(), Some(-0.0023049902245975227), "E[R] median: {grade_line}");
assert_eq!(mc["prob_le_zero"].as_f64(), Some(0.558), "P(E[R] <= 0): {grade_line}");
}
/// Property: `aura mc --strategy r-sma --real` is now 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 leading argmax sweep and its
/// walk_forward stage each persist one family; the terminal monte_carlo stage is an
/// annotator and adds NO 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() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let cwd = temp_cwd("mc-dissolves");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"mc", "--strategy", "r-sma", "--real", "GER40",
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0",
"--block-len", "5", "--resamples", "1000", "--seed", "42",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
// the single grade line is emitted (and nothing else on stdout).
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert_eq!(stdout.lines().count(), 1, "mc prints exactly one grade line: {stdout:?}");
assert!(
stdout.lines().next().is_some_and(|l| l.starts_with("{\"mc_r_bootstrap\":")),
"the one line is the mc_r_bootstrap grade: {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 sugar 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 generated campaign document carries the constant 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 = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(&cwd)
.output()
.expect("spawn families");
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
assert_eq!(
fams_out.lines().filter(|l| l.contains("\"kind\":\"Sweep\"")).count(),
1,
"one Sweep family from the pipeline's leading argmax stage: {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}"
);
}
/// Fork A: the stop is a single risk regime on the dissolved mc path — a multi-value
/// `--stop-length`/`--stop-k` is a usage refusal (exit 2), not a swept axis. Mirrors
/// `walkforward_dissolved_refuses_a_multi_value_stop`. NOT gated — the refusal is pure
/// argument parsing, before any data access.
#[test]
fn mc_dissolved_refuses_a_multi_value_stop() {
let cwd = temp_cwd("mc-multi-stop");
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"mc", "--strategy", "r-sma", "--real", "GER40",
"--fast", "3", "--slow", "12", "--stop-length", "14,20", "--stop-k", "2.0",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "multi-value stop is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("single risk regime"), "stop-regime refusal: {stderr}");
}
/// 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() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let run = |seed: &str| {
let cwd = temp_cwd(&format!("mc-seed-{seed}"));
std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"mc", "--strategy", "r-sma", "--real", "GER40",
"--fast", "3,5", "--slow", "12,20", "--stop-length", "14", "--stop-k", "2.0",
"--block-len", "5", "--resamples", "1000", "--seed", seed,
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(&cwd)
.output()
.expect("spawn aura")
};
let out42 = run("42");
if out42.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out42.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
let out7 = run("7");
assert_eq!(out42.status.code(), Some(0), "exit (seed 42): {:?}", out42.status);
assert_eq!(out7.status.code(), Some(0), "exit (seed 7): {:?}", out7.status);
let parse = |out: &std::process::Output| -> serde_json::Value {
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let line = stdout
.lines()
.find(|l| l.starts_with("{\"mc_r_bootstrap\":"))
.unwrap_or_else(|| panic!("the mc_r_bootstrap grade line: {stdout}"))
.to_string();
serde_json::from_str(&line).expect("grade line parses as JSON")
};
let v42 = parse(&out42);
let v7 = parse(&out7);
assert_eq!(
v42["mc_r_bootstrap"]["n_trades"], v7["mc_r_bootstrap"]["n_trades"],
"the pooled OOS trade-R series is seed-independent (argmax winners): {v42} vs {v7}"
);
assert_ne!(
v42["mc_r_bootstrap"]["e_r"]["mean"], v7["mc_r_bootstrap"]["e_r"]["mean"],
"a different --seed must move the resampled E[R] mean: {v42} vs {v7}"
);
}
/// `aura run` on the shipped closed example reproduces the built-in r-sma grade
/// (#159): the example is the proven data successor to `--harness r-sma`. Survives
/// the Cut-1b builder deletion (depends only on the example + generic run path).
#[test]
fn shipped_r_sma_example_reproduces_the_builtin_grade() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "examples/r_sma.json"])
.output()
.expect("spawn aura run example");
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");
// the same metrics r_sma_single_run_output_golden pins for --harness r-sma
assert!(stdout.contains("\"expectancy_r\":1.2710005136982836"), "{stdout}");
assert!(stdout.contains("\"sqn\":3.141496526818299"), "{stdout}");
assert!(stdout.contains("\"total_pips\":0.34185000000002036"), "{stdout}");
assert!(stdout.contains("\"n_trades\":3"), "{stdout}");
}