Files
Aura/crates/aura-cli/tests/cli_run.rs
T
Brummel 96dc7832e5 test(cli): pin the exact walkforward grade — byte-identity anchor (#210)
The third verb dissolution (walkforward) reroutes the inline per-window
IS-refit (`walkforward_family` -> `select_winner` -> OOS run -> stitch/pool)
through the campaign `std::walk_forward` stage. Two summary fields have no
campaign equivalent today: `stitched_total_pips` (the runner discards the raw
OOS pip curve, `oos_equity: vec![]`) and pooled `oos_r` (only a following
`std::monte_carlo` stage produces it). Before the executor work that closes
those gaps, this pins the EXACT current grade of a fixed 2025 GER40 invocation
so the dissolution must reproduce it byte-for-byte (the acceptance gate).

The grid is deliberately multi-point (fast 3,5 x slow 12,20): it makes the
per-window winner selection non-degenerate (`param_stability` varies across the
9 windows), so a winner-pick divergence between the inline and campaign refit
cannot ship green — the same gap the generalize anchor closed for its sibling
shape-only test. Deterministic (C1); gated on the local GER40 archive, skips
cleanly on a data refusal.

refs #210
2026-07-06 20:47:09 +02:00

5029 lines
249 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", "tests/fixtures/sma_signal.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` and the cost flags structurally parseable, so the
/// dispatch guard must re-reject them — 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.
#[test]
fn run_blueprint_rejects_builtin_only_flags_exit_two() {
for extra in [
&["--trace", "foo"][..],
&["--cost-per-trade", "1"][..],
&["--slip-vol-mult", "1"][..],
&["--carry-per-cycle", "1"][..],
] {
let mut args = vec!["run", "tests/fixtures/sma_signal.json"];
args.extend_from_slice(extra);
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 (#153): the cost-flag units/constraints are discoverable from
/// `aura run --help` — the appended note states the in-R charge, the per-ENGINE-
/// cycle carry semantics, the `>= 0` constraint, and that cost flags are r-sma
/// only. Pre-#153 the usage listed the flags with no unit or scale guidance.
#[test]
fn run_help_surfaces_cost_flag_units_note() {
let out = Command::new(BIN).args(["run", "--help"]).output().expect("spawn aura run --help");
assert_eq!(out.status.code(), Some(0), "run --help exits 0: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
// clap wraps `long_help` at the render width, which can split a multi-word phrase
// across lines; pin the single tokens clap never hyphenates instead of the
// multi-word "per ENGINE cycle".
assert!(stdout.contains("ENGINE"), "help must explain carry units: {stdout}");
assert!(stdout.contains("cycle"), "help must explain the per-cycle carry scale: {stdout}");
assert!(stdout.contains("r-sma"), "help must state the R-harness requirement: {stdout}");
}
/// Property (#153/#175): a malformed cost value (`--cost-per-trade x`, not a f64) is
/// a usage error — exit 2 with a Usage line on STDERR and nothing on stdout. Post-clap
/// migration the terse parse error names the offending flag and points to `--help`
/// (where the units note lives, pinned by `run_help_surfaces_cost_flag_units_note`);
/// clap does not inline `long_help` into a parse error, so the multi-word units note
/// no longer rides the grammar-error path — the pin is the exit code + Usage surface.
#[test]
fn run_malformed_cost_value_usage_error_carries_note_on_stderr() {
let out = Command::new(BIN)
.args(["run", "--harness", "r-sma", "--cost-per-trade", "x"])
.output()
.expect("spawn aura run --cost-per-trade x");
assert_eq!(out.status.code(), Some(2), "a malformed cost value is a usage error (exit 2); stderr: {}",
String::from_utf8_lossy(&out.stderr));
assert!(out.stdout.is_empty(), "a usage error must not emit a report on stdout: {:?}", out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("--cost-per-trade"), "the parse error names the offending flag: {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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal.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 r-sma` runs the R-scored harness — its stdout
/// `RunReport` carries the nested `r` block (the R yardstick), and that block carries
/// the `sqn` field. Pins the selector wiring r-sma → `run_r_sma` at the
/// built-binary boundary; a miswiring to the pip-only SMA arm would drop the `r` key.
#[test]
fn run_harness_r_sma_prints_an_r_block() {
let out = std::process::Command::new(BIN).args(["run", "--harness", "r-sma"]).output().unwrap();
assert!(out.status.success());
let s = String::from_utf8(out.stdout).unwrap();
assert!(s.contains("\"r\":{"), "r-sma run must carry an r block: {s}");
assert!(s.contains("\"sqn\""), "the r block must carry sqn: {s}");
}
/// 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 (#153): a cost flag on a non-R harness (sma/macd, which produce no R)
/// is a usage error — exit 2 with a named diagnostic, no silent no-op, no `runs/`
/// write. The pre-#153 behaviour silently ignored the flag and exited 0.
#[test]
fn run_cost_flag_on_non_r_harness_is_refused_exit_2() {
let dir = temp_cwd("sma-cost-refused");
let out = Command::new(BIN)
.current_dir(&dir)
.args(["run", "--harness", "sma", "--cost-per-trade", "0.001"])
.output()
.expect("spawn aura run --harness sma --cost-per-trade");
assert_eq!(out.status.code(), Some(2), "cost on a non-R harness must exit 2; stderr: {}",
String::from_utf8_lossy(&out.stderr));
assert!(out.stdout.is_empty(), "a refused run must not emit a report: {:?}", out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("R-evaluator harness"), "stderr must name the cause: {stderr:?}");
assert!(!dir.join("runs").exists(), "a refused run must not write the registry");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (#153): the non-R-harness cost guard fires on the IMPLICIT default
/// harness too — `aura run --cost-per-trade ...` with no `--harness` defaults to
/// sma (non-R) and is refused (exit 2, named cause, no `runs/` write). Distinct
/// from the explicit `--harness sma` case: this pins that the guard reads the
/// *resolved* harness (after the `unwrap_or(Sma)` default), so a refactor that only
/// guarded an explicitly-named harness would leak a silent no-op through the default.
#[test]
fn run_cost_flag_on_default_harness_is_refused_exit_2() {
let dir = temp_cwd("default-harness-cost-refused");
let out = Command::new(BIN)
.current_dir(&dir)
.args(["run", "--cost-per-trade", "0.001"])
.output()
.expect("spawn aura run --cost-per-trade (no --harness)");
assert_eq!(out.status.code(), Some(2), "cost on the default (sma) harness must exit 2; stderr: {}",
String::from_utf8_lossy(&out.stderr));
assert!(out.stdout.is_empty(), "a refused run must not emit a report: {:?}", out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("R-evaluator harness"), "stderr must name the cause: {stderr:?}");
assert!(!dir.join("runs").exists(), "a refused run must not write the registry");
let _ = std::fs::remove_dir_all(&dir);
}
/// 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));
}
/// Property: `--trace` on the r-sma harness persists the THIRD `r_equity` tap
/// beside equity/exposure, and that tap round-trips through `aura chart --tap
/// r_equity` into a rendered series. Pins the full author→persist→chart loop for the
/// R-equity series end to end (the pip harnesses write only the two taps).
#[test]
fn r_sma_trace_persists_r_equity_and_charts_it() {
// `temp_cwd` (cli_run.rs:13) gives a unique CWD with no external tempfile dep — the
// pattern the existing trace tests use; it returns a `PathBuf`, so `.join` directly.
let dir = temp_cwd("r-sma-trace");
let run = Command::new(BIN)
.current_dir(&dir)
.args(["run", "--harness", "r-sma", "--trace", "q1"])
.output()
.unwrap();
assert!(run.status.success());
// the third tap is persisted beside equity/exposure
assert!(dir.join("runs/traces/q1/r_equity.json").exists(), "r_equity tap must persist");
let chart = Command::new(BIN)
.current_dir(&dir)
.args(["chart", "q1", "--tap", "r_equity"])
.output()
.unwrap();
assert!(chart.status.success());
let html = String::from_utf8(chart.stdout).unwrap();
assert!(!html.is_empty() && html.contains("r_equity"), "chart must render the r_equity series");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (Task 3, the run-path cost seam): `aura run --harness r-sma
/// --cost-per-trade <c>` charges a flat round-trip cost in R end to end — the run
/// persists a fourth `net_r_equity` tap beside r_equity and the report's
/// `net_expectancy_r` is STRICTLY below the gross `expectancy_r`. Pins the whole
/// flag→ConstantCost node→net_r_equity tap→summarize_r net fold loop (a no-cost run
/// leaves net == gross, the held golden); a regression that dropped the cost node or
/// left the net fold reading an empty stream would collapse net back onto gross here.
#[test]
fn r_sma_cost_run_persists_net_r_equity_and_charges_cost() {
let dir = temp_cwd("r-sma-cost");
let run = Command::new(BIN)
.current_dir(&dir)
.args(["run", "--harness", "r-sma", "--cost-per-trade", "2", "--trace", "c1"])
.output()
.unwrap();
assert!(run.status.success(), "exit: {:?}; stderr: {}", run.status,
String::from_utf8_lossy(&run.stderr));
assert!(dir.join("runs/traces/c1/net_r_equity.json").exists(), "net_r_equity trace persisted");
let s = String::from_utf8(run.stdout).expect("utf-8 stdout");
let v: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
let gross = v["metrics"]["r"]["expectancy_r"].as_f64().unwrap();
let net = v["metrics"]["r"]["net_expectancy_r"].as_f64().unwrap();
assert!(net < gross, "cost charged: net {net} < gross {gross}");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (spec 0082, the cost-graph composition headline): `--cost-per-trade`
/// and `--slip-vol-mult` set together compose — both cost nodes sum into one
/// net-R curve. The combined net is strictly below the flat-cost-only net (the
/// vol-slippage node bites on top), and the `net_r_equity` trace persists.
#[test]
fn r_sma_both_costs_compose_net_below_each_alone() {
let dir = temp_cwd("r-sma-compose");
let run_net = |args: &[&str], trace: &str| -> f64 {
let mut full = vec!["run", "--harness", "r-sma"];
full.extend_from_slice(args);
full.extend_from_slice(&["--trace", trace]);
let run = Command::new(BIN).current_dir(&dir).args(&full).output().unwrap();
assert!(run.status.success(), "exit: {:?}; stderr: {}", run.status,
String::from_utf8_lossy(&run.stderr));
let s = String::from_utf8(run.stdout).expect("utf-8 stdout");
let v: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
v["metrics"]["r"]["net_expectancy_r"].as_f64().unwrap()
};
let net_flat = run_net(&["--cost-per-trade", "2"], "flat");
let net_both = run_net(&["--cost-per-trade", "2", "--slip-vol-mult", "0.5"], "both");
assert!(dir.join("runs/traces/both/net_r_equity.json").exists(), "net_r_equity persisted");
assert!(net_both < net_flat, "composed cost bites more: net_both {net_both} < net_flat {net_flat}");
let _ = std::fs::remove_dir_all(&dir);
}
/// Golden characterization (cycle 0083, the CostNode-trait migration): pins the
/// EXACT `net_expectancy_r` of `aura run --harness r-sma --cost-per-trade 2`, so
/// the ConstantCost-through-CostRunner migration (Task 2) is VERIFIED byte-preserving
/// at the observable boundary, not assumed. The sibling
/// `r_sma_cost_run_persists_net_r_equity_and_charges_cost` only asserts the
/// RELATION `net < gross`; a regression that drifted the cost numerator or the
/// `cost_per_trade / |entry - stop|` R-normalization (the token form the cycle
/// promises to keep verbatim) would keep `net < gross` true and pass that test
/// silently while shifting this value. Pins only the cost-affected float — gross
/// `expectancy_r` is already pinned by `r_sma_single_run_output_golden` and is
/// unchanged by cost. Deterministic over the fixed synthetic stream (C1).
#[test]
fn r_sma_flat_cost_net_expectancy_r_golden() {
let out = Command::new(BIN)
.args(["run", "--harness", "r-sma", "--cost-per-trade", "2"])
.output()
.unwrap();
assert!(out.status.success(), "exit: {:?}; stderr: {}", out.status,
String::from_utf8_lossy(&out.stderr));
let s = String::from_utf8(out.stdout).expect("utf-8 stdout");
let v: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
let net = v["metrics"]["r"]["net_expectancy_r"].as_f64().unwrap();
assert_eq!(
net, -614.3134020253314,
"flat-cost net_expectancy_r drifted from the golden — the ConstantCost \
factor migration is not byte-preserving: {s}"
);
}
/// Golden characterization (cycle 0083, the composed cost path): pins the EXACT
/// `net_expectancy_r` of `aura run --harness r-sma --cost-per-trade 2
/// --slip-vol-mult 0.5`, so the VolSlippageCost-through-CostRunner migration (Task 3)
/// AND the CostSum per-field aggregation reading the shared `COST_FIELD_NAMES`
/// contract (Task 4) are VERIFIED byte-preserving end to end. The sibling
/// `r_sma_both_costs_compose_net_below_each_alone` only asserts the RELATION
/// `net_both < net_flat`; a drift in the vol-scaled numerator, the CostSum field
/// order, or the `cost_graph` composite's per-node `cost[k].<port>` wiring would keep
/// that relation true and pass silently while shifting this value. Deterministic
/// over the fixed synthetic stream (C1).
#[test]
fn r_sma_composed_cost_net_expectancy_r_golden() {
let out = Command::new(BIN)
.args(["run", "--harness", "r-sma", "--cost-per-trade", "2", "--slip-vol-mult", "0.5"])
.output()
.unwrap();
assert!(out.status.success(), "exit: {:?}; stderr: {}", out.status,
String::from_utf8_lossy(&out.stderr));
let s = String::from_utf8(out.stdout).expect("utf-8 stdout");
let v: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
let net = v["metrics"]["r"]["net_expectancy_r"].as_f64().unwrap();
assert_eq!(
net, -615.0304388396047,
"composed-cost net_expectancy_r drifted from the golden — the VolSlippageCost \
factor + CostSum aggregation is not byte-preserving: {s}"
);
}
/// Golden characterization (cycle 0085, the per-held-cycle accrual path): pins the EXACT
/// `net_expectancy_r` of `aura run --harness r-sma --carry-per-cycle 0.5`, so the
/// CarryCost accrual (open_cost grows over the hold, dumps into cum at close) is VERIFIED
/// at the observable boundary. Deterministic over the fixed synthetic stream (C1).
#[test]
fn r_sma_carry_cost_net_expectancy_r_golden() {
let out = Command::new(BIN)
.args(["run", "--harness", "r-sma", "--carry-per-cycle", "0.5"])
.output()
.unwrap();
assert!(out.status.success(), "exit: {:?}; stderr: {}", out.status,
String::from_utf8_lossy(&out.stderr));
let s = String::from_utf8(out.stdout).expect("utf-8 stdout");
let v: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
let net = v["metrics"]["r"]["net_expectancy_r"].as_f64().unwrap();
assert_eq!(
net, -768.2095026600887,
"carry net_expectancy_r drifted from the golden: {s}"
);
}
/// Golden characterization (cycle 0085, composed at-close + accrual): pins the EXACT
/// `net_expectancy_r` of `aura run --harness r-sma --cost-per-trade 2 --carry-per-cycle
/// 0.5` — the per-trade ConstantCost and the per-held-cycle CarryCost sum through
/// cost_graph/CostSum into one net-R curve. Deterministic (C1).
#[test]
fn r_sma_cost_and_carry_composed_net_expectancy_r_golden() {
let out = Command::new(BIN)
.args(["run", "--harness", "r-sma", "--cost-per-trade", "2", "--carry-per-cycle", "0.5"])
.output()
.unwrap();
assert!(out.status.success(), "exit: {:?}; stderr: {}", out.status,
String::from_utf8_lossy(&out.stderr));
let s = String::from_utf8(out.stdout).expect("utf-8 stdout");
let v: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
let net = v["metrics"]["r"]["net_expectancy_r"].as_f64().unwrap();
assert_eq!(
net, -1383.7939051991182,
"composed cost+carry net_expectancy_r drifted from the golden: {s}"
);
}
/// Property (cycle 0085, the approach-B discriminator): with `--carry-per-cycle`, the
/// `net_r_equity` tap BLEEDS continuously during a multi-cycle hold — the gap to the
/// gross `r_equity` strictly grows each held cycle as the carry accrues, rather than
/// stepping only at close (approach A, which net_expectancy_r alone could not tell
/// apart). Read over the synthetic fixture's terminal open hold (the position held to the
/// end, never closing), where cum_cost_in_r is constant so the growth is the open-side
/// accrual alone.
#[test]
fn r_sma_carry_net_r_equity_bleeds_over_the_hold() {
let dir = temp_cwd("r-sma-carry-bleed");
let run = Command::new(BIN)
.current_dir(&dir)
.args(["run", "--harness", "r-sma", "--carry-per-cycle", "0.5", "--trace", "bleed"])
.output()
.unwrap();
assert!(run.status.success(), "exit: {:?}; stderr: {}", run.status,
String::from_utf8_lossy(&run.stderr));
let read_col = |tap: &str| -> Vec<f64> {
let s = std::fs::read_to_string(dir.join(format!("runs/traces/bleed/{tap}.json")))
.unwrap_or_else(|e| panic!("read {tap}.json: {e}"));
json_array_body(&s, "\"columns\":[[")
.split(',')
.map(|x| x.trim().parse::<f64>().unwrap_or_else(|_| panic!("parse {tap} value {x:?}")))
.collect()
};
let r_eq = read_col("r_equity");
let net_eq = read_col("net_r_equity");
assert_eq!(r_eq.len(), net_eq.len(), "taps are co-temporal (parallel arrays)");
let n = r_eq.len();
assert!(n >= 3, "need a multi-cycle terminal hold to observe the bleed; len {n}");
// gap = r_equity net_r_equity = cum_cost_in_r + open_cost_in_r. Over the terminal
// open hold cum is constant, so a strictly growing gap proves the open-side accrual.
let gap = |i: usize| r_eq[i] - net_eq[i];
assert!(
gap(n - 3) < gap(n - 2) && gap(n - 2) < gap(n - 1),
"net_r_equity must bleed during the terminal hold (gap strictly grows): \
gaps {}, {}, {}",
gap(n - 3), gap(n - 2), gap(n - 1)
);
assert!(gap(n - 1) > 0.0, "carry is charged: terminal gap {} > 0", gap(n - 1));
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (cycle 0085 + #153, cost-flag validation symmetry): a NEGATIVE
/// `--carry-per-cycle` is refused at the binary boundary — exit 2, a named
/// non-negativity diagnostic on stderr, nothing on stdout, no `runs/` write — the
/// same shape the sibling `--cost-per-trade`/`--slip-vol-mult` flags enforce. The
/// `v < 0.0` guard is load-bearing: without it a negative carry reaches
/// `CarryCost::new` and PANICs, and a negative price-unit carry would CREDIT R —
/// inverting the accrual sign. Deterministic; the refusal precedes any data access.
#[test]
fn r_sma_negative_carry_per_cycle_refused_non_negative_exit_2() {
let dir = temp_cwd("r-sma-carry-neg");
let out = Command::new(BIN)
.current_dir(&dir)
.args(["run", "--harness", "r-sma", "--carry-per-cycle", "-1"])
.output()
.expect("spawn aura run --carry-per-cycle -1");
assert_eq!(out.status.code(), Some(2),
"negative carry must exit 2, not panic; status: {:?}; stderr: {}",
out.status, String::from_utf8_lossy(&out.stderr));
assert!(out.stdout.is_empty(), "a refused run must not emit a report on stdout: {:?}", out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("must be non-negative"), "negative carry stderr must name the cause: {stderr:?}");
assert!(!dir.join("runs").exists(), "a refused run must not write the registry");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (#153): a negative `--cost-per-trade` is refused at the binary
/// boundary — exit 2, a named non-negativity diagnostic on stderr, no `runs/`
/// write — the sibling of the carry guard, exercising the per-trade arm.
#[test]
fn run_negative_cost_per_trade_refused_non_negative_exit_2() {
let dir = temp_cwd("cost-per-trade-neg");
let out = Command::new(BIN)
.current_dir(&dir)
.args(["run", "--harness", "r-sma", "--cost-per-trade", "-0.5"])
.output()
.expect("spawn aura run --cost-per-trade -0.5");
assert_eq!(out.status.code(), Some(2), "negative cost must exit 2; stderr: {}",
String::from_utf8_lossy(&out.stderr));
assert!(out.stdout.is_empty(), "a refused run must not emit a report: {:?}", out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("must be non-negative"), "stderr must name the cause: {stderr:?}");
assert!(!dir.join("runs").exists(), "a refused run must not write the registry");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (cycle 0085, carry is a monotone debit in the rate): a larger
/// `--carry-per-cycle` makes the run's `net_expectancy_r` STRICTLY more negative. The
/// carry is purely downstream — bias/stop/signal (hence trade count and gross R) are
/// identical across rates — so doubling the per-held-cycle carry can only deepen the
/// charge. The point-goldens pin two exact values but not the DIRECTION: a sign flip that
/// made carry a CREDIT, or a rate that were ignored/clamped, would re-baseline to some
/// other pair of numbers and slip past a golden; this relation survives any future
/// fixture re-baseline. Deterministic over the fixed synthetic stream (C1).
#[test]
fn r_sma_larger_carry_per_cycle_lowers_net_expectancy_r() {
let net = |rate: &str| -> f64 {
let out = Command::new(BIN)
.args(["run", "--harness", "r-sma", "--carry-per-cycle", rate])
.output()
.unwrap();
assert!(out.status.success(), "exit: {:?}; stderr: {}", out.status,
String::from_utf8_lossy(&out.stderr));
let s = String::from_utf8(out.stdout).expect("utf-8 stdout");
let v: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
v["metrics"]["r"]["net_expectancy_r"].as_f64().unwrap()
};
let lo = net("0.5");
let hi = net("1.0");
assert!(hi < lo, "a larger carry must charge more (lower net): carry-1.0 {hi} < carry-0.5 {lo}");
}
/// Property (cycle 0085, the zero-rate boundary is exactly free): `--carry-per-cycle 0`
/// activates the cost path (a CarryCost node IS wired) yet charges NOTHING — the run's
/// `net_expectancy_r` is bit-identical to the gross `expectancy_r`. Guards against an
/// accrual branch that leaks a spurious charge even at rate 0 (e.g. an off-by-one in the
/// open-side mark): such a bug would shift net away from gross while the positive-rate
/// goldens stayed green. Pins the zero ACCRUAL is zero COST invariant relationally, so it
/// survives any future re-baseline of the gross value. Deterministic (C1).
#[test]
fn r_sma_zero_carry_per_cycle_charges_nothing() {
let out = Command::new(BIN)
.args(["run", "--harness", "r-sma", "--carry-per-cycle", "0"])
.output()
.unwrap();
assert!(out.status.success(), "exit: {:?}; stderr: {}", out.status,
String::from_utf8_lossy(&out.stderr));
let s = String::from_utf8(out.stdout).expect("utf-8 stdout");
let v: serde_json::Value = serde_json::from_str(s.trim()).unwrap();
let r = &v["metrics"]["r"];
let net = r["net_expectancy_r"].as_f64().unwrap();
let gross = r["expectancy_r"].as_f64().unwrap();
assert_eq!(net, gross, "zero carry must be exactly free: net {net} == gross {gross}; {s}");
}
/// Property (the spec's dual-yardstick headline): `aura run --harness r-sma` scores
/// ONE signal in BOTH yardsticks at once — the single emitted `RunReport` carries the pip
/// `metrics.total_pips` (off the SimBroker branch) AND the nested `r` block (off the
/// RiskExecutor branch), because one bias is fanned into both. The shipped selector test
/// pins only that the `r` block appears; this pins that the pip branch SURVIVES alongside
/// it. A regression that swapped the pip branch out for the R branch (dropping the dual
/// tap) would still print an `r` block — and pass that test — while silently losing the
/// pip yardstick; this fails it. Asserts on the structural co-presence of both keys, never
/// the volatile metric floats, so it stays deterministic.
#[test]
fn run_harness_r_sma_carries_both_pip_and_r_yardsticks() {
let out = std::process::Command::new(BIN)
.args(["run", "--harness", "r-sma"])
.output()
.expect("spawn aura run --harness r-sma");
assert!(out.status.success(), "exit: {:?}", out.status);
let s = String::from_utf8(out.stdout).expect("utf-8 stdout");
// exactly one report line.
assert_eq!(s.lines().count(), 1, "stdout was: {s:?}");
// the pip yardstick (SimBroker branch) is still present in the same report...
assert!(s.contains("\"total_pips\":"), "r-sma must keep the pip yardstick: {s}");
// ...and the R yardstick (RiskExecutor branch) is folded in alongside it.
assert!(s.contains("\"r\":{"), "r-sma must carry the R yardstick too: {s}");
}
/// Property (C1, the foundational determinism invariant at the new harness boundary):
/// `aura run --harness r-sma` is byte-deterministic — running it twice over the
/// fixed synthetic stream yields BYTE-IDENTICAL stdout (modulo the build-constant git
/// commit, which is equal within one build). The whole engine rests on a backtest being
/// reproducible (C1); the sibling sweep/walkforward paths each have a determinism E2E,
/// but the brand-new dual-tap r-sma run-loop (the SMA→Bias fan into SimBroker + the
/// RiskExecutor + the summarize_r fold) had none. A non-determinism in the new R branch
/// (e.g. an unstable channel drain order or an allocator-ordered fold) would surface as a
/// drift here. Compares the full stdout body, so it pins the metric floats too.
#[test]
fn run_harness_r_sma_is_byte_deterministic_across_runs() {
let run = || {
let out = std::process::Command::new(BIN)
.args(["run", "--harness", "r-sma"])
.output()
.expect("spawn aura run --harness r-sma");
assert!(out.status.success(), "exit: {:?}", out.status);
String::from_utf8(out.stdout).expect("utf-8 stdout")
};
assert_eq!(run(), run(), "the r-sma run must be byte-identical across runs (C1)");
}
/// Golden characterization (cycle 0066): pins the EXACT metric output of the
/// r-sma single run, so the `r_sma_graph` helper extraction (Task 2) is
/// VERIFIED byte-preserving, not assumed. Captured against HEAD before the
/// refactor; the refactor must keep it identical (a drift is a behaviour change →
/// debug). Commit-agnostic: pins only the `metrics` object, since the manifest
/// carries the volatile build commit.
#[test]
fn r_sma_single_run_output_golden() {
let out = Command::new(BIN).args(["run", "--harness", "r-sma"]).output().unwrap();
assert!(out.status.success(), "exit: {:?}", out.status);
let s = String::from_utf8(out.stdout).expect("utf-8 stdout");
let key = "\"metrics\":";
let start = s.find(key).expect("stdout has a metrics object");
let metrics = s[start..].trim_end();
assert_eq!(
metrics,
r#""metrics":{"total_pips":0.34185000000002036,"max_drawdown":0.11139999999998655,"bias_sign_flips":2,"r":{"expectancy_r":1.2710005136982836,"n_trades":3,"win_rate":1.0,"avg_win_r":1.2710005136982836,"avg_loss_r":0.0,"profit_factor":0.0,"max_r_drawdown":0.0,"n_open_at_end":1,"sqn":3.141496526818299,"sqn_normalized":3.141496526818299,"net_expectancy_r":1.2710005136982836,"conviction_terciles_r":[0.9285858482198718,2.0771328641652427,0.8072828287097363]}}}"#,
"r-sma single-run metric output drifted from the golden: {s}"
);
}
/// Property (#133): `aura sweep --strategy r-sma` runs the r-sma harness over
/// the fast×slow signal grid (4 members), each carrying an R block, persisted as a
/// rankable family; `runs family <id> rank sqn` orders them highest-SQN first.
#[test]
fn sweep_strategy_r_sma_ranks_a_family_by_sqn() {
let cwd = temp_cwd("sweep-r-sma");
let out = Command::new(BIN)
.args(["sweep", "--strategy", "r-sma"])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --strategy r-sma");
assert!(
out.status.success(),
"r-sma 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(), 4, "r-sma sweep must print 4 member lines: {stdout:?}");
for line in &lines {
assert!(line.contains("\"r\":{"), "member must carry an r block: {line}");
assert!(line.contains("\"sqn\""), "member r block must carry sqn: {line}");
// each member records the fixed R-defining params (the stop that defines 1R),
// so it is reproducible from its own manifest (C18) and the constant-stop
// basis of cross-member SQN comparability (C10) is auditable from the record.
assert!(
line.contains("\"stop_length\"") && line.contains("\"stop_k\""),
"member manifest must record the fixed R-defining stop params: {line}"
);
}
// rank the family by sqn — highest first.
let rank = Command::new(BIN)
.args(["runs", "family", "sweep-0", "rank", "sqn"])
.current_dir(&cwd)
.output()
.expect("spawn rank sqn");
assert!(rank.status.success(), "rank sqn exit: {:?}", rank.status);
let rank_out = String::from_utf8(rank.stdout).expect("utf-8");
assert_eq!(rank_out.lines().count(), 4, "rank: {rank_out:?}");
let sqns: Vec<f64> = rank_out
.lines()
.map(|l| {
let key = "\"sqn\":";
let start = l.find(key).expect("line has sqn") + key.len();
let tail = &l[start..];
let end = tail.find([',', '}']).expect("token end");
tail[..end].parse().expect("sqn is an f64")
})
.collect();
assert!(sqns.windows(2).all(|w| w[0] >= w[1]), "rank not descending by sqn: {sqns:?}");
// an unknown metric is a usage error (exit 2).
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);
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#130): `sqn_normalized` (SQN100) is a working rank key *through the
/// binary*, not just an in-memory `metric_cmp` arm. A persisted r-sma family
/// must carry the new `sqn_normalized` field in each member's on-disk `r` block,
/// and `aura runs family <id> rank sqn_normalized` must parse the new key (it was
/// an `UnknownMetric` exit-2 error before #130), read the field back from
/// `families.jsonl`, and order the members highest-first. This exercises the CLI
/// parse path + the serde round-trip of the new field through the family store,
/// which the registry unit test (synthetic in-memory reports) never touches. The
/// signal-only grid means each member's n_trades is small (cap inactive), so
/// `sqn_normalized == sqn` per member and the SQN100 ranking matches the raw-SQN
/// ranking — asserted as a cross-check.
#[test]
fn sweep_strategy_r_sma_ranks_a_family_by_sqn_normalized() {
let cwd = temp_cwd("sweep-r-sma-sqn100");
let out = Command::new(BIN)
.args(["sweep", "--strategy", "r-sma"])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --strategy r-sma");
assert!(
out.status.success(),
"r-sma sweep exit: {:?}; stderr: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
// every persisted member carries the new field in its r block.
for line in stdout.lines() {
assert!(
line.contains("\"sqn_normalized\""),
"member r block must carry sqn_normalized: {line}"
);
}
// rank by the NEW key — must succeed (was UnknownMetric/exit-2 before #130).
let rank = Command::new(BIN)
.args(["runs", "family", "sweep-0", "rank", "sqn_normalized"])
.current_dir(&cwd)
.output()
.expect("spawn rank sqn_normalized");
assert!(
rank.status.success(),
"rank sqn_normalized must succeed (exit 0): {:?}; stderr: {}",
rank.status,
String::from_utf8_lossy(&rank.stderr)
);
let rank_out = String::from_utf8(rank.stdout).expect("utf-8");
assert_eq!(rank_out.lines().count(), 4, "rank: {rank_out:?}");
let norms: Vec<f64> = rank_out
.lines()
.map(|l| {
let key = "\"sqn_normalized\":";
let start = l.find(key).expect("line has sqn_normalized") + key.len();
let tail = &l[start..];
let end = tail.find([',', '}']).expect("token end");
tail[..end].parse().expect("sqn_normalized is an f64")
})
.collect();
assert!(
norms.windows(2).all(|w| w[0] >= w[1]),
"rank not descending by sqn_normalized: {norms:?}"
);
// cap inactive at these trade counts -> per member sqn_normalized == sqn, so the
// SQN100 ranking is the same as the raw-sqn ranking.
let by_sqn = Command::new(BIN)
.args(["runs", "family", "sweep-0", "rank", "sqn"])
.current_dir(&cwd)
.output()
.expect("spawn rank sqn");
let by_sqn_out = String::from_utf8(by_sqn.stdout).expect("utf-8");
let sqns: Vec<f64> = by_sqn_out
.lines()
.map(|l| {
let key = "\"sqn\":";
let start = l.find(key).expect("line has sqn") + key.len();
let tail = &l[start..];
let end = tail.find([',', '}']).expect("token end");
tail[..end].parse().expect("sqn is an f64")
})
.collect();
assert_eq!(norms, sqns, "below the cap, SQN100 ranking must equal raw-sqn ranking");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#135): `aura sweep --strategy r-sma --trace <n>` now persists one
/// member dir per grid point (it was refused with exit 2 before). The 2×2 fast×slow
/// grid yields 4 members; each carries the THIRD `r_equity` tap (the r-sma member
/// has the R curve, unlike the two-tap sma/momentum members), and a member is
/// chartable via `chart <n>/<member> --tap r_equity`. `--name` (record the rankable
/// family, no per-member streams) still works.
#[test]
fn sweep_strategy_r_sma_trace_persists_member_dirs_with_r_equity() {
let cwd = temp_cwd("sweep-r-sma-trace");
let traced = Command::new(BIN)
.args(["sweep", "--strategy", "r-sma", "--trace", "t1"])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --strategy r-sma --trace");
assert!(
traced.status.success(),
"r-sma --trace must persist members (exit 0): {:?}; stderr: {}",
traced.status,
String::from_utf8_lossy(&traced.stderr)
);
let base = cwd.join("runs/traces/t1");
let members: Vec<String> = std::fs::read_dir(&base)
.expect("read t1 trace dir")
.map(|e| e.expect("dir entry").file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(members.len(), 4, "2x2 fast×slow r-sma grid = 4 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!(
base.join(m).join("r_equity.json").exists(),
"member {m} must carry the r_equity tap"
);
}
// one member is chartable via chart <name>/<member> --tap r_equity.
let one = &members[0];
let chart = Command::new(BIN)
.args(["chart", &format!("t1/{one}"), "--tap", "r_equity"])
.current_dir(&cwd)
.output()
.expect("spawn aura chart t1/<member> --tap r_equity");
assert!(chart.status.success(), "chart exit: {:?}", chart.status);
assert!(
String::from_utf8_lossy(&chart.stdout).contains("r_equity"),
"chart must render the r_equity series"
);
// --name records the rankable family (no per-member streams) — still succeeds.
let named = Command::new(BIN)
.args(["sweep", "--strategy", "r-sma", "--name", "n1"])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --strategy r-sma --name");
assert!(
named.status.success(),
"r-sma --name must succeed: {:?}; stderr: {}",
named.status,
String::from_utf8_lossy(&named.stderr)
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#137 — headline): `aura sweep --strategy r-sma` accepts the four
/// optional comma-separated grid flags `--fast`, `--slow`, `--stop-length`,
/// `--stop-k`, which become the sweep's grid axes over the four r_sma_graph
/// knobs (sma fast length, sma slow length, vol-stop length, vol-stop k). The
/// family is the cartesian product of the supplied lists; a single-value list per
/// axis is a 1-member family. This is the enabling change that lets the stop
/// timescale track a *slow* signal (the pinned 3-bar = 3-minute vol-stop is far too
/// fast for a 240/960-bar momentum cross) — so a slow time-series-momentum edge can
/// be screened across timeframes, stop included.
///
/// Mirrors `sweep_strategy_r_sma_ranks_a_family_by_sqn`: drive the built binary,
/// read each stdout member line (the assigned `family_id` + the nested RunReport),
/// and inspect the manifest `params` array — whose on-disk shape is
/// `["fast.length",{"I64":N}]` / `["stop_k",{"F64":K}]` (verified against the live
/// CLI output). The single member must carry exactly fast=240, slow=960,
/// stop_length=240, stop_k=2.0. Today every one of these four flags is an unknown
/// token rejected with exit 2 (the grid only honours the hardcoded fast×slow), so
/// this is RED for the right reason: the flags + gridding are absent.
#[test]
fn sweep_strategy_r_sma_grids_all_four_knobs_from_csv_flags() {
let cwd = temp_cwd("sweep-r-sma-grid-flags");
let out = Command::new(BIN)
.args([
"sweep",
"--strategy",
"r-sma",
"--fast",
"240",
"--slow",
"960",
"--stop-length",
"240",
"--stop-k",
"2.0",
"--name",
"g1",
])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --strategy r-sma with grid flags");
assert!(
out.status.success(),
"gridded r-sma sweep must succeed (exit 0): {:?}; 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();
// a one-value-per-axis grid is a 1-member family (cartesian product of singletons).
assert_eq!(
lines.len(),
1,
"single-value lists on all four axes = a 1-member family: {stdout:?}"
);
let member = lines[0];
// the member is a real run (carries an R block), not a "0 ran" no-op.
assert!(member.contains("\"r\":{"), "member must carry an r block: {member}");
// the single member's four griddable knobs take the supplied values — the
// on-disk manifest-params shape (verified against live CLI output).
for needle in [
"[\"fast.length\",{\"I64\":240}]",
"[\"slow.length\",{\"I64\":960}]",
"[\"stop_length\",{\"I64\":240}]",
"[\"stop_k\",{\"F64\":2.0}]",
] {
assert!(
member.contains(needle),
"member must grid {needle} from the CSV flag: {member}"
);
}
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#137 — default preservation, the hard constraint): when the grid flags
/// are ABSENT, `aura sweep --strategy r-sma` must produce *exactly today's*
/// family — the 2×2 fast×slow grid {2,3}×{6,12} (4 members) with the stop pinned at
/// stop_length=3, stop_k=2.0. This guards the byte-greenness of every existing
/// golden/sweep test once the four flags land: a no-flags invocation falls back to
/// the historical defaults, not to an empty/changed grid. It pins the contract that
/// the flag wiring must not move the no-flags baseline.
#[test]
fn sweep_strategy_r_sma_no_flags_keeps_the_default_grid() {
let cwd = temp_cwd("sweep-r-sma-default-grid");
let out = Command::new(BIN)
.args(["sweep", "--strategy", "r-sma"])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --strategy r-sma");
assert!(
out.status.success(),
"no-flags r-sma 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(), 4, "no-flags grid = the historical 2×2 = 4 members: {stdout:?}");
// the four members are exactly the {2,3}×{6,12} fast×slow cartesian product,
// each with the historical pinned stop (stop_length=3, stop_k=2.0).
let mut fast_slow: Vec<(&str, &str)> = Vec::new();
for m in &lines {
assert!(
m.contains("[\"stop_length\",{\"I64\":3}]") && m.contains("[\"stop_k\",{\"F64\":2.0}]"),
"no-flags member must keep the historical pinned stop: {m}"
);
let fast = if m.contains("[\"fast.length\",{\"I64\":2}]") {
"2"
} else {
assert!(m.contains("[\"fast.length\",{\"I64\":3}]"), "fast must be 2 or 3: {m}");
"3"
};
let slow = if m.contains("[\"slow.length\",{\"I64\":6}]") {
"6"
} else {
assert!(m.contains("[\"slow.length\",{\"I64\":12}]"), "slow must be 6 or 12: {m}");
"12"
};
fast_slow.push((fast, slow));
}
fast_slow.sort_unstable();
assert_eq!(
fast_slow,
vec![("2", "12"), ("2", "6"), ("3", "12"), ("3", "6")],
"no-flags grid must be exactly {{2,3}}×{{6,12}}: {fast_slow:?}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// 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}")
}
/// Property (#138, task 8 — the streaming-reduction wiring invariant): a no-trace
/// `aura sweep --strategy r-sma` (the *folded* path — `SeriesReducer` folds the
/// eq/ex series to one summary row each, `GatedRecorder` retains only the gated R
/// rows, so memory is O(trades) not O(cycles)) reports **byte-for-byte the same
/// `metrics` object per member** as the `--trace` path (the *raw-recorder*
/// reference that buffers every per-cycle row). This pins the actual M3 fix at the
/// CLI seam: the engine-level Task-7 equivalence tests prove the sink nodes agree
/// in isolation, but only this end-to-end check proves the CLI *wiring* —
/// `reduce = trace.is_none()` selecting the folding topology, and the folded
/// consumer reconstructing `RunMetrics` (`total_pips`/`max_drawdown` from the eq
/// `SeriesReducer` row, `bias_sign_flips` from the ex row, `r` from `summarize_r`
/// over the gated rows) — produces the identical observable report. Members are
/// matched in stdout order (both paths iterate the same deterministic grid, C1),
/// and the `metrics` block excludes the family_id, which is allowed to differ.
#[test]
fn sweep_strategy_r_sma_folded_no_trace_metrics_equal_raw_trace_metrics() {
let cwd = temp_cwd("sweep-r-sma-fold-vs-raw");
// folded path: no --trace -> reduce = true (SeriesReducer + GatedRecorder).
let folded = Command::new(BIN)
.args(["sweep", "--strategy", "r-sma"])
.current_dir(&cwd)
.output()
.expect("spawn folded (no-trace) r-sma sweep");
assert!(
folded.status.success(),
"folded no-trace sweep exit: {:?}; stderr: {}",
folded.status,
String::from_utf8_lossy(&folded.stderr)
);
let folded_out = String::from_utf8(folded.stdout).expect("utf-8");
// raw path: --trace -> reduce = false (the verbatim Recorder reference).
let raw = Command::new(BIN)
.args(["sweep", "--strategy", "r-sma", "--trace", "t1"])
.current_dir(&cwd)
.output()
.expect("spawn raw (--trace) r-sma sweep");
assert!(
raw.status.success(),
"raw --trace 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();
// both paths run the same 2×2 grid -> 4 members, in the same deterministic order.
assert_eq!(folded_lines.len(), 4, "folded sweep must print 4 members: {folded_out:?}");
assert_eq!(
raw_lines.len(),
folded_lines.len(),
"the two paths must yield the same member count: 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);
// the metrics block is non-trivial: it must carry the R yardstick, so this
// is comparing a real reduced output, not two empty objects.
assert!(fm.contains("\"r\":{"), "folded member {i} must carry an r block: {fm}");
assert_eq!(
fm, rm,
"folded vs raw metrics diverge at member {i}\n folded: {fm}\n raw: {rm}"
);
}
let _ = std::fs::remove_dir_all(&cwd);
}
/// The breakout strategy reaches the CLI seam: `aura sweep --strategy r-breakout`
/// emits an R-bearing member, and the folded no-trace path equals the raw --trace path
/// byte-for-byte (parity with the r-sma fold-vs-raw guard). channel 3 warms up on
/// the synthetic stream; one channel value × the single default stop = one member.
#[test]
fn sweep_strategy_r_breakout_folded_no_trace_metrics_equal_raw_trace_metrics() {
let cwd = temp_cwd("sweep-r-breakout-fold-vs-raw");
let folded = Command::new(BIN)
.args(["sweep", "--strategy", "r-breakout", "--channel", "3"])
.current_dir(&cwd)
.output()
.expect("spawn folded (no-trace) r-breakout sweep");
assert!(
folded.status.success(),
"folded no-trace breakout 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-breakout", "--channel", "3", "--trace", "b1"])
.current_dir(&cwd)
.output()
.expect("spawn raw (--trace) r-breakout sweep");
assert!(
raw.status.success(),
"raw --trace breakout 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 breakout 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 breakout member {i} must carry an r block: {fm}");
assert_eq!(
fm, rm,
"folded vs raw breakout metrics diverge at member {i}\n folded: {fm}\n raw: {rm}"
);
}
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: the breakout family is the cartesian product of its grid axes — a
/// multi-value `--channel` list yields exactly one member per channel value, each
/// member carrying its own bound channel length in the manifest params. Guards the
/// manual cartesian-product loop in `r_breakout_sweep_family`: a regression that
/// collapsed the loop (sweeping only the last value, or ganging the two channels)
/// would silently drop members here. Channels 2 and 3 both warm up on the 18-bar
/// synthetic stream, so two members must appear, in grid order, distinctly.
#[test]
fn sweep_strategy_r_breakout_grids_one_member_per_channel() {
let cwd = temp_cwd("sweep-r-breakout-channel-grid");
let out = Command::new(BIN)
.args(["sweep", "--strategy", "r-breakout", "--channel", "2,3"])
.current_dir(&cwd)
.output()
.expect("spawn 2-channel r-breakout sweep");
assert!(
out.status.success(),
"channel-grid breakout 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-channel grid must print 2 members: {stdout:?}");
// each member carries its own bound channel length (grid order: 2 then 3).
assert!(
lines[0].contains("[\"channel\",{\"I64\":2}]"),
"first member must bind channel=2: {}",
lines[0]
);
assert!(
lines[1].contains("[\"channel\",{\"I64\":3}]"),
"second member must bind channel=3: {}",
lines[1]
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// 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: `aura walkforward --strategy r-sma` on synthetic data reports R
/// quality per window AND pooled across windows — every per-window member line
/// carries a `metrics.r` block (the windowed reduce-mode IS sweep folds the dense
/// R-record), and the summary line carries a pooled `oos_r` block (the across-window
/// reduction of the OOS per-trade R series). Deterministic: a second run is
/// byte-identical (C1).
#[test]
fn walkforward_strategy_r_sma_reports_oos_r() {
let run = || {
let cwd = temp_cwd("wf-r-sma");
let out = Command::new(BIN)
.args(["walkforward", "--strategy", "r-sma"])
.current_dir(&cwd)
.output()
.expect("spawn aura walkforward --strategy r-sma");
assert!(
out.status.success(),
"walkforward --strategy r-sma exit: {:?}; stderr: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let _ = std::fs::remove_dir_all(&cwd);
stdout
};
let out = run();
let lines: Vec<&str> = out.trim().lines().collect();
let summary: serde_json::Value = serde_json::from_str(lines.last().unwrap()).unwrap();
assert!(summary["walkforward"]["oos_r"].is_object(), "summary carries oos_r");
assert!(summary["walkforward"]["oos_r"]["n_trades"].is_number());
// each per-window member line carries metrics.r
for l in &lines[..lines.len() - 1] {
let v: serde_json::Value = serde_json::from_str(l).unwrap();
assert!(v["report"]["metrics"]["r"].is_object(), "per-window r block present");
}
let out2 = run();
assert_eq!(out, out2, "r-sma walkforward is deterministic");
}
/// Property: a r-sma walk-forward stamps each OOS winner's manifest with the
/// trials-deflation provenance (#144), and `runs family <id> rank` surfaces it as a
/// human-readable `deflated=… P(overfit)=…` line beside each member's JSON. This is
/// the end-to-end witness that the selection record reaches disk (C18) and the
/// display path renders it; sweep/mc families stay unstamped (verified elsewhere).
#[test]
fn runs_family_rank_shows_deflated_line() {
let cwd = temp_cwd("runs-deflated");
let wf = Command::new(BIN)
.args(["walkforward", "--strategy", "r-sma", "--name", "wf-defl"])
.current_dir(&cwd)
.output()
.expect("spawn walkforward --strategy r-sma --name wf-defl");
assert!(
wf.status.success(),
"walkforward exit: {:?}; stderr: {}",
wf.status,
String::from_utf8_lossy(&wf.stderr)
);
let rank = Command::new(BIN)
.args(["runs", "family", "wf-defl-0", "rank", "sqn_normalized"])
.current_dir(&cwd)
.output()
.expect("spawn runs family wf-defl-0 rank sqn_normalized");
assert!(rank.status.success(), "rank exit: {:?}", rank.status);
let rank_out = String::from_utf8(rank.stdout).expect("utf-8");
let _ = std::fs::remove_dir_all(&cwd);
assert!(rank_out.contains("deflated="), "rank output missing deflated=: {rank_out:?}");
assert!(rank_out.contains("P(overfit)="), "rank output missing P(overfit)=: {rank_out:?}");
// each stamped member's JSON also carries the selection block (selection_metric).
assert!(
rank_out.contains("\"selection_metric\":\"sqn_normalized\""),
"selection block present: {rank_out:?}"
);
}
/// Property: a r-sma walk-forward run with `--select plateau:mean` selects the
/// neighbourhood-smoothed winner and stamps each OOS manifest with the plateau
/// provenance (`mode = PlateauMean`, `neighbourhood_score`, `n_neighbours`); `runs
/// family … rank` renders the `plateau(mean)=… over N cells` line, and the
/// deflation fields are omitted (orthogonal annotation). End-to-end witness that
/// the opt-in selection rule reaches disk (C18) and the display path renders it.
#[test]
fn walkforward_plateau_select_stamps_plateau_provenance() {
let cwd = temp_cwd("runs-plateau");
let wf = Command::new(BIN)
.args([
"walkforward", "--strategy", "r-sma", "--select", "plateau:mean",
"--fast", "50,100", "--slow", "200,400",
"--stop-length", "14,21", "--stop-k", "2.0,3.0",
"--name", "wf-plat",
])
.current_dir(&cwd)
.output()
.expect("spawn walkforward --select plateau:mean");
assert!(
wf.status.success(),
"walkforward exit: {:?}; stderr: {}",
wf.status,
String::from_utf8_lossy(&wf.stderr)
);
let rank = Command::new(BIN)
.args(["runs", "family", "wf-plat-0", "rank", "sqn_normalized"])
.current_dir(&cwd)
.output()
.expect("spawn runs family wf-plat-0 rank sqn_normalized");
assert!(rank.status.success(), "rank exit: {:?}", rank.status);
let rank_out = String::from_utf8(rank.stdout).expect("utf-8");
let _ = std::fs::remove_dir_all(&cwd);
assert!(rank_out.contains("plateau(mean)="), "rank output missing plateau(mean)=: {rank_out:?}");
assert!(rank_out.contains(" cells"), "rank output missing 'over N cells': {rank_out:?}");
assert!(rank_out.contains("\"mode\":\"PlateauMean\""), "manifest carries PlateauMean: {rank_out:?}");
assert!(!rank_out.contains("\"deflated_score\""), "plateau run omits deflated_score: {rank_out:?}");
}
/// Property (C23, the opt-in feature's headline guarantee): `--select argmax` is a
/// no-op against the default. A r-sma walk-forward run with an EXPLICIT
/// `--select argmax` produces stdout byte-for-byte identical to the same run with
/// no `--select` flag at all, AND keeps the trials-deflation provenance
/// (`mode == Argmax`, the deflated annotation) on each OOS manifest. Pins both
/// halves of "plateau is strictly opt-in": the flag's default IS argmax (no
/// divergence), and threading the new `Selection` did not silently demote argmax to
/// a bare pick (the #144 deflation path survives). A regression where `--select`
/// perturbed the default path, or where the default switched away from argmax,
/// passes every other test but fails this byte-equality.
#[test]
fn walkforward_select_argmax_is_byte_identical_to_default() {
let run = |args: &[&str]| {
let cwd = temp_cwd("wf-argmax-noop");
let out = Command::new(BIN)
.arg("walkforward")
.args(args)
.current_dir(&cwd)
.output()
.expect("spawn aura walkforward");
let _ = std::fs::remove_dir_all(&cwd);
assert!(
out.status.success(),
"walkforward exit: {:?}; stderr: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).expect("utf-8")
};
let default = run(&["--strategy", "r-sma"]);
let explicit = run(&["--strategy", "r-sma", "--select", "argmax"]);
assert_eq!(default, explicit, "explicit --select argmax must be a no-op vs the default");
// argmax stayed the deflation path (not a bare argmax): the per-window member
// JSON carries the Argmax mode and its deflation annotation.
assert!(default.contains("\"mode\":\"Argmax\""), "argmax manifest mode: {default:?}");
assert!(default.contains("\"deflated_score\""), "argmax keeps the deflation annotation: {default:?}");
assert!(!default.contains("\"neighbourhood_score\""), "argmax omits the plateau annotation: {default:?}");
}
/// Property: `--select plateau:worst` reaches disk and the display path through the
/// DISTINCT worst-case branch — a separate `SelectionMode::PlateauWorst` enum
/// variant on the wire and a separate `plateau(worst)=` display label. The landed
/// happy-path E2E exercises only `plateau:mean`; the worst arm's serialization
/// variant and its display label are otherwise untouched end-to-end, so a
/// regression that mislabelled worst as mean (or failed to round-trip the variant)
/// would pass the mean test. Same fixture grid as the mean E2E, only the rule
/// differs.
#[test]
fn walkforward_plateau_worst_stamps_worst_variant_and_label() {
let cwd = temp_cwd("runs-plateau-worst");
let wf = Command::new(BIN)
.args([
"walkforward", "--strategy", "r-sma", "--select", "plateau:worst",
"--fast", "50,100", "--slow", "200,400",
"--stop-length", "14,21", "--stop-k", "2.0,3.0",
"--name", "wf-worst",
])
.current_dir(&cwd)
.output()
.expect("spawn walkforward --select plateau:worst");
assert!(
wf.status.success(),
"walkforward exit: {:?}; stderr: {}",
wf.status,
String::from_utf8_lossy(&wf.stderr)
);
let rank = Command::new(BIN)
.args(["runs", "family", "wf-worst-0", "rank", "sqn_normalized"])
.current_dir(&cwd)
.output()
.expect("spawn runs family wf-worst-0 rank sqn_normalized");
assert!(rank.status.success(), "rank exit: {:?}", rank.status);
let rank_out = String::from_utf8(rank.stdout).expect("utf-8");
let _ = std::fs::remove_dir_all(&cwd);
assert!(rank_out.contains("plateau(worst)="), "rank output missing plateau(worst)=: {rank_out:?}");
assert!(rank_out.contains("\"mode\":\"PlateauWorst\""), "manifest carries PlateauWorst: {rank_out:?}");
// the worst label must not be rendered or stamped as mean
assert!(!rank_out.contains("plateau(mean)="), "worst run must not render the mean label: {rank_out:?}");
assert!(!rank_out.contains("\"mode\":\"PlateauMean\""), "worst run must not stamp PlateauMean: {rank_out:?}");
}
/// 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 yet (e.g.
/// `r-breakout`) 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 (`r-breakout`), 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", "r-breakout"])
.current_dir(&cwd)
.output()
.expect("spawn aura walkforward --strategy r-breakout");
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("r-breakout"),
"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: `aura mc --strategy r-sma` runs the r-sma walk-forward on
/// synthetic data, bootstraps the pooled OOS per-trade R series, and prints exactly
/// one canonical `mc_r_bootstrap` line carrying an `e_r` distribution block and
/// `prob_le_zero`. Deterministic: a second run with the same (default) seed is
/// byte-identical (C1). Frictionless gross R — no costs, no `runs/` family write.
#[test]
fn mc_strategy_r_sma_prints_a_bootstrap_line_deterministically() {
let dir = temp_cwd("mc_r_sma_boot");
let run = || {
Command::new(BIN)
.args(["mc", "--strategy", "r-sma", "--resamples", "200"])
.current_dir(&dir)
.output()
.expect("spawn aura mc --strategy r-sma")
};
let out = run();
assert!(out.status.success(), "exit: {:?} stderr: {}", out.status, String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert_eq!(stdout.lines().count(), 1, "exactly one mc_r_bootstrap line: {stdout:?}");
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("canonical JSON");
assert!(v["mc_r_bootstrap"]["e_r"]["mean"].is_number(), "e_r block present: {stdout}");
assert!(v["mc_r_bootstrap"]["prob_le_zero"].is_number(), "prob_le_zero present: {stdout}");
assert_eq!(v["mc_r_bootstrap"]["n_resamples"], 200);
let out2 = run();
assert_eq!(stdout.as_bytes(), out2.stdout.as_slice(), "same seed -> byte-identical line (C1)");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property: `--block-len` is the moving-block knob, and it is observable AND
/// load-bearing at the binary boundary — it round-trips into the emitted
/// `block_len` field, and a different `block-len` (over the same seed + pooled R
/// series) yields a genuinely different E[R] distribution. This is what separates
/// the moving-block bootstrap from a plain i.i.d. trade shuffle: contiguous runs
/// preserve serial correlation a `block-len 1` shuffle erases. Without this test a
/// regression that dropped `--block-len` on the floor (or wired it to `block_len 1`
/// always) would still pass the happy-path E2E. Same-seed determinism (C1) pins
/// that the difference is the block length, not RNG noise. Not gated — synthetic.
#[test]
fn mc_r_sma_block_len_is_observable_and_changes_the_distribution() {
let dir = temp_cwd("mc_r_sma_block");
let run = |block_len: &str| {
let out = Command::new(BIN)
.args(["mc", "--strategy", "r-sma", "--resamples", "256", "--seed", "1", "--block-len", block_len])
.current_dir(&dir)
.output()
.expect("spawn aura mc --strategy r-sma --block-len");
assert!(out.status.success(), "exit: {:?} stderr: {}", out.status, String::from_utf8_lossy(&out.stderr));
String::from_utf8(out.stdout).expect("utf-8 stdout")
};
let one = run("1");
let three = run("3");
let v1: serde_json::Value = serde_json::from_str(one.trim()).expect("canonical JSON (block-len 1)");
let v3: serde_json::Value = serde_json::from_str(three.trim()).expect("canonical JSON (block-len 3)");
// the flag round-trips verbatim into the emitted line.
assert_eq!(v1["mc_r_bootstrap"]["block_len"], 1, "block_len 1 must echo: {one}");
assert_eq!(v3["mc_r_bootstrap"]["block_len"], 3, "block_len 3 must echo: {three}");
// same seed, same pooled series, different block length -> different E[R] spread.
// (a no-op `--block-len` would make these byte-identical.)
assert_ne!(one, three, "block-len must change the resampled distribution, not be ignored");
// C1 at the CLI edge: the block-len-1 line is byte-identical on re-run.
assert_eq!(one, run("1"), "same seed+block-len -> byte-identical line (C1)");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property: a `--block-len` at or above the pooled-series length is CLAMPED to the
/// series length at the binary boundary (no panic, no out-of-bounds), and the
/// degenerate single-block case collapses the bootstrap — when every resample IS
/// the full series, the E[R] distribution becomes a single point (all quantiles
/// equal). The emitted `block_len` reports the clamped value, not the raw flag.
/// This pins the clamp the engine primitive guards in-crate, now at the observable
/// CLI edge where an off-by-one in the index arithmetic would otherwise surface as
/// a process crash. Not gated — synthetic r-sma pools a fixed, non-empty series.
#[test]
fn mc_r_sma_oversized_block_len_clamps_and_collapses() {
let dir = temp_cwd("mc_r_sma_clamp");
let out = Command::new(BIN)
.args(["mc", "--strategy", "r-sma", "--resamples", "200", "--seed", "1", "--block-len", "9999"])
.current_dir(&dir)
.output()
.expect("spawn aura mc --strategy r-sma --block-len 9999");
assert!(out.status.success(), "must not crash on oversized block-len: {:?} stderr: {}",
out.status, String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("canonical JSON");
let obj = &v["mc_r_bootstrap"];
let n_trades = obj["n_trades"].as_u64().expect("n_trades is an integer");
assert!(n_trades >= 1, "synthetic r-sma must pool a non-empty R series: {stdout}");
// block_len is reported clamped to the pooled-series length, never the raw 9999.
assert_eq!(obj["block_len"], serde_json::json!(n_trades),
"oversized block-len must clamp to n_trades: {stdout}");
// single-block degenerate: every resample is the full series, so the E[R]
// distribution is a single point — p5 == p95 (zero spread).
let p5 = obj["e_r"]["p5"].as_f64().expect("p5 is a number");
let p95 = obj["e_r"]["p95"].as_f64().expect("p95 is a number");
assert!((p5 - p95).abs() < 1e-12, "single-block bootstrap must have zero spread: p5={p5} p95={p95}");
let _ = std::fs::remove_dir_all(&dir);
}
/// 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}");
}
/// 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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal.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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal.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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal.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!("{}/tests/fixtures/sma_signal_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!("{}/tests/fixtures/sma_signal_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));
}