Files
Aura/crates/aura-cli/tests/cli_run.rs
T
Brummel 4de6d5cbad rename: retire the stage1-* family for the r-family (r-sma / r-breakout / r-meanrev)
The stage1 ordinal has been dead vocabulary since the C10 reframe dropped
the two-stage research model: a 1 structurally implies a 2 that no longer
exists. The family is renamed by its live discriminator - the R yardstick -
with members named by their signal, uniform with their signal-named
siblings (sma, macd, momentum):

- selectors: stage1-r -> r-sma, stage1-breakout -> r-breakout,
  stage1-meanrev -> r-meanrev (old tokens are usage errors, exit 2 - no
  silent alias)
- identifiers: Strategy::RSma/RBreakout/RMeanRev, HarnessKind::RSma,
  r_sma_*/r_breakout_*/r_meanrev_*, wrap_r, run_signal_r, RGrid, R_SMA_*
- persisted identity: the sma_signal composite (param prefix
  sma_signal.fast.length / .slow.length; fixtures regenerated; content-ids
  shift - no test pins a literal hash, the registry parses no record names)
- e2e test files git-mv'd to r_sma_e2e / r_breakout_e2e / r_meanrev_e2e
- dead Stage-1/Stage-2 prose reworded to post-C10 vocabulary across
  rustdoc, Cargo.tomls, ledger live lines, and the glossary (historical
  entries stay; fieldtests corpus untouched)
- CLAUDE.md invariant 7 rewritten to record the ratified C10 reframe
  faithfully (the token-swap alone would have laundered the retired
  gated-currency/realistic-broker design into unmarked live prose); the
  unbacked account-mode clause dropped

Verification: cargo build/test --workspace green (51 targets), clippy
-D warnings clean, doc build clean, acceptance grep gate leaves exactly
the one resampling-stage false positive (harness.rs), smoke: --harness
r-sma runs, --harness stage1-r exits 2 with the new usage line.

Decision log: forks and rationale recorded on the issue (reconciliation
+ implementation-phase comments).

closes #174
2026-07-02 12:03:09 +02:00

4107 lines
203 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).
#[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("UnknownNodeType"), "names the cause: {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)");
}
/// 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: `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}");
}
/// 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");
assert_eq!(fams_out.lines().count(), 1, "families: {fams_out:?}");
assert!(fams_out.contains("\"family_id\":\"generalize-0\""), "families: {fams_out:?}");
assert!(fams_out.contains("\"kind\":\"CrossInstrument\""), "families: {fams_out:?}");
assert!(fams_out.contains("\"members\":2"), "one member per instrument: {fams_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, safety): a malformed blueprint is rejected at the dispatch boundary
/// (exit 2, UnknownNodeType) rather than panicking.
#[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("UnknownNodeType"), "stderr: {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, safety): `--list-axes` over a malformed blueprint rejects cleanly
/// (exit 2, naming UnknownNodeType) 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("UnknownNodeType"), "stderr was: {stderr}");
assert!(!stderr.contains("panicked"), "must reject cleanly, not panic: {stderr}");
}
/// 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));
}