Files
Aura/crates/aura-cli/tests/cli_run.rs
T
Brummel f6d80486cd fix(cli): gate dissolved verbs on project presence, matching campaign run (#218)
A dissolved verb (sweep/generalize/walkforward/mc --real) run outside a project
silently created a content-addressed runs/ store in the cwd and ran to
completion — Env::runs_root falls back to a relative ./runs with no project, and
the four dispatchers skipped the provenance gate that `campaign run` applies
first (campaign_run.rs:322-333). It is now refused up front: one gate in the
shared validate_and_register_axes chokepoint, after axis validation and before
the first store touch (put_blueprint), via a new AxisRegisterError::NoProject
mapped to exit 1 carrying "<verb> needs a project …".

Placement is load-bearing: axis validation (an UnknownAxis usage error, exit 2)
stays ahead of the project gate, so a malformed --axis still exits 2 regardless
of project — a malformed command is malformed in any environment and already
writes no store. Four per-verb pins assert the refusal (exit 1, "needs a
project", no store); the six exit-2 usage-refusal tests stay green.

Workspace suite green (62 groups / 0 failed), clippy -D warnings clean.

closes #218
2026-07-09 22:40:48 +02:00

4226 lines
207 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::path::{Path, PathBuf};
use std::process::Command;
use std::sync::{Mutex, MutexGuard, OnceLock};
/// 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
}
/// The demo-project fixture (Aura.toml + a built cdylib present), built once —
/// mirrors research_docs.rs's helper (a separate test binary, so it needs its
/// own `OnceLock`); `cargo build` is idempotent, so a second binary building
/// the same fixture is a no-op.
fn built_project() -> &'static PathBuf {
static BUILT: OnceLock<PathBuf> = OnceLock::new();
BUILT.get_or_init(|| {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/demo-project");
let out = std::process::Command::new("cargo")
.arg("build")
.current_dir(&dir)
.output()
.expect("spawn cargo build for the fixture project");
assert!(
out.status.success(),
"fixture build failed:\n{}",
String::from_utf8_lossy(&out.stderr)
);
dir
})
}
/// Serializes every test that runs a verb inside the shared fixture (they reset
/// `<fixture>/runs`, so parallel threads would race). Poison-tolerant.
fn project_lock() -> MutexGuard<'static, ()> {
static LOCK: Mutex<()> = Mutex::new(());
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
/// Removes `<fixture>/runs` on drop (including mid-panic) so a migrated test never
/// leaks its content-addressed store into the git-tracked fixture tree.
struct RunsCleanup(PathBuf);
impl Drop for RunsCleanup {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.0).ok();
}
}
/// Enter the shared built project fixture with a FRESH `runs/` store, serialized.
/// Returns the fixture dir (has an Aura.toml, so `provenance()` is `Some`) and the
/// guards; bind the guards for the whole test body: `let (dir, _g) = fresh_project();`.
/// The guard tuple orders `RunsCleanup` before the `MutexGuard` deliberately: tuple
/// fields drop in index order, so the `runs/` removal (index 0) runs while the lock
/// (index 1) is still held — the serialization the lock exists to enforce.
fn fresh_project() -> (&'static PathBuf, (RunsCleanup, MutexGuard<'static, ()>)) {
let lock = project_lock();
let dir = built_project();
let runs = dir.join("runs");
std::fs::remove_dir_all(&runs).ok();
(dir, (RunsCleanup(runs), lock))
}
/// #218: a well-formed dissolved verb run outside a project refuses up front
/// (exit 1, "needs a project") and leaves no store — parity with `campaign run`.
/// The refusal names the INVOKED verb, not a copy-pasted generic string — a
/// per-verb literal shared across four call sites (`validate_and_register_axes`
/// takes `verb` as a parameter) is exactly the kind of edit that silently
/// regresses to one hardcoded verb name; this pins that each call site still
/// threads its own name through.
#[test]
fn sweep_outside_project_refuses_and_leaves_no_store() {
let cwd = temp_cwd("sweep-outside-project-refuses");
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["sweep", &bp, "--real", "GER40", "--axis", "sma_signal.fast.length=2,4"])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(1), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(String::from_utf8_lossy(&out.stderr).contains("sweep needs a project"));
assert!(!cwd.join("runs").exists(), "a refused run must leave no store");
}
/// #218: same refusal as `sweep_outside_project_refuses_and_leaves_no_store`,
/// for `generalize`'s instrument-list form — the message names `generalize`,
/// not `sweep` (see that test's doc comment for the property).
#[test]
fn generalize_outside_project_refuses_and_leaves_no_store() {
let cwd = temp_cwd("generalize-outside-project-refuses");
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["generalize", &bp, "--real", "GER40,USDJPY", "--axis", "sma_signal.fast.length=2"])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(1), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(String::from_utf8_lossy(&out.stderr).contains("generalize needs a project"));
assert!(!cwd.join("runs").exists(), "a refused run must leave no store");
}
/// #218: same refusal as `sweep_outside_project_refuses_and_leaves_no_store`,
/// for `walkforward`'s valid `--axis` form — the message names `walkforward`,
/// not `sweep` (see that test's doc comment for the property).
#[test]
fn walkforward_outside_project_refuses_and_leaves_no_store() {
let cwd = temp_cwd("walkforward-outside-project-refuses");
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", &bp, "--real", "GER40", "--axis", "sma_signal.fast.length=2,4"])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(1), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(String::from_utf8_lossy(&out.stderr).contains("walkforward needs a project"));
assert!(!cwd.join("runs").exists(), "a refused run must leave no store");
}
/// #218: same refusal as `sweep_outside_project_refuses_and_leaves_no_store`,
/// for `mc`'s valid `--axis` form — the message names `mc`, not `sweep` (see
/// that test's doc comment for the property).
#[test]
fn mc_outside_project_refuses_and_leaves_no_store() {
let cwd = temp_cwd("mc-outside-project-refuses");
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["mc", &bp, "--real", "GER40", "--axis", "sma_signal.fast.length=2,4"])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(1), "stderr: {}", String::from_utf8_lossy(&out.stderr));
assert!(String::from_utf8_lossy(&out.stderr).contains("mc needs a project"));
assert!(!cwd.join("runs").exists(), "a refused run must leave no store");
}
#[test]
fn run_prints_json_and_exits_zero() {
// `run` is blueprint-only now (#159 cut 4 retired the bare built-in default);
// the shipped r-sma example is the deterministic reference blueprint.
let out = Command::new(BIN).args(["run", "examples/r_sma.json"]).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+risk-executor(pip_size=0.0001)\""), "got: {line}");
assert!(line.contains("\"window\":[1,18]"), "got: {line}");
assert!(line.contains("\"metrics\":{\"total_pips\":"), "got: {line}");
// the r-sma harness carries an R block (unlike the retired pip-only default).
assert!(line.contains("\"r\":{\"expectancy_r\":"), "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.
// `run` is blueprint-only now (#159 cut 4); the shipped r-sma example stands in.
let out = Command::new(BIN).args(["run", "examples/r_sma.json"]).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 — a valid `aura run <blueprint.json>`
/// still emits the JSON report on stdout and exits 0 — so the strict guard cannot
/// regress the happy path. (`run` is blueprint-only now, #159 cut 4: there is no
/// more bare/built-in default to preserve, so the positive case uses the shipped
/// r-sma example.)
#[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: a valid blueprint invocation still runs and prints the report.
let ok = Command::new(BIN)
.args(["run", "examples/r_sma.json"])
.output()
.expect("spawn aura run examples/r_sma.json");
assert_eq!(
ok.status.code(),
Some(0),
"a valid `aura run <blueprint.json>` 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\":"),
"a valid 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. `run` is blueprint-only now (#159 cut 4);
// the shipped r-sma example reaches the same pip-refusal gate in `run_data_from`.
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "examples/r_sma.json", "--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", "examples/r_sma.json", "--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", "examples/r_sma.json", "--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() {
// A bounded window (the same EURUSD 2024-06 calendar month the gated sweep tests
// use) keeps this fast; the full archive is unbounded and not needed for a
// pip-lookup property. `run` is blueprint-only now (#159 cut 4).
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"run", "examples/r_sma.json", "--real", "EURUSD",
"--from", &EURUSD_JUN2024_FROM_MS.to_string(),
"--to", &EURUSD_JUN2024_TO_MS.to_string(),
])
.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";
// `run` is blueprint-only now (#159 cut 4); the shipped r-sma example still
// routes through the same real-data pip lookup + manifest stamping.
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "examples/r_sma.json", "--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+risk-executor(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";
// `run` is blueprint-only now (#159 cut 4); the shipped r-sma example still
// routes through the same real-data pip lookup.
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "examples/r_sma.json", "--real", "USDJPY", "--from", FROM_MS, "--to", TO_MS])
.output()
.expect("spawn aura");
// Skip on a host without USDJPY geometry/data — either refusal is a clean skip,
// sourced from recorded geometry, never an authored-table miss.
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
if stderr.contains("no recorded geometry") || stderr.contains("no local data") {
eprintln!("skip: no local USDJPY geometry/data");
return;
}
}
assert_eq!(out.status.code(), Some(0), "USDJPY should resolve and run; exit: {:?}, stderr: {}",
out.status, String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(
stdout.contains("pip_size=0.01"),
"USDJPY must render the JPY sidecar pip (0.01) in the manifest, got: {}",
stdout.trim_end()
);
}
/// `aura run <blueprint.json>` loads a serialized signal blueprint and runs it
/// end-to-end (#165): exit 0, a RunReport whose manifest carries the topology_hash.
#[test]
fn aura_run_loads_and_runs_a_blueprint_file() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "examples/r_sma.json"])
.output()
.expect("spawn aura run blueprint");
assert_eq!(
out.status.code(),
Some(0),
"exit: {:?} stderr={}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
assert!(stdout.contains("\"topology_hash\":\""), "manifest carries the topology hash: {stdout}");
}
/// A blueprint referencing a node type outside the closed vocabulary fails clean
/// at load (the #160 closed-set guard at the data-plane face; invariant 9), and
/// (#184) the failure is phrased as house-style prose — matching the `graph
/// build` presenter's wording — never the raw `UnknownNodeType` Debug leak.
#[test]
fn aura_run_rejects_an_unknown_node_blueprint() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "tests/fixtures/unknown_node.json"])
.output()
.expect("spawn aura run unknown");
assert_ne!(out.status.code(), Some(0), "an unknown node type must fail the run");
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains(r#"unknown node type "Nope""#),
"house-style prose names the type: {stderr}"
);
assert!(
!stderr.contains("UnknownNodeType"),
"does not leak the Debug variant name: {stderr}"
);
}
/// Property (#175, refuse-don't-guess): built-in-only flags on `aura run
/// <blueprint.json>` are REFUSED, not silently dropped. The loaded-blueprint
/// grammar accepts only `--params`/`--seed`/`--real`/`--from`/`--to`; the removed
/// hand-parser rejected any other flag (exit 2). clap's optional `[blueprint]`
/// positional makes `--trace` structurally parseable (it stays on `RunCmd` for
/// the built-in-harness grammar), so the dispatch guard must re-reject it —
/// mirroring the sweep/mc blueprint branches, which reject their non-branch
/// flags exhaustively. A regression that dropped the guard would silently
/// no-op a user-provided flag. (The cost flags this test used to also cover
/// were removed from `RunCmd` entirely in #159's builtin retirement; clap now
/// rejects them as unknown args before dispatch ever sees them, so they no
/// longer exercise this guard.)
#[test]
fn run_blueprint_rejects_builtin_only_flags_exit_two() {
let args = ["run", "examples/r_sma.json", "--trace", "foo"];
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(args)
.output()
.expect("spawn aura run blueprint + builtin-only flag");
assert_eq!(
out.status.code(),
Some(2),
"{args:?} must be refused (exit 2), got {:?}; stderr: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
assert!(
out.stdout.is_empty(),
"{args:?} must not emit a report on stdout: {:?}",
String::from_utf8_lossy(&out.stdout)
);
}
// Note (#159 cut 4 collateral): `--trace` on a single `aura run`/`aura sweep`
// is no longer reachable from any CLI surface — the built-in default that
// accepted it retired with the last two PIP strategies, and the blueprint
// branches (`dispatch_run`, `run_blueprint_sweep`) never wire a trace-persist
// path. Repointing would require new production code (out of scope for a
// test-fixing task), so the single-run trace-persist + shape tests are
// deleted here rather than adjusted.
/// Property: an unknown `chart <name>` is a runtime failure pinned at BOTH the exit code
/// and the user-facing message — exit 1 (never a panic) AND the NotFound branch's
/// "no recorded run or family '<name>'" wording, which now covers families too. A
/// regression that drifted the message (e.g. back to the run-only phrasing) or
/// downgraded the exit would slip past the bundled exit-only check; this pins the
/// message/exit contract. Archive-local: charts into an empty cwd so the name is
/// genuinely absent.
#[test]
fn chart_unknown_name_refuses_with_exit_1_and_message() {
let cwd = temp_cwd("chart-unknown");
let out = Command::new(BIN).args(["chart", "ghost"]).current_dir(&cwd).output().expect("spawn chart ghost");
assert_eq!(out.status.code(), Some(1), "unknown name must exit 1: {:?}", out.status);
assert!(out.stdout.is_empty(), "the refusal must not leak a chart page to stdout, got: {:?}", String::from_utf8_lossy(&out.stdout));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no recorded run or family 'ghost'"),
"expected the unknown-name (run-or-family) refusal message, got: {stderr}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
#[test]
fn no_args_prints_usage_and_exits_two() {
let out = Command::new(BIN).output().expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "exit status: {:?}", out.status);
assert!(out.stdout.is_empty(), "stdout should be empty on the usage path");
let stderr = String::from_utf8(out.stderr).expect("utf-8 stderr");
assert!(stderr.contains("Usage"), "stderr was: {stderr:?}");
}
/// Property: `--help`/`-h` is the success-path help affordance, not the error
/// path. A newcomer's reflex first command (C22 first-contact ergonomics, #20)
/// prints the usage to stdout and exits 0; the conventional `-h` alias behaves
/// identically. An *unknown* subcommand keeps the error path (exit 2) so the
/// help fix does not regress bad-args handling.
#[test]
fn help_flag_prints_usage_to_stdout_and_exits_zero() {
for flag in ["--help", "-h"] {
let out = Command::new(BIN).arg(flag).output().expect("spawn aura --help");
assert_eq!(out.status.code(), Some(0), "`aura {flag}` exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(
!stdout.trim().is_empty(),
"`aura {flag}` should print help to stdout, got: {stdout:?}"
);
// The usage names the only subcommand a newcomer can run.
assert!(
stdout.contains("run"),
"`aura {flag}` help should mention the `run` subcommand, got: {stdout:?}"
);
}
// Negative-preservation: an unknown subcommand is still the error path.
let out = Command::new(BIN).arg("frobnicate").output().expect("spawn aura frobnicate");
assert_eq!(
out.status.code(),
Some(2),
"unknown subcommand must stay exit 2: {:?}",
out.status
);
}
/// Property (post-clap migration, #175): subcommand help is SCOPED — `aura <sub>
/// --help` (and `-h`) prints that subcommand's own options to stdout and exits 0,
/// with nothing on stderr, for every subcommand. This supersedes the retired #131
/// "uniform (byte-identical) help" surface: clap gives each subcommand its own
/// scoped Options section rather than one shared global usage blob.
#[test]
fn per_subcommand_help_is_scoped_stdout_exit_zero() {
for sub in ["run", "chart", "sweep", "walkforward", "mc", "graph", "runs"] {
for help in ["--help", "-h"] {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([sub, help]).output().expect("spawn");
assert_eq!(out.status.code(), Some(0), "{sub} {help} exit");
assert!(!out.stdout.is_empty(), "{sub} {help} stdout empty");
assert!(out.stderr.is_empty(), "{sub} {help} stderr not empty: {:?}",
String::from_utf8_lossy(&out.stderr));
}
}
}
/// Property: `aura graph` emits a single self-contained HTML page — the
/// interactive pin-graph viewer with the deterministic model inlined and the
/// vendored Graphviz-WASM + pan-zoom inlined (no remote fetch). Driven through
/// the built binary so it pins the observable CLI contract. The DOT/SVG are
/// Graphviz-version-dependent and not asserted (the prototype is the by-hand
/// visual reference); this pins the page envelope + the retirement of the old
/// ascii-dag notation.
#[test]
fn graph_emits_self_contained_html_viewer() {
let out = Command::new(BIN).arg("graph").output().expect("spawn aura graph");
assert_eq!(out.status.code(), Some(0), "`aura graph` exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
// a self-contained HTML document...
assert!(
stdout.trim_start().starts_with("<!doctype html>"),
"not an HTML doc: {:?}",
&stdout[..stdout.len().min(80)]
);
// ...with the deterministic model inlined as the viewer's data source...
assert!(stdout.contains("window.AURA_MODEL = {\"root\":"), "model not inlined: {stdout:?}");
// ...the Graphviz-WASM bootstrap present, and no remote asset fetch.
assert!(stdout.contains("Viz.instance"), "viewer bootstrap missing");
assert!(!stdout.contains("<script src"), "assets must be inlined, found remote <script src>");
// the retired ascii-dag invented notation is gone.
assert!(!stdout.contains("Sub(#Sf,#Ss)"), "retired ascii-dag notation must be gone: {stdout}");
}
#[test]
fn sweep_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);
}
#[test]
fn runs_families_list_and_per_family_rank_across_invocations() {
let cwd = temp_cwd("runs-flow");
// two blueprint 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. `sweep` is blueprint-only now (#159 cut 4); the shipped open r-sma
// example over a 2x2 axis grid stands in for the retired built-in default.
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
for _ in 0..2 {
let out = Command::new(BIN)
.args([
"sweep", &fixture,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
])
.current_dir(&cwd)
.output()
.expect("spawn sweep");
assert!(out.status.success(), "sweep exit: {:?}; stderr: {}", out.status, String::from_utf8_lossy(&out.stderr));
}
// `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);
}
// Note: the family-trace-persistence tests deleted above (mc/sweep/walkforward
// per-member trace dirs, the momentum bool-param dir/gate proofs, the sweep
// determinism pin) are the Step 3 PIP-surface deletions — same underlying
// cause as the collateral note above (no blueprint-mode arm persists
// per-member traces).
// --- Real-data family path (#106, plan 0060 Task 5) -------------------------
//
// These drive `aura sweep|walkforward --real <SYMBOL>` end to end over the local
// data-server archive. The gated tests need the Pepperstone M1 archive present;
// they skip (print + pass) on a data-less machine, the project's
// skip-on-no-data convention. The pip-refusal test is NOT gated: a symbol with no
// recorded geometry is rejected before any data access.
// EURUSD 2024-06 (UTC inclusive ms): a bounded recorded-sidecar real window.
const EURUSD_JUN2024_FROM_MS: i64 = 1_717_200_000_000;
const EURUSD_JUN2024_TO_MS: i64 = 1_719_791_999_999;
fn local_data_present() -> bool {
std::path::Path::new(data_server::DEFAULT_DATA_PATH).is_dir()
}
/// Property: the per-instrument pip refusal fires for `aura sweep --real
/// <no-geometry symbol>` exactly as it does for `aura run --real` — a symbol with no
/// recorded geometry (`NONEXISTENT`, no sidecar on any host) is rejected with exit 1
/// and the "no recorded geometry" message BEFORE any bar-data access. NOT gated: the
/// refusal precedes the archive entirely, so it is CI-safe and runs on every machine.
/// (#159 cut 4: `sweep` is blueprint-only now, so the fixture supplies a valid
/// blueprint + axis to clear the usage grammar before the geometry refusal fires.)
#[test]
fn sweep_real_no_geometry_symbol_refuses_with_exit_1() {
// not gated: the pip refusal happens before any data access.
let (dir, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(BIN)
.args(["sweep", &fixture, "--axis", "sma_signal.fast.length=2,4", "--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"));
}
// GER40 Sept-2024 window (inclusive Unix-ms) — the same gated calendar month
// used by `run_real_sidecar_index_pip_reaches_the_emitted_manifest` and the
// campaign-path e2e in `research_docs.rs`.
const GER40_SEPT2024_FROM_MS: &str = "1725148800000";
const GER40_SEPT2024_TO_MS: &str = "1727740799999";
/// Characterization pin (grounding-check ratification for the risk-regime-axis
/// spec): the dissolved real-data sweep member manifest stamps its resolved stop
/// (`stop_length`/`stop_k`) — the default regime here. The R-path stop knobs are
/// bound via `run_blueprint_member`'s `stop` param and stamped into the manifest
/// alongside the swept signal axes; the sibling
/// `sweep_real_blueprint_member_lines_pin_the_dissolved_contract` locates each
/// binding with `find` and so never pins the exact resolved VALUES this test
/// checks. Gated on the local GER40 archive; skips cleanly when absent.
#[test]
fn sweep_real_blueprint_member_manifest_stamps_the_default_stop() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let (dir, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8",
"--name", "brp_nostop",
])
.current_dir(dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2, "one member line per grid point: {stdout}");
for line in &lines {
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
let params = v["report"]["manifest"]["params"]
.as_array()
.expect("manifest.params is an array");
let get = |name: &str| params.iter().find(|p| p[0].as_str() == Some(name));
assert_eq!(get("stop_length").and_then(|p| p[1]["I64"].as_i64()), Some(3),
"the resolved default stop length is stamped: {line}");
assert_eq!(get("stop_k").and_then(|p| p[1]["F64"].as_f64()), Some(2.0),
"the resolved default stop k is stamped: {line}");
}
}
/// Property (#218 migration follow-up): running a dissolved `sweep` inside a
/// project (`fresh_project()`, the fixture this migration introduced) stamps
/// `report.manifest.project` on every member with the project's audit-trail
/// provenance (C18/C16) — the `demo` namespace and a well-formed 64-hex-char
/// dylib sha256 — rather than the `None` a bare-cwd run (no `Aura.toml`)
/// produces. Before this migration no cli_run.rs sweep test ran inside a real
/// project, so this stamping path was never exercised end-to-end. Gated on the
/// local GER40 archive; skips cleanly when absent.
#[test]
fn sweep_real_blueprint_member_manifest_stamps_project_provenance() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let (dir, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8",
"--name", "brp_provenance",
])
.current_dir(dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2, "one member line per grid point: {stdout}");
for line in &lines {
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
let project = &v["report"]["manifest"]["project"];
assert_eq!(
project["namespace"].as_str(), Some("demo"),
"member manifest carries the fixture project's namespace: {line}"
);
let sha = project["dylib_sha256"].as_str().unwrap_or_else(|| panic!("dylib_sha256 present: {line}"));
assert_eq!(sha.len(), 64, "dylib_sha256 is a full sha256 hex digest: {line}");
assert!(sha.chars().all(|c| c.is_ascii_hexdigit()), "dylib_sha256 is hex: {line}");
}
}
/// Property: the real-data blueprint sweep path (`aura sweep <blueprint.json>
/// --real SYM --axis …`, dispatched as sugar over the one campaign executor)
/// prints one member line per grid point, IN AXIS ORDER (odometer: the first
/// `--axis` varies slowest), each line JSON with a `family_id` and a `report`
/// whose `manifest.params` carries every swept param binding — and, the
/// sanctioned additive delta over the synthetic path, **`report.manifest`
/// carries a stamped `instrument` key** (the campaign substrate stamps every
/// member's manifest with the instrument under test, exactly like the sibling
/// `campaign_run_real_e2e_sweep_gate_walkforward` in `research_docs.rs`). Also
/// pins that the run persists exactly one new `FamilyKind::Sweep` family with
/// the grid's member count, and that the underlying generated process/campaign
/// documents and campaign-run record are durably auto-registered. Gated on the
/// local GER40 archive (the project's skip-on-no-data convention); skips
/// cleanly when absent.
#[test]
fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let (dir, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
"--name", "brp",
])
.current_dir(dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 4, "one member line per 2x2 grid point: {stdout}");
// Odometer order: `sma_signal.fast.length` (first `--axis`, outer/slowest)
// x `sma_signal.slow.length` (second `--axis`, inner) -> (2,8),(2,16),(4,8),(4,16).
let expected: [[(&str, i64); 2]; 4] = [
[("sma_signal.fast.length", 2), ("sma_signal.slow.length", 8)],
[("sma_signal.fast.length", 2), ("sma_signal.slow.length", 16)],
[("sma_signal.fast.length", 4), ("sma_signal.slow.length", 8)],
[("sma_signal.fast.length", 4), ("sma_signal.slow.length", 16)],
];
for i in 0..4 {
let line = lines[i];
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
assert!(v["family_id"].as_str().is_some(), "member line carries a family_id: {line}");
let manifest = &v["report"]["manifest"];
assert!(manifest.is_object(), "report.manifest is present: {line}");
let params = manifest["params"].as_array().expect("manifest.params is an array");
for (name, value) in expected[i] {
let bound = params
.iter()
.find(|p| p[0].as_str() == Some(name))
.and_then(|p| p[1]["I64"].as_i64());
assert_eq!(bound, Some(value), "manifest carries the swept binding {name}={value}: {line}");
}
// the sanctioned additive delta: the campaign substrate stamps every
// member's manifest with the instrument.
assert_eq!(
manifest["instrument"].as_str(),
Some("GER40"),
"the dissolved sweep stamps the instrument: {line}"
);
assert!(
params.iter().any(|p| p[0].as_str() == Some("stop_length"))
&& params.iter().any(|p| p[0].as_str() == Some("stop_k")),
"the dissolved sweep stamps the resolved stop: {line}"
);
}
// exactly one new Sweep family persisted, with the grid's four members.
let fams = std::process::Command::new(BIN)
.args(["runs", "families"])
.current_dir(dir)
.output()
.unwrap();
assert!(fams.status.success(), "families exit: {:?}", fams.status);
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
assert_eq!(fams_out.lines().count(), 1, "exactly one family persisted: {fams_out}");
assert!(fams_out.contains("\"kind\":\"Sweep\""), "families: {fams_out}");
assert!(fams_out.contains("\"members\":4"), "2x2 grid -> four members: {fams_out}");
// Auto-registration: generated documents are durable intent — exactly
// one process doc, one campaign doc, one campaign run record.
let count = |sub: &str| {
std::fs::read_dir(dir.join("runs").join(sub))
.map(|d| d.count())
.unwrap_or(0)
};
assert_eq!(count("processes"), 1, "one generated process document registered");
assert_eq!(count("campaigns"), 1, "one generated campaign document registered");
let runs_log = std::fs::read_to_string(dir.join("runs").join("campaign_runs.jsonl"))
.expect("campaign_runs.jsonl exists after a sugar run");
assert_eq!(runs_log.lines().count(), 1, "one campaign run recorded");
// `CampaignRunRecord` (aura-registry's lineage.rs)
// carries no name field at all (campaign id/process id/run/seed/cells/
// generalizations/trace_name only) — the `--name` handle is NOT part of
// the run-log line. It IS part of the generated CAMPAIGN DOCUMENT
// (`translate_sweep` sets `campaign.name = name`), so the handle is
// checked on that persisted document instead.
let campaigns_dir = dir.join("runs").join("campaigns");
let campaign_doc_path = std::fs::read_dir(&campaigns_dir)
.expect("campaigns dir exists")
.next()
.expect("exactly one campaign document")
.expect("readable dir entry")
.path();
let campaign_doc_json = std::fs::read_to_string(&campaign_doc_path).expect("read campaign doc");
assert!(
campaign_doc_json.contains("\"name\":\"brp\""),
"the --name handle lands on the generated campaign document: {campaign_doc_json}"
);
// Property (dispatch-rewire seam, docs/specs/sweep-dissolution.md Task 4
// Step 3): the CLI's `--axis` names are typed against the WRAPPED probe
// namespace (`sma_signal.fast.length`), but a campaign document's
// `strategies[].axes` speak the RAW `param_space` namespace
// (`fast.length`) — `bind_axes`'s convention (#203). `dispatch_sweep`
// strips the wrapper's one leading node segment before generating the
// document; a regression that stopped stripping (or stripped one segment
// too many/few) would persist the wrong axis keys and
// `validate_campaign_refs` would refuse every axis as unknown before any
// member ran — this pin catches that at the artifact level, directly.
let campaign_doc: serde_json::Value =
serde_json::from_str(&campaign_doc_json).expect("generated campaign doc parses as JSON");
let axes = campaign_doc["strategies"][0]["axes"]
.as_object()
.expect("strategies[0].axes is an object");
assert!(
axes.contains_key("fast.length"),
"the raw (unwrapped) axis name is the document key: {campaign_doc_json}"
);
assert!(
axes.contains_key("slow.length"),
"the raw (unwrapped) axis name is the document key: {campaign_doc_json}"
);
assert!(
!axes.contains_key("sma_signal.fast.length") && !axes.contains_key("sma_signal.slow.length"),
"the wrapped CLI probe name must never leak into the stored document: {campaign_doc_json}"
);
// Property (dispatch-rewire seam: the ms<->ns crossing, C3): the probed
// archive window is carried through `DataSource::full_window` (aura's
// native epoch-ns `Timestamp`, clamped to the archive's actual first/last
// bar inside the requested range) and converted back to Unix-ms via
// `aura_ingest::epoch_ns_to_unix_ms` exactly once before landing in the
// generated document's `data.windows[0]`. A regression that skipped the
// conversion (or applied it twice, or swapped ms<->ns) would persist a
// window off by a factor of ~1e6 from the requested `--from`/`--to` —
// silently, since the executor would still run (over the wrong or an
// empty range) rather than refuse outright. Pin the sane invariant that
// survives the archive's exact clamp: the stored window is a
// (from <= to) sub-range of the requested Unix-ms bounds, not a value
// orders of magnitude away.
let from_req = GER40_SEPT2024_FROM_MS.parse::<i64>().unwrap();
let to_req = GER40_SEPT2024_TO_MS.parse::<i64>().unwrap();
let window = &campaign_doc["data"]["windows"][0];
let from_stored = window["from_ms"].as_i64().expect("from_ms is an integer");
let to_stored = window["to_ms"].as_i64().expect("to_ms is an integer");
assert!(
from_req <= from_stored && from_stored <= to_stored && to_stored <= to_req,
"the stored window is a ms sub-range of the requested [{from_req}, {to_req}]: {campaign_doc_json}"
);
// Dedup: a second identical invocation reuses both documents (content-
// addressed) and appends a second run record.
let out2 = std::process::Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
"--name", "brp",
])
.current_dir(dir)
.output()
.unwrap();
assert!(out2.status.success(), "second run stderr: {}", String::from_utf8_lossy(&out2.stderr));
// `family_id` carries a per-execution uniqueness
// suffix ("the '-{run}' suffix is appended by the registry",
// aura-campaign/src/exec.rs `run_cell`) — a fresh family record is
// written on every execution even when the campaign/process documents
// themselves dedupe, so two identical invocations are NOT literally
// byte-identical on stdout: only the trailing run-suffix (0 -> 1)
// differs. C1 determinism is checked at the member-report level instead
// (every report field byte-identical), plus the run-suffix increment.
let out2_stdout = String::from_utf8_lossy(&out2.stdout).into_owned();
let lines1: Vec<&str> = stdout.lines().collect();
let lines2: Vec<&str> = out2_stdout.lines().collect();
assert_eq!(lines2.len(), lines1.len(), "same member count on the dedup run: {out2_stdout}");
for (l1, l2) in lines1.iter().zip(&lines2) {
let v1: serde_json::Value = serde_json::from_str(l1).expect("first run member line parses");
let v2: serde_json::Value = serde_json::from_str(l2).expect("second run member line parses");
assert_eq!(v1["report"], v2["report"], "identical invocations reproduce identical member reports (C1): {l1} vs {l2}");
let (fam1, fam2) = (v1["family_id"].as_str().unwrap(), v2["family_id"].as_str().unwrap());
assert_eq!(
fam1.trim_end_matches(|c: char| c.is_ascii_digit()),
fam2.trim_end_matches(|c: char| c.is_ascii_digit()),
"same family name up to the run-suffix: {fam1} vs {fam2}"
);
assert!(fam1.ends_with("-0") && fam2.ends_with("-1"), "run-suffix increments 0 -> 1: {fam1} vs {fam2}");
}
assert_eq!(count("processes"), 1, "second identical run dedupes the process doc");
assert_eq!(count("campaigns"), 1, "second identical run dedupes the campaign doc");
}
/// Property (#210 c0110 fieldtest, "the wrapped-name refusal mangles the axis
/// the user typed"): the dissolved real-data blueprint sweep types `--axis`
/// against the WRAPPED probe namespace (`sma_signal.fast.length`, what
/// `--list-axes` prints), not the raw campaign-document namespace
/// (`fast.length`). A user who types the raw form must be refused up front,
/// echoing exactly what they typed — never a stripped-then-mangled fragment
/// (the old failure mode: `fast.length` -> strip one segment -> the
/// nonsensical bare `"length"`). NOT gated: the preflight is data-free (it
/// only reloads the blueprint to probe its param space), so the refusal
/// fires before the archive is ever touched — this test needs no local data.
#[test]
fn aura_sweep_real_blueprint_refuses_a_raw_form_axis_name_before_data_access() {
let dir = temp_cwd("sweep_real_rawname_refusal");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "fast.length=2,4",
"--name", "c0110-rawname",
])
.current_dir(&dir)
.output()
.expect("spawn aura sweep (raw-form axis)");
assert_eq!(out.status.code(), Some(2), "a raw-form axis name must be refused, not run: status={:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(
stderr.contains("axis \"fast.length\""),
"the refusal echoes exactly the name the user typed: {stderr}"
);
assert!(
!stderr.contains("axis \"length\""),
"must never mangle the typed axis into a stripped fragment: {stderr}"
);
// Data-free: no archive/store access at all.
assert!(!dir.join("runs").exists(), "a refused axis name must not start a real run");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (#210 c0110 fieldtest, "refused dissolved sweeps still register
/// their generated campaign document" — store litter, probe P3): binding only
/// a SUBSET of the blueprint's open axes (here: `fast.length`, leaving
/// `slow.length` unbound) is a referential refusal at member-run time — but
/// the sugar must validate the generated documents (including the
/// axis-binding shape) BEFORE `register_generated`, so the refusal leaves
/// `runs/processes/` and `runs/campaigns/` untouched and appends no
/// `campaign_runs.jsonl` record. Gated on the local GER40 archive (the
/// referential refusal is reached only after the archive is probed for the
/// window, same as the sibling dissolved-sweep pin above).
#[test]
fn aura_sweep_real_blueprint_subset_axes_refuses_without_store_litter() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let (dir, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "sma_signal.fast.length=2,4",
"--name", "c0110-subset",
])
.current_dir(dir)
.output()
.expect("spawn aura sweep (subset axes)");
assert_ne!(
out.status.code(),
Some(0),
"leaving slow.length unbound must refuse, not run: stdout={}",
String::from_utf8_lossy(&out.stdout)
);
let count = |sub: &str| {
std::fs::read_dir(dir.join("runs").join(sub)).map(|d| d.count()).unwrap_or(0)
};
assert_eq!(count("processes"), 0, "a refused sweep must not register a generated process document");
assert_eq!(count("campaigns"), 0, "a refused sweep must not register a generated campaign document");
let runs_log = dir.join("runs").join("campaign_runs.jsonl");
assert!(
!runs_log.exists() || std::fs::read_to_string(&runs_log).unwrap().is_empty(),
"no realization recorded for a refused sweep"
);
}
/// 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 the blueprint positional + `--axis` campaign form (the
/// R-bootstrap over the pooled OOS R series,
/// `mc_r_bootstrap_real_e2e_pins_the_exact_current_grade`; spelled
/// `--strategy r-sma` before the #220 argv migration); a `--real` without a
/// blueprint 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 without a blueprint is a usage error");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("Usage: aura mc"), "usage refusal: {stderr}");
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (window-flags-require-`--real`, at the binary boundary): a family
/// command given a `--from`/`--to` window WITHOUT `--real` is a usage error —
/// there is no synthetic window knob, so `aura sweep --from 100` and
/// `aura walkforward --to 200` exit 2 (usage on stderr), never a silent synthetic
/// run over the whole built-in stream. The pure grammar is unit-tested in
/// `main.rs`; this pins that the `Err` arm actually wires through to `exit(2)` at
/// the dispatch for BOTH family commands. NOT gated — the refusal is pure
/// argument parsing, before any data access.
#[test]
fn family_window_flag_without_real_refuses_with_usage_exit_2() {
// (subcommand args, the subcommand token the usage message must name)
for (args, cmd) in [
(&["sweep", "--from", "100"][..], "sweep"),
(&["sweep", "--to", "200"][..], "sweep"),
(&["walkforward", "--from", "100"][..], "walkforward"),
(&["walkforward", "--to", "200"][..], "walkforward"),
] {
let dir = temp_cwd("family_window_no_real");
let out = Command::new(BIN)
.args(args)
.current_dir(&dir)
.output()
.unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
assert!(out.stdout.is_empty(), "`aura {args:?}` must emit no run on stdout: {:?}", out.stdout);
// the refusal carries the subcommand's usage, which names `--real` — the
// flag the window needs and was given without.
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains(cmd), "`aura {args:?}` stderr must name `{cmd}`: {stderr:?}");
assert!(stderr.contains("--real"), "`aura {args:?}` stderr must name `--real`: {stderr:?}");
let _ = std::fs::remove_dir_all(&dir);
}
}
/// Property (house style, #179): every hand-rolled usage line reads
/// `Usage: aura <verb> …` — capital `Usage:` plus the program name inside the
/// grammar. `sweep`/`walkforward`'s built-in-branch usage closures were bare
/// (`"sweep [--strategy …]"`, no prefix, no program name) before cycle 0101; a
/// regression that drops the prefix again would NOT be caught by the older
/// `family_window_flag_without_real_refuses_with_usage_exit_2` pin above, which
/// only checks for the verb token and `--real` — not the `Usage: aura` lead-in.
/// An unknown `--strategy` name reaches sweep's closure; the bare verb reaches
/// walkforward's.
#[test]
fn builtin_branch_usage_lines_lead_with_the_house_style_prefix() {
for (args, verb) in [(&["sweep", "--strategy", "bogus"][..], "sweep"), (&["walkforward"][..], "walkforward")] {
let dir = temp_cwd("builtin_usage_prefix");
let out = Command::new(BIN)
.args(args)
.current_dir(&dir)
.output()
.unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr);
let want = format!("aura: Usage: aura {verb} ");
assert!(stderr.starts_with(&want), "`aura {args:?}` stderr must lead with {want:?}: {stderr:?}");
let _ = std::fs::remove_dir_all(&dir);
}
}
/// Property (house style, #179): the built-in `mc` usage line carries TWO
/// `|`-separated grammar alternatives, and BOTH name the program (`aura mc …`),
/// not just the first. This is the one lockstep-pinned site
/// (`mc_rejects_real_flag_with_usage_exit_2` above), but that pin only checks
/// for a single `"Usage"` occurrence — a regression that fixed only the first
/// alternative (leaving the second `| aura mc <blueprint.json> --real …`
/// alternative unprefixed) would still pass it. `--seeds` with no blueprint
/// file is the built-in branch's own
/// grammar violation, reaching this exact literal. (#159 cut 4: the first
/// alternative's literal is now the blueprint-mode grammar, not a bare
/// `[--name` — `mc` is blueprint-first.)
#[test]
fn mc_builtin_usage_names_the_program_in_both_alternatives() {
let dir = temp_cwd("mc_usage_both_alts");
let out = Command::new(BIN)
.args(["mc", "--seeds", "3"])
.current_dir(&dir)
.output()
.expect("spawn aura mc --seeds");
assert_eq!(out.status.code(), Some(2), "mc --seeds without a blueprint must exit 2; status: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("Usage: aura mc <blueprint.json>"),
"first alternative must name the program: {stderr:?}"
);
assert!(
stderr.contains("| aura mc <blueprint.json> --real"),
"second alternative must ALSO name the program: {stderr:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
/// Property (house style, #179): `mc`'s loaded-blueprint branch usage line
/// also normalizes to `Usage: aura mc <blueprint.json> …` — before cycle 0101
/// it was fully lowercase and carried no program name (`"usage: mc
/// <blueprint.json> …"`). Reusing `sma_signal.json` (an existing closed
/// blueprint fixture) only to reach the dispatch branch; omitting `--seeds`
/// is this branch's own grammar violation.
#[test]
fn mc_over_a_blueprint_usage_names_the_program() {
let dir = temp_cwd("mc_blueprint_usage_prefix");
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args(["mc", &fixture])
.current_dir(&dir)
.output()
.expect("spawn aura mc <blueprint.json>");
assert_eq!(out.status.code(), Some(2), "mc <blueprint.json> without --seeds must exit 2; status: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("aura: Usage: aura mc <blueprint.json>"),
"stderr must lead with the house-style, program-named usage: {stderr:?}"
);
let _ = std::fs::remove_dir_all(&dir);
}
// Note (#159 cut 4 collateral): this removes the `aura chart <name>`
// CLI-integration tests (single-run and family overlay/tap-filter/meta, the
// cross-kind write-guard collision + same-kind-overwrite pair) — every one of
// them populated its `TraceStore` fixture via a bare `run --trace`/`sweep
// --trace` invocation, both retired. The only surviving `TraceStore::write`
// call site left in the whole crate is `campaign_run::persist_campaign_traces`
// (behind a campaign's `--persist-taps`) — an entirely different, much
// heavier fixture than any of these tests set up. Removed as PIP-retirement
// collateral, not
// repointed; the render/build logic they exercised stays covered by the
// hand-built-fixture unit tests (`build_chart_data_threads_run_manifest_into_meta`
// here, `render_chart_html_*` in render.rs), which never touch the CLI or disk.
// --- `aura run --harness <name>` selector (iter-3 Task 3) --------------------
/// Property: 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 (#159 cut 2): `--strategy r-breakout` is a fully retired CLI token, on
/// BOTH `sweep` and `walkforward` — indistinguishable from an unrecognized token
/// (`bogus`). `--strategy` is now a plain `Option<String>` with no per-token
/// dispatch, so a retired value carries nothing special: it flows into clap's
/// generic usage path and out as exit 2 with the generic `aura: Usage: aura
/// <verb> …` line, never a "valid-but-unsupported" message. A regression that
/// re-taught the CLI to recognize "r-breakout" specially (rather than leaving it
/// deleted) would make this test's stderr start naming "r-breakout" instead of
/// reading the generic usage line — this is the negative that catches a
/// half-finished retirement.
#[test]
fn sweep_and_walkforward_reject_retired_r_breakout_token_as_unrecognized() {
for (args, verb) in [
(&["sweep", "--strategy", "r-breakout"][..], "sweep"),
(&["walkforward", "--strategy", "r-breakout"][..], "walkforward"),
] {
let cwd = temp_cwd("retired-r-breakout-token");
let out = Command::new(BIN)
.args(args)
.current_dir(&cwd)
.output()
.unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr);
if verb == "sweep" {
let want = format!("aura: Usage: aura {verb} ");
assert!(
stderr.starts_with(&want),
"`aura {args:?}` must fall into the generic usage error, not a special-cased \
message: {stderr:?}"
);
} else {
assert!(
stderr.contains("unexpected argument '--strategy'"),
"`aura {args:?}` must be a clap unknown-argument rejection: {stderr:?}"
);
}
assert!(out.stdout.is_empty(), "no family summary on the rejected path: {:?}", out.stdout);
let _ = std::fs::remove_dir_all(&cwd);
}
}
/// Property (#159 cut 3): `--strategy r-meanrev` is a fully retired token on both
/// sweep and walkforward — the generic usage error, no special-casing.
#[test]
fn sweep_and_walkforward_reject_retired_r_meanrev_token_as_unrecognized() {
for (args, verb) in [
(&["sweep", "--strategy", "r-meanrev"][..], "sweep"),
(&["walkforward", "--strategy", "r-meanrev"][..], "walkforward"),
] {
let cwd = temp_cwd("retired-r-meanrev-token");
let out = Command::new(BIN).args(args).current_dir(&cwd).output()
.unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
assert_eq!(out.status.code(), Some(2), "`aura {args:?}` must exit 2; status: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr);
if verb == "sweep" {
let want = format!("aura: Usage: aura {verb} ");
assert!(stderr.starts_with(&want), "generic usage, not special-cased: {stderr:?}");
} else {
assert!(
stderr.contains("unexpected argument '--strategy'"),
"`aura {args:?}` must be a clap unknown-argument rejection: {stderr:?}"
);
}
assert!(out.stdout.is_empty(), "no summary on the rejected path: {:?}", out.stdout);
let _ = std::fs::remove_dir_all(&cwd);
}
}
/// Property (#159 cut 4): `--strategy sma` and `--strategy momentum` are fully
/// retired CLI tokens on both sweep and walkforward — the generic usage error,
/// no special-casing (the PIP built-in strategies retired with their runners).
#[test]
fn sweep_and_walkforward_reject_retired_pip_tokens_as_unrecognized() {
for tok in ["sma", "momentum"] {
for verb in ["sweep", "walkforward"] {
let args = [verb, "--strategy", tok];
let cwd = temp_cwd(&format!("retired-pip-{tok}-{verb}"));
let out = Command::new(BIN)
.args(args)
.current_dir(&cwd)
.output()
.unwrap_or_else(|e| panic!("spawn aura {args:?}: {e}"));
assert_eq!(
out.status.code(),
Some(2),
"`aura {args:?}` must exit 2; status: {:?}",
out.status
);
let stderr = String::from_utf8_lossy(&out.stderr);
if verb == "sweep" {
let want = format!("aura: Usage: aura {verb} ");
assert!(
stderr.starts_with(&want),
"`aura {args:?}` must fall into the generic usage error, not a \
special-cased message: {stderr:?}"
);
} else {
assert!(
stderr.contains("unexpected argument '--strategy'"),
"`aura {args:?}` must be a clap unknown-argument rejection: {stderr:?}"
);
}
assert!(out.stdout.is_empty(), "no summary on the rejected path: {:?}", out.stdout);
let _ = std::fs::remove_dir_all(&cwd);
}
}
}
/// Property (#159 cut 2): `--channel` is genuinely removed from the sweep grammar,
/// not merely unused — clap rejects it as a structurally unknown argument (exit 2)
/// rather than silently accepting and dropping it. A regression that left the flag
/// on `SweepCmd` while deleting only its plumbing would parse successfully here and
/// this test would fail on the exit code.
#[test]
fn sweep_channel_flag_is_removed_from_the_grammar() {
let cwd = temp_cwd("sweep-channel-flag-retired");
let out = Command::new(BIN)
.args(["sweep", "--strategy", "sma", "--channel", "3"])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --channel");
assert_eq!(out.status.code(), Some(2), "--channel must be an unrecognized clap argument: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--channel") || stderr.to_lowercase().contains("unexpected argument"),
"clap must name the unknown flag: {stderr:?}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#220 generalize vertical): `--strategy`/`--fast`/`--slow` are
/// genuinely removed from the generalize grammar, not merely unused — clap
/// rejects `--strategy` as a structurally unknown argument (exit 2) rather
/// than silently accepting and ignoring it now that the candidate is an
/// arbitrary blueprint positional + generic `--axis`. A regression that left
/// the old flags on `GeneralizeCmd` while deleting only their plumbing would
/// parse successfully here and this test would fail on the exit code.
#[test]
fn generalize_strategy_flag_is_removed_from_the_grammar() {
let cwd = temp_cwd("generalize-strategy-flag-retired");
let out = Command::new(BIN)
.args([
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
])
.current_dir(&cwd)
.output()
.expect("spawn aura generalize --strategy");
assert_eq!(out.status.code(), Some(2), "--strategy must be an unrecognized clap argument: {:?}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("--strategy") || stderr.to_lowercase().contains("unexpected argument"),
"clap must name the unknown flag: {stderr:?}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property: `aura generalize` grades a single r-sma candidate across two
/// instruments — its stdout carries the per-instrument breakdown + the worst-case
/// floor + the sign-agreement, then the persisted CrossInstrument family handle.
/// Gated on local GER40/USDJPY data (the shared Sept-2024 window), skips cleanly
/// when the archive is absent.
#[test]
fn generalize_grades_a_candidate_across_two_instruments() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(stdout.contains("\"generalize\":"), "aggregate object: {stdout}");
assert!(stdout.contains("\"n_instruments\":2"), "two instruments: {stdout}");
assert!(stdout.contains("\"worst_case\":"), "worst_case present: {stdout}");
assert!(stdout.contains("\"sign_agreement\":"), "sign_agreement present: {stdout}");
assert!(stdout.contains("GER40") && stdout.contains("USDJPY"), "per-instrument breakdown: {stdout}");
assert!(stdout.contains("\"family_id\":\"generalize-0\""), "family handle: {stdout}");
}
/// Characterization pin (byte-identity anchor for the generalize dissolution,
/// #210). The current `aura generalize` binds its protective stop as a grid axis
/// (`blueprint_sweep_family` with `stop_open=true`); the dissolution rebinds the same
/// stop through the risk-regime seam (`RiskRegime::Vol -> StopRule::Vol`). The
/// sibling `generalize_grades_a_candidate_across_two_instruments` asserts shape
/// only — `worst_case` and the per-instrument R floats are never checked — so a
/// stop-mechanism divergence between the two bindings would ship green. This pins
/// the EXACT current R grade of the identical invocation; after the dissolution
/// the same command must reproduce these bytes (the acceptance gate), and any
/// drift fails here loudly. Gated on the shared GER40/USDJPY Sept-2024 archive;
/// skips cleanly on a data refusal.
#[test]
fn generalize_real_e2e_pins_the_exact_current_grade() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let grade_line = stdout
.lines()
.find(|l| l.starts_with("{\"generalize\":"))
.unwrap_or_else(|| panic!("the generalize grade line: {stdout}"));
let v: serde_json::Value = serde_json::from_str(grade_line).expect("grade line parses as JSON");
let g = &v["generalize"];
assert_eq!(g["metric"].as_str(), Some("expectancy_r"), "metric: {grade_line}");
assert_eq!(g["n_instruments"].as_u64(), Some(2), "two instruments: {grade_line}");
assert_eq!(g["sign_agreement"].as_u64(), Some(2), "both agree in sign: {grade_line}");
// EXACT R floats — the byte-identity anchor the dissolution must preserve.
assert_eq!(g["worst_case"].as_f64(), Some(0.005795903617609842), "worst-case R floor: {grade_line}");
let per = g["per_instrument"].as_array().expect("per_instrument is an array");
assert_eq!(per.len(), 2, "two per-instrument grades: {grade_line}");
assert_eq!(per[0][0].as_str(), Some("GER40"), "first instrument: {grade_line}");
assert_eq!(per[0][1].as_f64(), Some(0.01056371324510624), "GER40 expectancy R: {grade_line}");
assert_eq!(per[1][0].as_str(), Some("USDJPY"), "second instrument: {grade_line}");
assert_eq!(per[1][1].as_f64(), Some(0.005795903617609842), "USDJPY expectancy R: {grade_line}");
}
/// Property (#220 generalize vertical): `aura generalize` grades ANY sweepable
/// blueprint, not just the historical r-sma candidate — the whole point of
/// dropping `--strategy r-sma`/`--fast`/`--slow` for a positional blueprint +
/// generic `--axis`. Every other generalize E2E test here still exercises
/// `r_sma_open.json`, so a regression that silently special-cased r-sma paths
/// back in (e.g. resolving axes against a hardcoded `sma_signal.*` namespace)
/// would ship green there while breaking every other blueprint. This runs the
/// unrelated r-breakout candidate (`channel_hi`/`channel_lo` axes, no
/// `sma_signal` node in sight) end-to-end and asserts a well-formed two-
/// instrument grade. Gated on the shared GER40/USDJPY Sept-2024 archive; skips
/// cleanly on a data refusal.
#[test]
fn generalize_grades_a_non_r_sma_blueprint() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", "r_breakout_signal.channel_hi.length=20",
"--axis", "r_breakout_signal.channel_lo.length=20",
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/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\":"), "family handle: {stdout}");
}
/// Characterization pin (byte-identity anchor for the walkforward dissolution,
/// #210). The current `aura walkforward` runs its per-window IS-refit inline
/// (`walkforward_family` -> `select_winner` -> OOS run -> stitch/pool); the
/// dissolution reroutes the same rolling IS-sweep -> OOS-run -> deflation through
/// the campaign `std::walk_forward` stage, and must retain the OOS pip curve the
/// campaign runner currently discards (`oos_equity: vec![]`) to reproduce
/// `stitched_total_pips`, plus reconstruct pooled `oos_r`. The multi-point grid
/// (fast 3,5 x slow 12,20) makes the per-window winner selection non-degenerate --
/// `param_stability` varies across windows -- so a winner-pick divergence between
/// the inline and campaign refit cannot ship green. This pins the EXACT current
/// grade of the identical invocation over a fixed 2025 GER40 window; after the
/// dissolution the same command must reproduce these bytes (the acceptance gate).
/// Gated on the shared GER40 archive; skips cleanly on a data refusal.
#[test]
fn walkforward_real_e2e_pins_the_exact_current_grade() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20",
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let grade_line = stdout
.lines()
.find(|l| l.starts_with("{\"walkforward\":"))
.unwrap_or_else(|| panic!("the walkforward summary line: {stdout}"));
let v: serde_json::Value = serde_json::from_str(grade_line).expect("summary line parses as JSON");
let wf = &v["walkforward"];
assert_eq!(wf["windows"].as_u64(), Some(9), "window count: {grade_line}");
// EXACT floats -- the byte-identity anchor the dissolution must preserve.
// stitched_total_pips is retained only if the campaign runner keeps the OOS curve.
assert_eq!(wf["stitched_total_pips"].as_f64(), Some(-10398606.666650848), "stitched OOS pips: {grade_line}");
let r = &wf["oos_r"];
assert_eq!(r["n_trades"].as_u64(), Some(20681), "pooled OOS trade count: {grade_line}");
assert_eq!(r["expectancy_r"].as_f64(), Some(-0.002397100685333715), "pooled OOS expectancy R: {grade_line}");
// param_stability varies across windows -> the per-window IS-refit picks
// different winners; pinning the means locks the winner-selection equivalence.
let ps = wf["param_stability"].as_array().expect("param_stability is an array");
assert_eq!(ps[0]["mean"].as_f64(), Some(3.888888888888889), "fast-MA refit mean: {grade_line}");
assert_eq!(ps[1]["mean"].as_f64(), Some(17.333333333333332), "slow-MA refit mean: {grade_line}");
}
/// Property (#220 walkforward vertical): `aura walkforward --real` IS-refits ANY
/// sweepable blueprint through the campaign path, not just the historical r-sma
/// candidate — the whole point of dropping `--strategy r-sma`/`--fast`/`--slow`
/// for a positional blueprint + generic `--axis`. Every other walkforward E2E
/// test here still exercises `r_sma_open.json`; a regression that silently
/// special-cased the refit's axis lookup back to a hardcoded `sma_signal.*`
/// namespace (e.g. in `walkforward_summary_json_from_reports`'s per-axis
/// `raw_matches_wrapped` search) would ship green there while breaking every
/// other blueprint. This runs the unrelated r-breakout candidate
/// (`channel_hi`/`channel_lo` axes, no `sma_signal` node in sight) end-to-end
/// and asserts a well-formed multi-window summary whose `param_stability` has
/// exactly one entry per bound axis (2 blueprint axes + the 2 stop columns).
/// The window count (9) is roller/data-derived, not strategy-derived, so it is
/// pinned exactly against the same GER40 2025 span the r-sma anchor uses.
/// Gated on the shared GER40 archive; skips cleanly on a data refusal.
#[test]
fn walkforward_dissolves_a_non_r_sma_blueprint() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "GER40",
"--axis", "r_breakout_signal.channel_hi.length=10,20",
"--axis", "r_breakout_signal.channel_lo.length=10,20",
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let grade_line = stdout
.lines()
.find(|l| l.starts_with("{\"walkforward\":"))
.unwrap_or_else(|| panic!("the walkforward summary line: {stdout}"));
let v: serde_json::Value = serde_json::from_str(grade_line).expect("summary line parses as JSON");
let wf = &v["walkforward"];
assert_eq!(wf["windows"].as_u64(), Some(9), "window count (data/roller-derived): {grade_line}");
assert!(wf["oos_r"]["n_trades"].as_u64().is_some(), "pooled OOS trade count present: {grade_line}");
let ps = wf["param_stability"].as_array().expect("param_stability is an array");
assert_eq!(ps.len(), 4, "one entry per bound axis (channel_hi, channel_lo, stop_length, stop_k): {grade_line}");
}
/// Fork A: the dissolved walkforward binds the stop as a single risk regime, so a
/// multi-value --stop-length/--stop-k is refused (exit 2) before any run.
#[test]
fn walkforward_dissolved_refuses_a_multi_value_stop() {
let cwd = temp_cwd("walkforward-multi-stop");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14,20", "--stop-k", "2.0",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "multi-value stop is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("single risk regime"), "stop-regime refusal: {stderr}");
}
/// Fork B: --select plateau is refused on the dissolved path (exit 2) with a
/// pointer to #215 until the wf-stage plateau relaxation ships.
#[test]
fn walkforward_dissolved_refuses_select_plateau() {
let cwd = temp_cwd("walkforward-plateau");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
"--select", "plateau:worst",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "plateau is refused on the dissolved path");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("#215"), "forward pointer to the plateau follow-up: {stderr}");
}
/// Property (#217): `walkforward_args_from` no longer requires both stop knobs
/// together on the campaign path — each defaults independently to the
/// single-sourced regime. Omitting only `--stop-k` while keeping `--stop-length`
/// explicit still succeeds, proving the defaulting is per-flag, not
/// all-or-nothing (the sibling
/// `walkforward_stopless_defaults_the_regime_byte_identically_to_explicit_3_2`
/// pins the both-omitted case). Gated on the shared GER40 archive; skips
/// cleanly on a data refusal.
#[test]
fn walkforward_dissolved_defaults_a_single_omitted_knob() {
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "3",
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(
out.status.code(),
Some(0),
"omitting only --stop-k must still succeed (it defaults independently): {}",
String::from_utf8_lossy(&out.stderr)
);
}
/// An empty `--real ""` still satisfies clap's `Option::is_some()` dispatch guard, so
/// `walkforward_args_from`'s own empty-string check is what refuses it (exit 2) —
/// distinct from omitting `--real` entirely, which never enters the dissolved branch.
#[test]
fn walkforward_dissolved_refuses_an_empty_real_symbol() {
let cwd = temp_cwd("walkforward-empty-real");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "empty --real is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("dissolves only over --real"), "empty-real refusal: {stderr}");
}
/// Property: an `--axis` name that is not among the loaded blueprint's sweepable
/// axes is refused before the raw-namespace strip or any archive access (exit 2),
/// echoing exactly the name the user typed — the same WRAPPED-probe preflight the
/// sibling verbs enforce
/// (`aura_sweep_real_blueprint_refuses_a_raw_form_axis_name_before_data_access`,
/// `generalize_refuses_an_unknown_axis_name`). Data-free: no archive/store access.
#[test]
fn walkforward_dissolved_refuses_an_unknown_axis_name() {
let cwd = temp_cwd("walkforward-unknown-axis");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "GER40",
"--axis", "sma_signal.nope=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "an unknown axis name must exit 2");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("axis \"sma_signal.nope\"") && stderr.contains("sweepable axes"),
"must name the unresolved axis exactly as typed: {stderr}"
);
assert!(
out.stdout.is_empty(),
"the refusal must not leak an aggregate to stdout, got: {:?}",
String::from_utf8_lossy(&out.stdout)
);
assert!(!cwd.join("runs").exists(), "a refused axis name must not start a real run");
}
/// Front-end parity: an unknown `--select` token is refused (exit 2) on the
/// dissolved path exactly as it is on the inline path — it must not be silently
/// swallowed by `select_is_plateau`'s plateau-only check and fall through to argmax.
#[test]
fn walkforward_dissolved_refuses_an_unknown_select_token() {
let cwd = temp_cwd("walkforward-bad-select");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
"--select", "bogus",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "unknown --select token is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("Usage: aura walkforward"), "unknown-select refusal: {stderr}");
}
/// `--trace`'s own help text ("mutually exclusive with --name") is a contract the
/// inline path already enforces via `name_persist`; the dissolved r-sma-real path
/// must refuse the same combination (exit 2) rather than silently letting --name
/// win over --trace.
#[test]
fn walkforward_dissolved_refuses_name_and_trace_together() {
let cwd = temp_cwd("walkforward-name-and-trace");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
"--name", "a", "--trace", "b",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "--name and --trace together is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("mutually exclusive"), "name/trace refusal: {stderr}");
}
/// Property (#210 T3, dispatch split): a successful real-data walkforward
/// (`aura walkforward <blueprint> --real --axis …`; spelled `--strategy r-sma
/// --real` before the #220 argv migration) durably auto-registers exactly one
/// generated process document, one
/// generated campaign document (carrying the `--name` handle and the stop as a
/// non-empty single risk regime), and one campaign-run record — mirroring the
/// generalize dissolution's audit trail. The persisted family set is exactly one
/// `Sweep` family (the pipeline's leading `std::sweep(argmax)` stage) plus one
/// `WalkForward` family (the per-window OOS reports) — the observable proof that
/// `dispatch_walkforward`'s real-data path runs through the one campaign
/// executor rather than the deleted inline roller. Gated on the shared GER40
/// archive; skips cleanly on a data refusal.
#[test]
fn walkforward_dissolves_through_the_campaign_path() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let count = |sub: &str| {
std::fs::read_dir(cwd.join("runs").join(sub))
.map(|d| d.count())
.unwrap_or(0)
};
assert_eq!(count("processes"), 1, "one generated process document registered");
assert_eq!(count("campaigns"), 1, "one generated campaign document registered");
let runs_log = std::fs::read_to_string(cwd.join("runs").join("campaign_runs.jsonl"))
.expect("campaign_runs.jsonl exists after a sugar run");
assert_eq!(runs_log.lines().count(), 1, "one campaign run recorded");
let campaigns_dir = cwd.join("runs").join("campaigns");
let campaign_doc_path = std::fs::read_dir(&campaigns_dir)
.expect("campaigns dir exists")
.next()
.expect("exactly one campaign document")
.expect("readable dir entry")
.path();
let campaign_doc_json = std::fs::read_to_string(&campaign_doc_path).expect("read campaign doc");
assert!(
campaign_doc_json.contains("\"name\":\"walkforward\""),
"the generated campaign document carries the --name handle: {campaign_doc_json}"
);
assert!(
campaign_doc_json.contains("\"risk\":[")
&& campaign_doc_json.contains("\"length\":14")
&& campaign_doc_json.contains("\"k\":2.0"),
"the stop rides a non-empty risk regime: {campaign_doc_json}"
);
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(cwd)
.output()
.expect("spawn families");
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
assert_eq!(
fams_out.lines().filter(|l| l.contains("\"kind\":\"Sweep\"")).count(),
1,
"one Sweep family from the pipeline's leading argmax stage: {fams_out}"
);
assert_eq!(
fams_out.lines().filter(|l| l.contains("\"kind\":\"WalkForward\"")).count(),
1,
"one WalkForward family holding the per-window OOS reports: {fams_out}"
);
}
/// Property (#217): on the dissolved `aura walkforward --real` path,
/// `--stop-length`/`--stop-k` are OPTIONAL — a stop-less invocation succeeds,
/// defaulting each omitted knob to the single-sourced default regime (length 3,
/// k 2.0), the very regime the campaign member runner already resolves for an
/// unbound regime (`stop_rule_for_regime`). The default is byte-identical to
/// spelling the knobs out: running the same blueprint in the same store first
/// stop-less, then with an explicit `--stop-length 3 --stop-k 2.0`, leaves
/// EXACTLY ONE generated campaign document — the two documents are byte-identical
/// so content-addressed storage collapses them onto one content id — while each
/// invocation still records its own campaign-run line. This aligns walkforward's
/// knob contract with sweep's (which already defaults the stop) without any
/// content-id churn on the explicit spelling. The sibling verbs mc/generalize gain
/// the same independent per-flag defaulting on the GREEN side. Gated on the shared
/// GER40 archive; skips cleanly on a data refusal. (The multi-value stop is still
/// refused — `walkforward_dissolved_refuses_a_multi_value_stop` — the stop stays a
/// single risk regime; only its *presence* becomes optional.)
#[test]
fn walkforward_stopless_defaults_the_regime_byte_identically_to_explicit_3_2() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
// Stop-less: the two knobs are omitted entirely. This is the headline — today it
// is refused at the argv boundary (exit 2, "requires --stop-length --stop-k");
// the feature makes the knobs optional so this run defaults the regime and runs.
let stopless = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if stopless.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&stopless.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(
stopless.status.code(),
Some(0),
"a stop-less walkforward must succeed (the stop knobs are optional, each defaulting \
to the single-sourced 3/2.0 regime): {}",
String::from_utf8_lossy(&stopless.stderr)
);
// Explicit 3/2.0: names the very defaults the stop-less run resolves. Same
// blueprint, same axes, same default `--name` ("walkforward"), same window.
let explicit = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "3", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura");
assert_eq!(
explicit.status.code(),
Some(0),
"the explicit --stop-length 3 --stop-k 2.0 walkforward must succeed: {}",
String::from_utf8_lossy(&explicit.stderr)
);
// Byte-identity: the default regime IS 3/2.0, so both invocations generate the
// same campaign document and content-addressed storage collapses them onto one
// content id — exactly one campaign document in the store.
let count = |sub: &str| {
std::fs::read_dir(cwd.join("runs").join(sub))
.map(|d| d.count())
.unwrap_or(0)
};
assert_eq!(
count("campaigns"),
1,
"the stop-less default binds the SAME 3/2.0 regime as the explicit run, so the two \
campaign documents are byte-identical and dedup to one content id"
);
let runs_log = std::fs::read_to_string(cwd.join("runs").join("campaign_runs.jsonl"))
.expect("campaign_runs.jsonl exists after two sugar runs");
assert_eq!(
runs_log.lines().count(),
2,
"each invocation still records its own campaign run (a run is an event, a document \
is content)"
);
}
/// Property: `aura generalize` is now thin sugar over the one campaign path — a
/// successful run durably auto-registers exactly one generated process document,
/// one generated campaign document (carrying the `--name` handle and the stop as
/// a non-empty single risk regime), and one campaign-run record. This is the
/// observable proof the inline path is gone and the dissolution runs through the
/// executor. Gated on the shared GER40/USDJPY Sept-2024 archive; skips cleanly on
/// a data refusal.
#[test]
fn generalize_dissolves_through_the_campaign_path() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let count = |sub: &str| {
std::fs::read_dir(cwd.join("runs").join(sub))
.map(|d| d.count())
.unwrap_or(0)
};
assert_eq!(count("processes"), 1, "one generated process document registered");
assert_eq!(count("campaigns"), 1, "one generated campaign document registered");
let runs_log = std::fs::read_to_string(cwd.join("runs").join("campaign_runs.jsonl"))
.expect("campaign_runs.jsonl exists after a sugar run");
assert_eq!(runs_log.lines().count(), 1, "one campaign run recorded");
let campaigns_dir = cwd.join("runs").join("campaigns");
let campaign_doc_path = std::fs::read_dir(&campaigns_dir)
.expect("campaigns dir exists")
.next()
.expect("exactly one campaign document")
.expect("readable dir entry")
.path();
let campaign_doc_json = std::fs::read_to_string(&campaign_doc_path).expect("read campaign doc");
assert!(
campaign_doc_json.contains("\"name\":\"generalize\""),
"the generated campaign document carries the --name handle: {campaign_doc_json}"
);
// The structural delta vs the sweep translator: the stop is a non-empty
// single risk regime, not empty risk.
assert!(
campaign_doc_json.contains("\"risk\":[")
&& campaign_doc_json.contains("\"length\":14")
&& campaign_doc_json.contains("\"k\":2.0"),
"the stop rides a non-empty risk regime: {campaign_doc_json}"
);
// The campaign path's family set (#210 Q4): exactly one CrossInstrument grade
// family (the generalize result, appended by the sugar) plus one
// per-instrument Sweep family per cell (persisted by the executor).
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(cwd)
.output()
.expect("spawn families");
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
assert_eq!(
fams_out.lines().filter(|l| l.contains("\"kind\":\"CrossInstrument\"")).count(),
1,
"one CrossInstrument grade family: {fams_out}"
);
assert_eq!(
fams_out.lines().filter(|l| l.contains("\"kind\":\"Sweep\"")).count(),
2,
"one per-instrument Sweep family per cell: {fams_out}"
);
}
/// Property (#210, dissolution — content-addressed idempotency): re-running the
/// IDENTICAL `aura generalize` invocation in the same project does not litter the
/// store with a second generated process/campaign document — `register_generated_g`
/// writes under a content-id filename, so two runs of the same candidate collapse
/// onto the same one process doc and one campaign doc, while each invocation still
/// records its OWN campaign-run line (a run is an event, a document is content).
/// Were the sugar to salt its generated documents with anything non-deterministic
/// (e.g. a timestamp), this would regress from 1 to 2 stored documents per repeat
/// invocation — the store-litter regression the campaign-path dissolution must not
/// introduce. Gated on the shared GER40/USDJPY Sept-2024 archive; skips cleanly on
/// a data refusal.
#[test]
fn generalize_repeated_identical_invocation_does_not_litter_the_store() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let invoke = || {
std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura")
};
let first = invoke();
if first.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&first.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
return;
}
assert_eq!(first.status.code(), Some(0), "first run exit: {:?}", first.status);
let second = invoke();
assert_eq!(second.status.code(), Some(0), "second run exit: {:?}", second.status);
let count = |sub: &str| {
std::fs::read_dir(cwd.join("runs").join(sub))
.map(|d| d.count())
.unwrap_or(0)
};
assert_eq!(count("processes"), 1, "two identical invocations share one process document");
assert_eq!(count("campaigns"), 1, "two identical invocations share one campaign document");
let runs_log = std::fs::read_to_string(cwd.join("runs").join("campaign_runs.jsonl"))
.expect("campaign_runs.jsonl exists");
assert_eq!(runs_log.lines().count(), 2, "each invocation still records its own campaign run");
}
/// Property (#210, dissolution — content-addressed distinctness): the idempotency
/// above is not a bug masquerading as a feature — two DIFFERENT `--fast`
/// invocations (differing candidate content) generate and persist TWO distinct
/// campaign documents (and two distinct process documents, since the `--metric`
/// text is unchanged so the process content matches — pinned via the campaign
/// count only, the axis differs on the campaign, not the process). Together with
/// `generalize_repeated_identical_invocation_does_not_litter_the_store`, this rules
/// out a store that always overwrites one fixed filename regardless of content —
/// the failure mode that would make the idempotency test above pass for the wrong
/// reason. Gated on the shared GER40/USDJPY Sept-2024 archive; skips cleanly on a
/// data refusal.
#[test]
fn generalize_distinct_invocations_persist_distinct_campaign_documents() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let invoke = |fast: &str| {
let fast_axis = format!("sma_signal.fast.length={fast}");
std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", &fast_axis, "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura")
};
let first = invoke("3");
if first.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&first.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
return;
}
assert_eq!(first.status.code(), Some(0), "first run exit: {:?}", first.status);
let second = invoke("4");
assert_eq!(second.status.code(), Some(0), "second run exit: {:?}", second.status);
let count = |sub: &str| {
std::fs::read_dir(cwd.join("runs").join(sub))
.map(|d| d.count())
.unwrap_or(0)
};
assert_eq!(count("campaigns"), 2, "two distinct candidates persist two distinct campaign documents");
}
/// Property (#210 T3, dissolution): omitting `--from`/`--to` still completes —
/// the dispatch rewrite's window-resolution fallback (`dispatch_generalize` in
/// main.rs) resolves ONE shared campaign window from the FIRST listed symbol's
/// full archive geometry and applies it to every instrument in the campaign
/// document (a `CampaignDoc` carries a single shared window, unlike the old
/// inline `run_generalize`, which let each instrument resolve its OWN
/// independent full window). A wrong index or an empty-`symbols` panic in that
/// new fallback would only surface when `--from`/`--to` are absent — every
/// other generalize e2e pins the explicit-window path, leaving this one
/// unexercised. Gated on local GER40/USDJPY data.
#[test]
fn generalize_without_explicit_window_falls_back_to_the_first_symbols_full_window() {
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40/USDJPY data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(stdout.contains("\"generalize\":"), "aggregate object: {stdout}");
assert!(stdout.contains("\"n_instruments\":2"), "two instruments graded: {stdout}");
assert!(stdout.contains("\"family_id\":\"generalize-0\""), "family handle: {stdout}");
}
/// 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 fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=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 fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=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 fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,GER40",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=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 multi-value axis is refused at the binary boundary (exit 2, no
/// run, data-free). A *candidate* is a single grid cell, not a sweep — so any
/// `--axis` carrying more than one value is refused, never silently widened
/// into a multi-cell run whose generalization grade would be ill-defined.
#[test]
fn generalize_refuses_a_multi_value_axis() {
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", "sma_signal.fast.length=2,3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
])
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "a multi-value axis must exit 2");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("each --axis takes exactly one value"), "arity refusal: {stderr}");
assert!(
out.stdout.is_empty(),
"the refusal must not leak an aggregate to stdout, got: {:?}",
String::from_utf8_lossy(&out.stdout)
);
}
/// Property: a `--axis` with no `--axis` flags at all is a usage error (exit 2,
/// no run) — generalize needs >= 1 axis to name a candidate, mirroring the sibling
/// sweep/walkforward "no axis" refusals. Data-free (fires before any archive access).
#[test]
fn generalize_refuses_no_axis() {
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--stop-length", "14", "--stop-k", "2.0",
])
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "zero axes must exit 2");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("Usage: aura generalize"), "must print the generalize usage: {stderr}");
assert!(
out.stdout.is_empty(),
"the refusal must not leak an aggregate to stdout, got: {:?}",
String::from_utf8_lossy(&out.stdout)
);
}
/// Property: an `--axis` name that is not among the loaded blueprint's sweepable
/// axes is refused before the raw-namespace strip or any archive access (exit 2),
/// echoing exactly the name the user typed — the same WRAPPED-probe preflight
/// `aura sweep` enforces (`aura_sweep_real_blueprint_refuses_a_raw_form_axis_name_before_data_access`).
#[test]
fn generalize_refuses_an_unknown_axis_name() {
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", "sma_signal.nope=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
])
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "an unknown axis name must exit 2");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("axis \"sma_signal.nope\"") && stderr.contains("sweepable axes"),
"must name the unresolved axis exactly as typed: {stderr}"
);
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, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=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);
// the run persisted the per-instrument members as a discoverable CrossInstrument family.
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(cwd)
.output()
.expect("spawn families");
assert!(fams.status.success(), "families exit: {:?}", fams.status);
let fams_out = String::from_utf8(fams.stdout).expect("utf-8");
// The dissolved path routes through the campaign executor, which persists a
// per-instrument Sweep family per cell (each instrument's candidate run, now
// a durable audit artifact) alongside the one CrossInstrument grade family
// the sugar appends. Pin exactly one CrossInstrument family (the generalize
// result) — not the total family count, which now includes those per-cell
// Sweep families.
let cross: Vec<&str> = fams_out
.lines()
.filter(|l| l.contains("\"kind\":\"CrossInstrument\""))
.collect();
assert_eq!(cross.len(), 1, "exactly one CrossInstrument family: {fams_out:?}");
assert!(cross[0].contains("\"family_id\":\"generalize-0\""), "families: {fams_out:?}");
assert!(cross[0].contains("\"members\":2"), "one member per instrument: {fams_out:?}");
}
/// Property (#210 T3, dissolution): the persisted `CrossInstrument` family's
/// members are attributed to the RIGHT instrument in the RIGHT cell order —
/// not merely correct in count. The dissolved path builds the family via
/// `cross_instrument_members`, which walks `outcome.cells` and trusts their
/// order/labelling, replacing the old `run_generalize`'s explicit
/// `zip(symbols, members)`. A cell-ordering or index bug in the new
/// extraction would still print the right aggregate (the campaign's own
/// `Generalize` stage computes that independently) while silently persisting
/// swapped or mislabelled members — a regression this test, not the exact-grade
/// anchor, would catch. Asserts `aura runs family generalize-0` lists GER40
/// first with the exact-grade anchor's GER40 `expectancy_r`, then USDJPY with
/// its own. Gated on the local GER40/USDJPY data (the shared Sept-2024 window).
#[test]
fn generalize_family_members_are_attributed_to_the_right_instrument() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=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 fam = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "family", "generalize-0"])
.current_dir(cwd)
.output()
.expect("spawn runs family");
assert!(fam.status.success(), "runs family exit: {:?}", fam.status);
let fam_out = String::from_utf8(fam.stdout).expect("utf-8");
let members: Vec<serde_json::Value> = fam_out
.lines()
.filter(|l| l.starts_with('{'))
.map(|l| serde_json::from_str(l).expect("member line parses as JSON"))
.collect();
assert_eq!(members.len(), 2, "one member per instrument: {fam_out:?}");
assert_eq!(
members[0]["manifest"]["instrument"].as_str(), Some("GER40"),
"first member is GER40: {fam_out:?}"
);
assert_eq!(
members[0]["metrics"]["r"]["expectancy_r"].as_f64(), Some(0.01056371324510624),
"GER40's own expectancy_r, matching the exact-grade anchor: {fam_out:?}"
);
assert_eq!(
members[1]["manifest"]["instrument"].as_str(), Some("USDJPY"),
"second member is USDJPY: {fam_out:?}"
);
assert_eq!(
members[1]["metrics"]["r"]["expectancy_r"].as_f64(), Some(0.005795903617609842),
"USDJPY's own expectancy_r, matching the exact-grade anchor: {fam_out:?}"
);
}
/// Property (#218 migration follow-up): `generalize`'s per-instrument members
/// route through the shared campaign path (`campaign_run.rs`'s
/// `run_blueprint_member` call, a code path distinct from sweep's in
/// `main.rs`), and that path ALSO stamps `manifest.project` with the running
/// project's provenance (C18/C16) for every persisted member — not just the
/// sweep chokepoint. Before the project-fixture migration this cli_run.rs
/// path only ever ran outside a project, so `project` was always `None` here.
/// Gated on the shared GER40/USDJPY Sept-2024 archive.
#[test]
fn generalize_family_members_stamp_project_provenance() {
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=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 fam = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "family", "generalize-0"])
.current_dir(cwd)
.output()
.expect("spawn runs family");
assert!(fam.status.success(), "runs family exit: {:?}", fam.status);
let fam_out = String::from_utf8(fam.stdout).expect("utf-8");
let members: Vec<serde_json::Value> = fam_out
.lines()
.filter(|l| l.starts_with('{'))
.map(|l| serde_json::from_str(l).expect("member line parses as JSON"))
.collect();
assert_eq!(members.len(), 2, "one member per instrument: {fam_out:?}");
for m in &members {
let project = &m["manifest"]["project"];
assert_eq!(
project["namespace"].as_str(), Some("demo"),
"campaign-path member manifest carries the fixture project's namespace: {fam_out:?}"
);
let sha = project["dylib_sha256"].as_str()
.unwrap_or_else(|| panic!("dylib_sha256 present: {fam_out:?}"));
assert_eq!(sha.len(), 64, "dylib_sha256 is a full sha256 hex digest: {fam_out:?}");
}
}
/// `aura sweep <blueprint.json> --axis <name>=<csv> …` (#166): sweeps a loaded OPEN
/// signal over its named param-space axes and persists a discoverable Sweep family.
/// Both open knobs (fast/slow) must be bound by axes; the 2×2 grid yields 4 members.
#[test]
fn aura_sweep_loads_a_blueprint_and_persists_a_sweep_family() {
let cwd = temp_cwd("blueprint-sweep");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args([
"sweep", &fixture,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
"--trace", "f",
])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep blueprint");
assert!(out.status.success(), "exit: {:?} stderr={}", out.status, String::from_utf8_lossy(&out.stderr));
let fams = Command::new(BIN).args(["runs", "families"]).current_dir(&cwd).output().expect("spawn families");
let fo = String::from_utf8(fams.stdout).expect("utf-8");
assert!(fo.contains("\"kind\":\"Sweep\""), "families: {fo}");
assert!(fo.contains("\"members\":4"), "2×2 grid -> four members: {fo}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#158/#164, C18/C11/C12): a blueprint sweep content-addresses its topology
/// — the canonical blueprint is persisted ONCE under `runs/blueprints/<topology_hash>.json`,
/// keyed by the very SHA256 every member's manifest carries, holding exactly the bytes
/// `blueprint_to_json(blueprint_from_json(doc))` yields. That stored topology is what makes
/// a generated sweep family reproducible from disk (the `aura reproduce` re-derivation).
#[test]
fn aura_sweep_persists_the_canonical_blueprint_keyed_by_topology_hash() {
use aura_engine::{blueprint_from_json, blueprint_to_json};
use aura_std::std_vocabulary;
use sha2::{Digest, Sha256};
let cwd = temp_cwd("blueprint-sweep-store");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args([
"sweep", &fixture,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
"--trace", "f",
])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep blueprint");
assert!(out.status.success(), "exit: {:?} stderr={}", out.status, String::from_utf8_lossy(&out.stderr));
// exactly one blueprint stored for the whole family (one topology per family).
let store = cwd.join("runs/blueprints");
let mut entries: Vec<_> = std::fs::read_dir(&store)
.unwrap_or_else(|e| panic!("the sweep must create the blueprint store {store:?}: {e}"))
.map(|e| e.expect("dir entry").path())
.collect();
entries.sort();
assert_eq!(entries.len(), 1, "one stored topology per family: {entries:?}");
// keyed by a 64-hex topology hash, holding the canonical re-serialization.
let stored_path = &entries[0];
let stem = stored_path.file_stem().expect("stem").to_str().expect("utf-8");
assert_eq!(stem.len(), 64, "content id is a 64-hex SHA256: {stem}");
assert!(stem.bytes().all(|b| b.is_ascii_hexdigit()), "content id is hex: {stem}");
let doc = std::fs::read_to_string(&fixture).expect("read fixture");
let expected = blueprint_to_json(&blueprint_from_json(&doc, &|t| std_vocabulary(t)).expect("loads"))
.expect("re-serializes");
let stored = std::fs::read_to_string(stored_path).expect("read stored blueprint");
assert_eq!(stored, expected, "the store holds the canonical blueprint bytes");
// content-addressing, the feature's load-bearing property: the key IS the SHA256 of
// the stored bytes — `get_blueprint(hash)` addresses exactly these bytes by identity,
// not by coincidence of the store and the members computing the hash separately.
let content_id: String =
Sha256::digest(stored.as_bytes()).iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(stem, content_id, "the store key is the SHA256 of its content");
// and that key is exactly what every family member's manifest carries — the hash the
// reproduce path reads back to fetch the topology. All 4 members share the one topology.
let members = std::fs::read_to_string(cwd.join("runs/families.jsonl")).expect("read family store");
let carried = members.matches(&format!("\"topology_hash\":\"{stem}\"")).count();
assert_eq!(carried, 4, "every member records the store key as its topology_hash: {members}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// An `--axis` name that does not resolve against the loaded blueprint's param_space
/// fails clean (a named BindError, non-zero exit), not a panic. The OPEN fixture has
/// sweepable knobs, so a nonsense axis name (`nope`) is genuinely unknown — distinct
/// from a fully-bound blueprint, which is refused earlier as "nothing to sweep".
#[test]
fn aura_sweep_rejects_an_unknown_axis() {
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args(["sweep", &fixture, "--axis", "nope=1,2", "--name", "f"])
.output()
.expect("spawn aura sweep unknown-axis");
assert_ne!(out.status.code(), Some(0), "an unknown axis must fail the sweep");
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("Knob") || stderr.to_lowercase().contains("nope"),
"names the unresolved axis: {stderr}"
);
}
/// Property (#158, C18 "re-derives full results on demand"): `aura reproduce <family-id>`
/// re-derives every member of a persisted sweep family from the content-addressed
/// blueprint store and reports each bit-identical, exiting 0. Drives the verb through the
/// binary — the `reproduce_family` stdout contract (a per-member `bit-identical` line plus
/// a `reproduced N/N` summary) that the `reproduce_family_in` unit seam bypasses.
#[test]
fn aura_reproduce_re_derives_a_persisted_sweep_family() {
let cwd = temp_cwd("blueprint-reproduce");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let sweep = Command::new(BIN)
.args([
"sweep", &fixture,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
"--trace", "f",
])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep blueprint");
assert!(sweep.status.success(), "sweep exit: {:?} stderr={}", sweep.status, String::from_utf8_lossy(&sweep.stderr));
// `--trace f` names the family `f-0`; reproduce it from disk.
let out = Command::new(BIN)
.args(["reproduce", "f-0"])
.current_dir(&cwd)
.output()
.expect("spawn aura reproduce");
assert!(out.status.success(), "reproduce exit: {:?} stderr={}", out.status, String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
// one verdict line per member (all bit-identical) + the summary; the label renders
// param values portably (`length=2`, not the `I64(2)` Debug form).
assert_eq!(
stdout.lines().filter(|l| l.contains("reproduced: bit-identical")).count(),
4,
"every member re-derives bit-identically: {stdout}"
);
assert!(stdout.contains("sma_signal.fast.length=2"), "label renders values portably: {stdout}");
assert!(!stdout.contains("I64("), "label must not leak the Scalar Debug form: {stdout}");
assert!(stdout.contains("reproduced 4/4 members bit-identically"), "summary line: {stdout}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (C1/C18 integrity net): when a persisted member's stored metrics no longer
/// match its re-derivation, `aura reproduce` reports that member `DIVERGED` and exits 1
/// — the non-happy branch of the verb. Forced by tampering one member's on-disk
/// `total_pips` after the sweep (the store's bit-identical compare IS the integrity check).
#[test]
fn aura_reproduce_reports_a_diverged_member_and_exits_one() {
let cwd = temp_cwd("blueprint-reproduce-diverge");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let sweep = Command::new(BIN)
.args([
"sweep", &fixture,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
"--trace", "f",
])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep blueprint");
assert!(sweep.status.success(), "sweep exit: {:?} stderr={}", sweep.status, String::from_utf8_lossy(&sweep.stderr));
// corrupt the first member's stored `total_pips` so its re-run metrics differ.
let store = cwd.join("runs/families.jsonl");
let s = std::fs::read_to_string(&store).expect("read family store");
let key = "\"total_pips\":";
let at = s.find(key).expect("a member records total_pips") + key.len();
let end = at + s[at..].find(',').expect("total_pips value terminates");
let corrupted = format!("{}999.0{}", &s[..at], &s[end..]);
std::fs::write(&store, corrupted).expect("write corrupted store");
let out = Command::new(BIN)
.args(["reproduce", "f-0"])
.current_dir(&cwd)
.output()
.expect("spawn aura reproduce");
assert_eq!(out.status.code(), Some(1), "a diverged member exits 1: stderr={}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(stdout.contains("reproduced: DIVERGED"), "the tampered member is reported DIVERGED: {stdout}");
assert!(stdout.contains("reproduced 3/4 members bit-identically"), "summary counts the diverged member: {stdout}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (acc 2): persist a blueprint sweep, then `aura reproduce <id>` re-derives every
/// member bit-identically from the content-addressed store and exits 0.
#[test]
fn aura_reproduce_re_derives_a_persisted_sweep_bit_identically() {
let cwd = temp_cwd("reproduce_roundtrip");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let sweep = std::process::Command::new(BIN)
.args([
"sweep",
&fixture,
"--axis",
"sma_signal.fast.length=2",
"--axis",
"sma_signal.slow.length=4,6",
"--name",
"smacross",
])
.current_dir(&cwd)
.output()
.expect("run aura sweep");
assert!(sweep.status.success(), "sweep ok: {}", String::from_utf8_lossy(&sweep.stderr));
let repro = std::process::Command::new(BIN)
.args(["reproduce", "smacross-0"])
.current_dir(&cwd)
.output()
.expect("run aura reproduce");
let stdout = String::from_utf8_lossy(&repro.stdout);
assert!(repro.status.success(), "reproduce exits 0: {}", String::from_utf8_lossy(&repro.stderr));
assert!(
stdout.contains("reproduced 2/2 members bit-identically"),
"every member re-derives: {stdout}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (acc 1+3+4): `aura mc <closed blueprint.json> --seeds N` builds a MonteCarlo
/// family (one member per seed), writes exactly one content-addressed blueprint, and
/// `aura reproduce <id>` re-derives every member bit-identically (exit 0).
#[test]
fn aura_mc_over_a_blueprint_reproduces_bit_identically() {
let cwd = temp_cwd("mc-blueprint-reproduce");
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let mc = Command::new(BIN)
.args(["mc", &fixture, "--seeds", "4", "--name", "mcx"])
.current_dir(&cwd)
.output()
.expect("spawn aura mc blueprint");
assert!(mc.status.success(), "mc exit: {:?} stderr={}", mc.status, String::from_utf8_lossy(&mc.stderr));
let mc_out = String::from_utf8(mc.stdout).expect("utf-8 stdout");
// one member line per seed (each carries "seed":n) + one aggregate line.
assert_eq!(
mc_out.lines().filter(|l| l.contains("\"seed\":")).count(),
4,
"one member line per seed: {mc_out}"
);
assert!(mc_out.contains("mc_aggregate"), "prints the aggregate line: {mc_out}");
// exactly one blueprint stored for the whole family (one topology per family).
let store = cwd.join("runs/blueprints");
let entries: Vec<_> = std::fs::read_dir(&store)
.unwrap_or_else(|e| panic!("mc must create the blueprint store {store:?}: {e}"))
.map(|e| e.expect("dir entry").path())
.collect();
assert_eq!(entries.len(), 1, "one stored topology per family: {entries:?}");
let repro = Command::new(BIN)
.args(["reproduce", "mcx-0"])
.current_dir(&cwd)
.output()
.expect("spawn aura reproduce");
assert!(repro.status.success(), "reproduce exit: {:?} stderr={}", repro.status, String::from_utf8_lossy(&repro.stderr));
let repro_out = String::from_utf8(repro.stdout).expect("utf-8 stdout");
assert!(
repro_out.contains("reproduced 4/4 members bit-identically"),
"every MC member re-derives: {repro_out}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (#173): an IS-refit walk-forward over a loaded blueprint persists a
/// content-addressed WalkForward family that `aura reproduce`s bit-identically —
/// each OOS member's windowed slice + winner params are recovered from its manifest.
#[test]
fn aura_walkforward_over_a_blueprint_reproduces_bit_identically() {
let cwd = temp_cwd("blueprint-walkforward-reproduce");
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let run = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", &bp, "--axis", "sma_signal.fast.length=2,3",
"--axis", "sma_signal.slow.length=4,6", "--name", "wfr"])
.current_dir(&cwd)
.output()
.expect("spawn aura walkforward");
assert_eq!(run.status.code(), Some(0), "stderr: {}", String::from_utf8_lossy(&run.stderr));
let repro = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["reproduce", "wfr-0"])
.current_dir(&cwd)
.output()
.expect("spawn aura reproduce");
assert_eq!(repro.status.code(), Some(0));
let out = String::from_utf8(repro.stdout).expect("utf-8");
assert!(out.contains("reproduced 3/3 members bit-identically"), "stdout: {out}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (#173): the loaded-blueprint walk-forward runs + persists a WalkForward
/// family (3 OOS members) with a {"walkforward":…} summary carrying oos_r, and
/// stores exactly one content-addressed blueprint.
#[test]
fn aura_walkforward_over_a_blueprint_persists_a_family() {
let cwd = temp_cwd("blueprint-walkforward-persist");
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", &bp, "--axis", "sma_signal.fast.length=2,3",
"--axis", "sma_signal.slow.length=4,6", "--name", "wfx"])
.current_dir(&cwd)
.output()
.expect("spawn aura walkforward");
assert_eq!(out.status.code(), Some(0), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8");
assert_eq!(stdout.matches("wfx-").count(), 3, "3 OOS member lines: {stdout}");
assert!(stdout.contains("\"walkforward\""), "summary line: {stdout}");
assert!(stdout.contains("\"oos_r\""), "R-measured summary: {stdout}");
let families = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"]).current_dir(&cwd).output().expect("spawn families");
let fo = String::from_utf8(families.stdout).expect("utf-8");
assert!(fo.contains("\"kind\":\"WalkForward\""), "families: {fo}");
let n = std::fs::read_dir(cwd.join("runs").join("blueprints")).map(|d| d.count()).unwrap_or(0);
assert_eq!(n, 1, "exactly one content-addressed blueprint stored");
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (#173): an OPEN blueprint with no --axis (nothing to re-fit) is a usage
/// error (exit 2), naming that >= 1 --axis is required.
#[test]
fn aura_walkforward_over_a_blueprint_rejects_no_axis() {
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", &bp])
.output()
.expect("spawn aura walkforward (no axis)");
assert_eq!(out.status.code(), Some(2));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(stderr.contains("requires >= 1 --axis"), "stderr: {stderr}");
}
/// E2E (#173/#184, safety): a malformed blueprint is rejected at the dispatch
/// boundary (exit 2) rather than panicking, phrased house-style (#184's
/// `blueprint_load_prose` convention) — never the raw `UnknownNodeType` Debug
/// leak (c0110 fieldtest finding, mirroring `aura_run_rejects_an_unknown_node_blueprint`).
#[test]
fn aura_walkforward_over_a_blueprint_rejects_malformed() {
let bp = format!("{}/tests/fixtures/unknown_node.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", &bp, "--axis", "sma_signal.fast.length=2,3"])
.output()
.expect("spawn aura walkforward (malformed)");
assert_eq!(out.status.code(), Some(2), "malformed must be rejected, not panic");
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(stderr.contains(r#"unknown node type "Nope""#), "house-style prose names the type: {stderr}");
assert!(!stderr.contains("UnknownNodeType"), "does not leak the Debug variant name: {stderr}");
assert!(!stderr.contains("panicked"), "must reject cleanly: {stderr}");
}
/// E2E (#173, panic-safety): a `--axis` naming a knob that is not in the loaded
/// blueprint's param_space fails clean (named BindError, exit 2, no panic). This
/// pins the in-closure fallible-bind path — `blueprint_sweep_over` returns a
/// `BindError` from inside the `walk_forward` window closure — which is a DISTINCT
/// branch from the dispatch-boundary blueprint-parse rejection (the malformed test):
/// the blueprint here is well-formed and passes dispatch, then the per-window
/// re-fit's bind fails. A regression to `.expect()` on that bind would panic here
/// while leaving the malformed test green.
#[test]
fn aura_walkforward_over_a_blueprint_rejects_an_unknown_axis() {
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", &bp, "--axis", "nope=1,2"])
.output()
.expect("spawn aura walkforward (unknown axis)");
assert_eq!(out.status.code(), Some(2), "an unknown axis must fail clean, not panic");
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(
stderr.contains("Knob") && stderr.contains("nope"),
"names the unresolved axis via BindError: {stderr}"
);
assert!(!stderr.contains("panicked"), "must reject cleanly, never panic: {stderr}");
}
/// E2E (#177): a rejected `--axis` on a loaded walk-forward blueprint is reported
/// EXACTLY ONCE, however many IS/OOS windows the roll would have spanned — mirroring
/// the sibling `aura sweep` / `aura mc` paths, which validate the axis ONCE up front
/// and emit their rejection a single time. The property this protects: axis
/// resolution is a single pre-flight, not a per-window re-raise.
///
/// The CLOSED fixture has an empty param_space, so ANY `--axis` name is an
/// `UnknownKnob`. The pre-fix code validates the axis INSIDE the per-window closure
/// that `walk_forward` fans out in parallel across the windows, so more than one
/// window `eprintln!`s the same rejection before the racing `exit(2)` tears the
/// process down — a race whose emit count is 1 or 2 per run. A single invocation
/// would therefore be a flaky assertion (it emits once ~1/3 of the time by luck), so
/// this drives the verb repeatedly: observing the double-emit at least once is then
/// near-certain, and the assertion is that EVERY run emits the rejection exactly
/// once. The fix (hoist axis resolution to one pre-flight before the windowed
/// fan-out) makes every run deterministically single.
#[test]
fn aura_walkforward_emits_an_unknown_axis_rejection_once() {
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
for attempt in 1..=20 {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", &bp, "--axis", "graph.fast.length=2,3"])
.output()
.expect("spawn aura walkforward (unknown axis)");
assert_eq!(out.status.code(), Some(2), "an unknown axis must fail clean, not panic");
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(!stderr.contains("panicked"), "must reject cleanly, never panic: {stderr}");
let emits = stderr.matches("UnknownKnob").count();
assert_eq!(
emits, 1,
"attempt {attempt}: the axis rejection must be emitted exactly once, got {emits}: {stderr:?}",
);
}
}
/// E2E (acc 2): `aura mc <open blueprint.json>` is rejected with a named error + exit 2
/// (NOT a panic / exit 101) — MC binds no axis, so a free knob has no binder.
#[test]
fn aura_mc_rejects_an_open_blueprint() {
let cwd = temp_cwd("mc-blueprint-open-reject");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args(["mc", &fixture, "--seeds", "4"])
.current_dir(&cwd)
.output()
.expect("spawn aura mc open-blueprint");
assert_eq!(out.status.code(), Some(2), "an open blueprint fails clean (exit 2, not a panic): stderr={}", String::from_utf8_lossy(&out.stderr));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(stderr.contains("closed blueprint"), "names the closed-blueprint requirement: {stderr}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (#176): `aura run <open blueprint.json>` refuses an open blueprint the same way the
/// sibling `aura mc` does — a clean named error + exit 2, NEVER a `compile_with_params`
/// arity panic (exit 101). The `run` dispatch bootstraps over the EMPTY point (a
/// closed-blueprint assumption), so a free knob must be rejected at the dispatch boundary
/// before that bootstrap, mirroring `blueprint_mc_family`'s closed-guard.
#[test]
fn aura_run_rejects_an_open_blueprint_without_panicking() {
let cwd = temp_cwd("run-blueprint-open-reject");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = Command::new(BIN)
.args(["run", &fixture])
.current_dir(&cwd)
.output()
.expect("spawn aura run open-blueprint");
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert_eq!(
out.status.code(),
Some(2),
"an open blueprint fails clean (exit 2, not a panic/exit 101): stderr={stderr}"
);
assert!(
stderr.contains("closed blueprint"),
"names the closed-blueprint requirement (mirroring `aura mc`): {stderr}"
);
assert!(
!stderr.contains("panicked"),
"must refuse at the dispatch boundary, never panic: {stderr}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (negative): `aura reproduce <unknown>` is a hard error (exit non-zero + named
/// cause on stderr), distinct from `runs family <id>`'s treat-as-empty exit 0.
#[test]
fn aura_reproduce_rejects_an_unknown_family() {
let cwd = temp_cwd("reproduce_unknown");
let out = std::process::Command::new(BIN)
.args(["reproduce", "no-such-family-1"])
.current_dir(&cwd)
.output()
.expect("run aura reproduce");
assert!(!out.status.success(), "unknown family exits non-zero");
assert!(
String::from_utf8_lossy(&out.stderr).contains("no such family 'no-such-family-1'"),
"names the cause: {}",
String::from_utf8_lossy(&out.stderr)
);
let _ = std::fs::remove_dir_all(&cwd);
}
// Note (#159 cut 4 collateral): this removes
// `aura_reproduce_rejects_a_non_generated_family` — its fixture was a
// HARD-WIRED (bare `sweep --name`) family with no `topology_hash`; sweep is
// blueprint-only now and every blueprint-sweep member stamps a
// `topology_hash`, so no surviving CLI surface can construct the fixture.
// The defensive "not a generated run" refusal in `main.rs` stays as dead-path
// safety for a malformed/legacy `families.jsonl`. Removed as collateral, not
// repointed.
/// E2E (refuse-don't-guess): `aura reproduce` when the content-addressed blueprint was
/// removed from the store exits non-zero, naming the missing blueprint — never re-runs
/// against a guessed topology (C10/C18).
#[test]
fn aura_reproduce_rejects_a_missing_stored_blueprint() {
let cwd = temp_cwd("reproduce_missing_store");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let sweep = std::process::Command::new(BIN)
.args([
"sweep",
&fixture,
"--axis",
"sma_signal.fast.length=2",
"--axis",
"sma_signal.slow.length=4",
"--name",
"bp",
])
.current_dir(&cwd)
.output()
.expect("run aura sweep");
assert!(sweep.status.success(), "blueprint sweep ok: {}", String::from_utf8_lossy(&sweep.stderr));
std::fs::remove_dir_all(cwd.join("runs").join("blueprints")).expect("remove the blueprint store");
let out = std::process::Command::new(BIN)
.args(["reproduce", "bp-0"])
.current_dir(&cwd)
.output()
.expect("run aura reproduce");
assert!(!out.status.success(), "a missing stored blueprint exits non-zero");
assert!(
String::from_utf8_lossy(&out.stderr).contains("missing from store"),
"names the cause: {}",
String::from_utf8_lossy(&out.stderr)
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// E2E (#169): `aura sweep <open blueprint.json> --list-axes` prints one
/// `<name>:<kind>` line per open sweepable knob, in `param_space()` order, and
/// exits 0. The names are prefixed by the wrapping (sma_signal.*) — exactly
/// the strings `--axis` binds — so a user can discover then sweep without guessing.
#[test]
fn aura_sweep_list_axes_prints_prefixed_names() {
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["sweep", &bp, "--list-axes"])
.output()
.expect("spawn aura sweep --list-axes");
assert_eq!(out.status.code(), Some(0));
let stdout = String::from_utf8(out.stdout).expect("utf-8");
assert_eq!(stdout, "sma_signal.fast.length:I64\nsma_signal.slow.length:I64\n");
}
/// E2E (#169): `--list-axes` on a CLOSED blueprint (all knobs bound) prints
/// nothing and exits 0 — an empty axis set is the honest answer, not an error.
#[test]
fn aura_sweep_list_axes_closed_is_empty() {
let bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["sweep", &bp, "--list-axes"])
.output()
.expect("spawn aura sweep --list-axes (closed)");
assert_eq!(out.status.code(), Some(0));
assert!(out.stdout.is_empty(), "a closed blueprint has no open axes");
}
/// E2E (#169): `--list-axes` is a standalone query — combining it with a real
/// sweep flag (`--axis`) is a usage error (exit 2), naming that it lists axes
/// and takes no other flags. A query cannot also seed a sweep.
#[test]
fn aura_sweep_list_axes_rejects_other_flags() {
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["sweep", &bp, "--list-axes", "--axis", "sma_signal.fast.length=2,3"])
.output()
.expect("spawn aura sweep --list-axes + --axis");
assert_eq!(out.status.code(), Some(2));
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(stderr.contains("--list-axes lists axes"), "stderr was: {stderr}");
}
/// E2E (#169/#184, safety): `--list-axes` over a malformed blueprint rejects
/// cleanly (exit 2), phrased house-style — never the raw `UnknownNodeType`
/// Debug leak (c0110 fieldtest finding) — rather than panicking; the axis
/// probe reloads the doc, so the dispatch-boundary validation must run first,
/// never a raw `.expect`.
#[test]
fn aura_sweep_list_axes_rejects_malformed_blueprint() {
let bp = format!("{}/tests/fixtures/unknown_node.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["sweep", &bp, "--list-axes"])
.output()
.expect("spawn aura sweep --list-axes (malformed)");
assert_eq!(out.status.code(), Some(2), "malformed blueprint must be rejected, not panic");
let stderr = String::from_utf8(out.stderr).expect("utf-8");
assert!(stderr.contains(r#"unknown node type "Nope""#), "house-style prose names the type: {stderr}");
assert!(!stderr.contains("UnknownNodeType"), "does not leak the Debug variant name: {stderr}");
assert!(!stderr.contains("panicked"), "must reject cleanly, not panic: {stderr}");
}
/// E2E (#210 c0110 fieldtest, "aura sweep <op-script> rejects the op-script
/// array with a raw serde error"): `aura sweep`'s blueprint slot accepts only
/// the built blueprint envelope; an un-built op-script (a JSON array) is
/// shape-discriminated and refused with a targeted, house-style hint — never
/// the leaked `Json(Error("invalid type: map, expected u32", ...))`.
#[test]
fn aura_sweep_rejects_an_op_script_array_with_a_build_first_hint() {
let dir = temp_cwd("sweep_rejects_op_script");
let op_script = dir.join("ops.json");
std::fs::write(&op_script, r#"[{"op":"source","role":"price","kind":"F64"}]"#)
.expect("write op-script fixture");
let out = Command::new(BIN)
.args(["sweep", op_script.to_str().unwrap(), "--list-axes"])
.output()
.expect("spawn aura sweep (op-script)");
assert_eq!(out.status.code(), Some(2), "an op-script array must be refused, not panic");
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(
stderr.contains("op-script") && stderr.contains("aura graph build"),
"names the op-script cause + the fix: {stderr}"
);
assert!(!stderr.contains("Json(Error"), "does not leak the raw serde error: {stderr}");
let _ = std::fs::remove_dir_all(&dir);
}
/// E2E (#210 c0110 fieldtest, house-style loader prose): a genuinely malformed
/// (syntactically invalid) JSON document in the sweep blueprint slot is
/// refused with `blueprint_load_prose`'s wording, never the raw `{e:?}` Debug
/// form the dispatch used to print.
#[test]
fn aura_sweep_rejects_malformed_json_house_style() {
let dir = temp_cwd("sweep_rejects_malformed_json");
let bad = dir.join("bad.json");
std::fs::write(&bad, "{not valid json").expect("write malformed fixture");
let out = Command::new(BIN)
.args(["sweep", bad.to_str().unwrap(), "--list-axes"])
.output()
.expect("spawn aura sweep (malformed json)");
assert_eq!(out.status.code(), Some(2), "malformed JSON must be refused, not panic");
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
assert!(
stderr.contains("blueprint document is not valid JSON"),
"house-style prose, not a raw Debug leak: {stderr}"
);
assert!(!stderr.contains("Json(Error"), "does not leak the raw serde Debug form: {stderr}");
let _ = std::fs::remove_dir_all(&dir);
}
/// E2E (#169, the load-bearing "listed == swept" invariant): a name emitted by
/// `--list-axes` is accepted VERBATIM by `--axis` — the discovered axis namespace
/// and the swept axis namespace are the same set (shared-probe construction). The
/// axis names here are DERIVED from the `--list-axes` output, never hardcoded, then
/// fed straight back into a real sweep: the sweep exits 0 and persists a `Sweep`
/// family, proving every listed name resolved (no UnknownKnob). This is the
/// discovery-removes-guessing property #169 exists to close — were the list and
/// sweep surfaces ever to diverge, a discovered name would be rejected here and this
/// round-trip would fail, whereas the literal-pinned per-surface tests could not see it.
#[test]
fn aura_sweep_list_axes_names_round_trip_into_a_working_sweep() {
let cwd = temp_cwd("blueprint-list-axes-roundtrip");
let bp = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
// 1) discover the axis names (do NOT hardcode them) — this is what a user reads.
let list = Command::new(BIN)
.args(["sweep", &bp, "--list-axes"])
.output()
.expect("spawn aura sweep --list-axes");
assert_eq!(list.status.code(), Some(0));
let listed = String::from_utf8(list.stdout).expect("utf-8");
let names: Vec<String> = listed
.lines()
.map(|l| l.split(':').next().expect("a `name:kind` line").to_string())
.collect();
assert!(!names.is_empty(), "the open fixture must list >= 1 axis");
// 2) feed the DISCOVERED names straight back into a real sweep. Values are
// position-assigned (first axis small, rest large) to stay in a non-degenerate
// SMA regime without re-hardcoding the names; each axis takes two values.
let mut args: Vec<String> = vec!["sweep".into(), bp.clone()];
for (i, name) in names.iter().enumerate() {
args.push("--axis".into());
args.push(format!("{name}={}", if i == 0 { "2,4" } else { "8,16" }));
}
let sweep = Command::new(BIN)
.args(&args)
.current_dir(&cwd)
.output()
.expect("spawn aura sweep over discovered axes");
assert!(
sweep.status.success(),
"every listed name must be a valid --axis; exit={:?} stderr={}",
sweep.status,
String::from_utf8_lossy(&sweep.stderr)
);
// 3) the sweep persisted a Sweep family whose grid spans exactly the discovered
// axes (each bound to two values -> 2^k members) — the names resolved end-to-end.
let fams = Command::new(BIN).args(["runs", "families"]).current_dir(&cwd).output().expect("spawn families");
let fo = String::from_utf8(fams.stdout).expect("utf-8");
assert!(fo.contains("\"kind\":\"Sweep\""), "families: {fo}");
let expected = 1usize << names.len(); // two values per discovered axis
assert!(fo.contains(&format!("\"members\":{expected}")), "grid over the discovered axes: {fo}");
let _ = std::fs::remove_dir_all(&cwd);
}
/// `--version` / `-V` surface the crate version on stdout and exit 0 (GNU MUST):
/// a version query is a successful query, not a usage error. The expected string
/// is the crate version itself (`CARGO_PKG_VERSION`, i.e. the workspace version),
/// so this stays honest across version bumps and keeps one source of truth.
#[test]
fn version_flag_prints_version_to_stdout_and_exits_zero() {
for flag in ["--version", "-V"] {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.arg(flag)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(0), "{flag}: exit {:?}", out.status);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains(env!("CARGO_PKG_VERSION")), "{flag} stdout: {stdout:?}");
}
}
/// Subcommand help is SCOPED: `aura sweep --help` names sweep's own flags and is
/// NOT byte-identical to `aura run --help` — each subcommand documents its own
/// options rather than reprinting one shared global blob (the #131 uniform-help
/// surface retired).
#[test]
fn subcommand_help_is_scoped_not_uniform() {
let sweep = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["sweep", "--help"]).output().expect("spawn");
let run = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--help"]).output().expect("spawn");
assert_eq!(sweep.status.code(), Some(0), "sweep --help exit");
assert_eq!(run.status.code(), Some(0), "run --help exit");
let sweep_out = String::from_utf8_lossy(&sweep.stdout);
let run_out = String::from_utf8_lossy(&run.stdout);
assert!(sweep_out.contains("--strategy"), "sweep help names its flags: {sweep_out:?}");
assert_ne!(sweep_out, run_out, "per-subcommand help must be scoped, not uniform");
}
/// The GNU `--flag=value` equals form is accepted, equivalent to the
/// space-separated `--flag value`. (#159 cut 4: `--harness` retired along with
/// the built-in default harness; `--seed` on a shipped closed blueprint is the
/// surviving vehicle flag for this clap-grammar property.)
#[test]
fn gnu_equals_form_is_accepted() {
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", &fixture, "--seed=0"]).output().expect("spawn");
assert_eq!(out.status.code(), Some(0),
"run --seed=0 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). (#159 cut 4: repointed off the retired
/// `--harness` onto `--seed` over a shipped closed blueprint.)
#[test]
fn double_dash_terminator_is_recognized() {
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", &fixture, "--seed", "0", "--"]).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 (`--se` → `--seed`), 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. (#159 cut 4:
/// repointed off the retired `--harness` onto `--seed` over a shipped closed
/// blueprint.)
#[test]
fn long_option_abbreviation_is_accepted() {
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", &fixture, "--se", "0"]).output().expect("spawn");
assert_eq!(out.status.code(), Some(0),
"run --se 0 (abbrev of --seed) 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", "--name", "x"][..],
&["walkforward", "bogus"][..],
&["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 refused with the same
/// usage error as a bare, blueprint-less `run` (exit 2), NOT read as a 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. (#159 cut 4: the built-in `--harness` branch retired, so the
/// no-blueprint arm now emits the loaded-blueprint usage line unconditionally; the
/// assertion is repointed onto that line, still distinct from a file-read error.)
/// 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("Usage: aura run <blueprint.json>"),
"the no-blueprint usage arm (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.
/// (#159 cut 4: `run` is blueprint-only now, so the runtime-failure arm needs a
/// shipped closed blueprint to clear the usage grammar before the no-geometry
/// refusal fires.)
#[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 fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let runtime = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", &fixture, "--real", "NONEXISTENT"]).output().expect("spawn");
assert_eq!(runtime.status.code(), Some(1),
"no data for a valid command is a runtime failure → 1; stderr: {}",
String::from_utf8_lossy(&runtime.stderr));
}
/// Characterization pin (byte-identity anchor for the mc R-bootstrap dissolution,
/// #210, the last verb; argv since migrated to the generic form by #220).
/// `aura mc <blueprint> --real --axis …` pools the per-window OOS trade-R
/// series and r-bootstraps E[R] through the campaign
/// `[std::sweep(argmax), std::walk_forward, std::monte_carlo]`
/// process, whose terminal monte_carlo stage does the
/// `StageBootstrap::PooledOos(r_bootstrap(...))` seeded from the campaign seed -- so
/// the campaign seed carries the mc `--seed`. The wf winners are
/// deflation-seed-independent (argmax), so the pooled series, and thus the
/// bootstrap, is stable across that seed remap (the walkforward anchor already
/// proved winner seed-independence). The multi-point grid makes the per-window
/// winner selection non-degenerate. The pinned bootstrap grade of this fixed 2025
/// GER40 invocation ORIGINATES from the pre-#210 inline rolling walk-forward
/// (`mc_r_bootstrap_report`) under the retired welded `aura mc --strategy r-sma
/// --real` grammar, and survived both the campaign dissolution and the #220 argv
/// migration byte-identically (the acceptance gate then, the regression anchor
/// now). Gated on the GER40 archive; skips
/// cleanly on a data refusal.
#[test]
fn mc_r_bootstrap_real_e2e_pins_the_exact_current_grade() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"mc", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20",
"--stop-length", "14", "--stop-k", "2.0",
"--block-len", "5", "--resamples", "1000", "--seed", "42",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let grade_line = stdout
.lines()
.find(|l| l.starts_with("{\"mc_r_bootstrap\":"))
.unwrap_or_else(|| panic!("the mc_r_bootstrap grade line: {stdout}"));
let v: serde_json::Value = serde_json::from_str(grade_line).expect("grade line parses as JSON");
let mc = &v["mc_r_bootstrap"];
assert_eq!(mc["block_len"].as_u64(), Some(5), "block_len: {grade_line}");
assert_eq!(mc["n_resamples"].as_u64(), Some(1000), "n_resamples: {grade_line}");
assert_eq!(mc["n_trades"].as_u64(), Some(20681), "pooled OOS trade count: {grade_line}");
// EXACT bootstrap floats -- the byte-identity anchor the dissolution must preserve.
assert_eq!(mc["e_r"]["mean"].as_f64(), Some(-0.0025753095301594307), "E[R] mean: {grade_line}");
assert_eq!(mc["e_r"]["p50"].as_f64(), Some(-0.0023049902245975227), "E[R] median: {grade_line}");
assert_eq!(mc["prob_le_zero"].as_f64(), Some(0.558), "P(E[R] <= 0): {grade_line}");
}
/// Property (#220 mc vertical): `aura mc --real` R-bootstraps ANY sweepable
/// blueprint through the campaign path, not just the historical r-sma candidate
/// — the whole point of dropping `--strategy r-sma`/`--fast`/`--slow` for a
/// positional blueprint + generic `--axis`. Every other mc E2E test here still
/// exercises `r_sma_open.json`; a regression that silently special-cased the
/// pipeline's axis lookup back to a hardcoded `sma_signal.*` namespace would
/// ship green there while breaking every other blueprint. This runs the
/// unrelated r-breakout candidate (`channel_hi`/`channel_lo` axes, no
/// `sma_signal` node in sight) end-to-end and asserts a well-formed
/// `mc_r_bootstrap` grade: the argv-echoed knobs (`block_len`/`n_resamples`)
/// pin exactly, `n_trades`/`e_r.mean`/`prob_le_zero` are asserted present and
/// well-typed (their exact values are strategy-specific, already the r-sma
/// pin's job above). Gated on the shared GER40 archive; skips cleanly on a
/// data refusal.
#[test]
fn mc_dissolves_a_non_r_sma_blueprint() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"mc", &fixture, "--real", "GER40",
"--axis", "r_breakout_signal.channel_hi.length=10,20",
"--axis", "r_breakout_signal.channel_lo.length=10,20",
"--stop-length", "14", "--stop-k", "2.0",
"--block-len", "5", "--resamples", "1000", "--seed", "42",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let grade_line = stdout
.lines()
.find(|l| l.starts_with("{\"mc_r_bootstrap\":"))
.unwrap_or_else(|| panic!("the mc_r_bootstrap grade line: {stdout}"));
let v: serde_json::Value = serde_json::from_str(grade_line).expect("grade line parses as JSON");
let mc = &v["mc_r_bootstrap"];
assert_eq!(mc["block_len"].as_u64(), Some(5), "block_len (argv echo): {grade_line}");
assert_eq!(mc["n_resamples"].as_u64(), Some(1000), "n_resamples (argv echo): {grade_line}");
assert!(mc["n_trades"].as_u64().is_some_and(|n| n > 0), "pooled OOS trade count present: {grade_line}");
assert!(mc["e_r"]["mean"].as_f64().is_some_and(|m| m.is_finite()), "E[R] mean present + finite: {grade_line}");
assert!(
mc["prob_le_zero"].as_f64().is_some_and(|p| (0.0..=1.0).contains(&p)),
"P(E[R] <= 0) present + in [0,1]: {grade_line}"
);
}
/// Property: real-data `aura mc <blueprint> --real --axis …` (the #210 dissolution
/// of the retired welded `--strategy r-sma` form, argv since migrated by #220) is
/// thin sugar over the one campaign
/// path — a successful run durably auto-registers exactly one generated process
/// document, one generated campaign document (carrying the constant "mc" name and the
/// stop as a non-empty single risk regime), and one campaign-run record, and emits the
/// single `mc_r_bootstrap` grade line. The pipeline's leading argmax sweep and its
/// walk_forward stage each persist one family; the terminal monte_carlo stage is an
/// annotator and adds NO family. This is the observable proof the inline path is gone
/// and the dissolution runs through the executor. Gated on the GER40 2025 archive;
/// skips cleanly on a data refusal.
#[test]
fn mc_dissolves_through_the_campaign_path() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"mc", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20",
"--stop-length", "14", "--stop-k", "2.0",
"--block-len", "5", "--resamples", "1000", "--seed", "42",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
// the single grade line is emitted (and nothing else on stdout).
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert_eq!(stdout.lines().count(), 1, "mc prints exactly one grade line: {stdout:?}");
assert!(
stdout.lines().next().is_some_and(|l| l.starts_with("{\"mc_r_bootstrap\":")),
"the one line is the mc_r_bootstrap grade: {stdout}"
);
let count = |sub: &str| {
std::fs::read_dir(cwd.join("runs").join(sub))
.map(|d| d.count())
.unwrap_or(0)
};
assert_eq!(count("processes"), 1, "one generated process document registered");
assert_eq!(count("campaigns"), 1, "one generated campaign document registered");
let runs_log = std::fs::read_to_string(cwd.join("runs").join("campaign_runs.jsonl"))
.expect("campaign_runs.jsonl exists after a sugar run");
assert_eq!(runs_log.lines().count(), 1, "one campaign run recorded");
let campaigns_dir = cwd.join("runs").join("campaigns");
let campaign_doc_path = std::fs::read_dir(&campaigns_dir)
.expect("campaigns dir exists")
.next()
.expect("exactly one campaign document")
.expect("readable dir entry")
.path();
let campaign_doc_json = std::fs::read_to_string(&campaign_doc_path).expect("read campaign doc");
assert!(
campaign_doc_json.contains("\"name\":\"mc\""),
"the generated campaign document carries the constant mc name: {campaign_doc_json}"
);
assert!(
campaign_doc_json.contains("\"risk\":[")
&& campaign_doc_json.contains("\"length\":14")
&& campaign_doc_json.contains("\"k\":2.0"),
"the stop rides a non-empty risk regime: {campaign_doc_json}"
);
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["runs", "families"])
.current_dir(cwd)
.output()
.expect("spawn families");
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
assert_eq!(
fams_out.lines().filter(|l| l.contains("\"kind\":\"Sweep\"")).count(),
1,
"one Sweep family from the pipeline's leading argmax stage: {fams_out}"
);
assert_eq!(
fams_out.lines().filter(|l| l.contains("\"kind\":\"WalkForward\"")).count(),
1,
"one WalkForward family holding the per-window OOS reports: {fams_out}"
);
assert_eq!(
fams_out.lines().filter(|l| l.contains("\"kind\":\"MonteCarlo\"")).count(),
0,
"the terminal monte_carlo stage is an annotator, not a family: {fams_out}"
);
}
/// Fork A: the stop is a single risk regime on the dissolved mc path — a multi-value
/// `--stop-length`/`--stop-k` is a usage refusal (exit 2), not a swept axis. Mirrors
/// `walkforward_dissolved_refuses_a_multi_value_stop`. NOT gated — the refusal is pure
/// argument parsing, before any data access.
#[test]
fn mc_dissolved_refuses_a_multi_value_stop() {
let cwd = temp_cwd("mc-multi-stop");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"mc", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14,20", "--stop-k", "2.0",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "multi-value stop is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("single risk regime"), "stop-regime refusal: {stderr}");
}
/// Property: an `--axis` name that is not among the loaded blueprint's sweepable
/// axes is refused before the raw-namespace strip or any archive access (exit 2),
/// echoing exactly the name the user typed — mc shares the identical WRAPPED-probe
/// preflight `walkforward_dissolved_refuses_an_unknown_axis_name` pins for
/// walkforward (both dispatchers now call the same `validate_and_register_axes`
/// helper); this test pins mc's own copy so a future edit cannot silently break it
/// while walkforward's stays green. Data-free: no archive/store access.
#[test]
fn mc_dissolved_refuses_an_unknown_axis_name() {
let cwd = temp_cwd("mc-unknown-axis");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"mc", &fixture, "--real", "GER40",
"--axis", "sma_signal.nope=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "an unknown axis name must exit 2");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("axis \"sma_signal.nope\"") && stderr.contains("sweepable axes"),
"must name the unresolved axis exactly as typed: {stderr}"
);
assert!(
out.stdout.is_empty(),
"the refusal must not leak an aggregate to stdout, got: {:?}",
String::from_utf8_lossy(&out.stdout)
);
assert!(!cwd.join("runs").exists(), "a refused axis name must not start a real run");
}
/// Property: the one load-bearing decision new to the mc dissolution — `translate_mc`
/// maps `campaign.seed` to the mc `--seed` (unlike `translate_walkforward`, which
/// hardcodes `seed: 0`) — actually flows through the executor to the emitted grade,
/// not just into the generated document's content id (already unit-pinned by
/// `translate_mc_diverging_seeds_and_stops_do_not_collide_content_ids`). Two runs that
/// differ ONLY in `--seed` must emit a different `e_r` distribution (the bootstrap
/// resample draw is seeded) while pooling the IDENTICAL `n_trades` (the wf winners are
/// argmax-selected, hence deflation-seed-independent — the walkforward anchor already
/// proved this for the shared roller): the seed perturbs the resample, never the
/// underlying OOS trade-R series. Without this test, a dropped seed remap (e.g. mc
/// always bootstrapping at a fixed internal seed) would still pass every other mc
/// dissolution test, since none of them compare two distinct `--seed` runs. Gated on
/// the GER40 2025 archive; skips cleanly on a data refusal.
#[test]
fn mc_dissolved_seed_changes_the_bootstrap_draw_not_the_pooled_series() {
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let run = |seed: &str| {
std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"mc", &fixture, "--real", "GER40",
"--axis", "sma_signal.fast.length=3,5", "--axis", "sma_signal.slow.length=12,20",
"--stop-length", "14", "--stop-k", "2.0",
"--block-len", "5", "--resamples", "1000", "--seed", seed,
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura")
};
let out42 = run("42");
if out42.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out42.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
let out7 = run("7");
assert_eq!(out42.status.code(), Some(0), "exit (seed 42): {:?}", out42.status);
assert_eq!(out7.status.code(), Some(0), "exit (seed 7): {:?}", out7.status);
let parse = |out: &std::process::Output| -> serde_json::Value {
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let line = stdout
.lines()
.find(|l| l.starts_with("{\"mc_r_bootstrap\":"))
.unwrap_or_else(|| panic!("the mc_r_bootstrap grade line: {stdout}"))
.to_string();
serde_json::from_str(&line).expect("grade line parses as JSON")
};
let v42 = parse(&out42);
let v7 = parse(&out7);
assert_eq!(
v42["mc_r_bootstrap"]["n_trades"], v7["mc_r_bootstrap"]["n_trades"],
"the pooled OOS trade-R series is seed-independent (argmax winners): {v42} vs {v7}"
);
assert_ne!(
v42["mc_r_bootstrap"]["e_r"]["mean"], v7["mc_r_bootstrap"]["e_r"]["mean"],
"a different --seed must move the resampled E[R] mean: {v42} vs {v7}"
);
}
/// #220 weld-gone proof: the walkforward campaign path runs an arbitrary user
/// blueprint with axis names the CLI has never heard of — the r-sma weld
/// (hardcoded fast/slow) is structurally gone. Gated on the GER40 archive;
/// skips cleanly when absent.
#[test]
fn walkforward_campaign_runs_an_arbitrary_blueprint() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "GER40",
"--axis", "r_breakout_signal.channel_hi.length=20,40",
"--axis", "r_breakout_signal.channel_lo.length=10,20",
"--stop-length", "3", "--stop-k", "2.0",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(
out.status.code(),
Some(0),
"exit: {:?} stderr: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let grade_line = stdout
.lines()
.find(|l| l.starts_with("{\"walkforward\":"))
.unwrap_or_else(|| panic!("the walkforward summary line: {stdout}"));
let v: serde_json::Value = serde_json::from_str(grade_line).expect("summary line parses as JSON");
let wf = &v["walkforward"];
assert!(wf["windows"].as_u64().is_some_and(|w| w >= 1), "at least one window: {grade_line}");
// param_stability rows: channel_hi, channel_lo, stop_length, stop_k.
let ps = wf["param_stability"].as_array().expect("param_stability is an array");
assert_eq!(ps.len(), 4, "two channel axes + the stop pair: {grade_line}");
}
/// #220 weld-gone proof for mc: the R-bootstrap pipeline runs the same
/// arbitrary breakout blueprint. Gated on the GER40 archive.
#[test]
fn mc_campaign_runs_an_arbitrary_blueprint() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
const FROM_MS: &str = "1735689600000";
const TO_MS: &str = "1767225599000";
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"mc", &fixture, "--real", "GER40",
"--axis", "r_breakout_signal.channel_hi.length=20,40",
"--axis", "r_breakout_signal.channel_lo.length=10,20",
"--stop-length", "3", "--stop-k", "2.0",
"--block-len", "5", "--resamples", "100", "--seed", "7",
"--from", FROM_MS, "--to", TO_MS,
])
.current_dir(cwd)
.output()
.expect("spawn aura");
if out.status.code() == Some(1) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
"exit 1 must be a data refusal, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(
out.status.code(),
Some(0),
"exit: {:?} stderr: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let grade_line = stdout
.lines()
.find(|l| l.starts_with("{\"mc_r_bootstrap\":"))
.unwrap_or_else(|| panic!("the mc_r_bootstrap grade line: {stdout}"));
let v: serde_json::Value = serde_json::from_str(grade_line).expect("grade line parses as JSON");
assert_eq!(v["mc_r_bootstrap"]["n_resamples"].as_u64(), Some(100), "resamples flow: {grade_line}");
assert!(v["mc_r_bootstrap"]["e_r"]["mean"].is_number(), "bootstrap E[R] present: {grade_line}");
}
/// #220 weld-gone proof for generalize: one mean-reversion candidate graded
/// across two instruments — three axes with names the CLI has never heard of,
/// including an F64 axis. Gated on the GER40/USDJPY Sept-2024 archive.
#[test]
fn generalize_campaign_runs_an_arbitrary_blueprint() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let (cwd, _g) = fresh_project();
let fixture = format!("{}/examples/r_meanrev_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"--axis", "r_meanrev_signal.mean_window.length=20",
"--axis", "r_meanrev_signal.var_window.length=20",
"--axis", "r_meanrev_signal.band.factor=2.0",
"--stop-length", "3", "--stop-k", "2.0",
"--from", GER40_SEPT2024_FROM_MS, "--to", GER40_SEPT2024_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: {:?} stderr: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(stdout.contains("\"generalize\":"), "aggregate object: {stdout}");
assert!(stdout.contains("\"n_instruments\":2"), "two instruments graded: {stdout}");
assert!(stdout.contains("\"worst_case\":"), "worst-case floor present: {stdout}");
}
/// #220: the walkforward campaign path requires >= 1 --axis (exit 2, data-free).
#[test]
fn walkforward_campaign_refuses_missing_axis() {
let cwd = temp_cwd("walkforward-campaign-no-axis");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["walkforward", &fixture, "--real", "GER40", "--stop-length", "14", "--stop-k", "2.0"])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "missing --axis is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("requires >= 1 --axis"), "axis-arity refusal: {stderr}");
}
/// #220: mc's two modes are disjoint — --seeds (synthetic seed family) with
/// --real (campaign pipeline) is a cross-mode usage error (exit 2, data-free).
#[test]
fn mc_campaign_refuses_seeds_with_real() {
let cwd = temp_cwd("mc-campaign-seeds-mix");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"mc", &fixture, "--real", "GER40", "--seeds", "3",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "--seeds with --real is a cross-mode usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("Usage: aura mc"), "usage refusal: {stderr}");
}
/// #220: a raw-form axis name is refused before any data access on the migrated
/// verbs, exactly as sweep's shipped refusal — echoing what the user typed, with
/// no store litter (mirrors aura_sweep_real_blueprint_refuses_a_raw_form_axis_name_before_data_access).
#[test]
fn walkforward_campaign_refuses_a_raw_form_axis_name() {
let cwd = temp_cwd("walkforward-campaign-raw-axis");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"walkforward", &fixture, "--real", "GER40",
"--axis", "fast.length=3,5", "--axis", "sma_signal.slow.length=12,20",
"--stop-length", "14", "--stop-k", "2.0",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "a raw-form axis name is refused before any data access");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("axis \"fast.length\""), "echoes the raw name verbatim: {stderr}");
assert!(!cwd.join("runs").exists(), "no store litter on the refusal path");
}
/// #220: an unknown axis name is refused at the dispatch boundary on the
/// migrated verbs (mirrors aura_sweep_rejects_an_unknown_axis).
#[test]
fn mc_campaign_refuses_an_unknown_axis() {
let cwd = temp_cwd("mc-campaign-unknown-axis");
let fixture = format!("{}/examples/r_breakout_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"mc", &fixture, "--real", "GER40",
"--axis", "r_breakout_signal.nope.length=1",
"--stop-length", "14", "--stop-k", "2.0",
])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "an unknown axis is refused at the dispatch boundary");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("axis \"r_breakout_signal.nope.length\""),
"names the offending axis: {stderr}"
);
}
/// #220: the mc campaign path also requires >= 1 --axis before any archive
/// access (exit 2, data-free) — the same arity gate walkforward enforces
/// (`walkforward_campaign_refuses_missing_axis`) and generalize enforces
/// (`generalize_refuses_no_axis`), but which Task 5 left untested for mc: the
/// `axes.is_empty()` check lives in `dispatch_mc`'s `--real` arm alone, so a
/// regression dropping it would otherwise surface only once real data hits the
/// R-bootstrap pipeline, not at argv-parse time.
#[test]
fn mc_campaign_refuses_missing_axis() {
let cwd = temp_cwd("mc-campaign-no-axis");
let fixture = format!("{}/examples/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["mc", &fixture, "--real", "GER40", "--stop-length", "14", "--stop-k", "2.0"])
.current_dir(&cwd)
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "missing --axis is a usage refusal");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(stderr.contains("Usage: aura mc"), "usage refusal: {stderr}");
assert!(!cwd.join("runs").exists(), "no store litter on the refusal path");
}
/// `aura run` on the shipped closed example reproduces the built-in r-sma grade
/// (#159): the example is the proven data successor to `--harness r-sma`. Survives
/// the Cut-1b builder deletion (depends only on the example + generic run path).
#[test]
fn shipped_r_sma_example_reproduces_the_builtin_grade() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "examples/r_sma.json"])
.output()
.expect("spawn aura run example");
assert_eq!(
out.status.code(),
Some(0),
"exit: {:?} stderr={}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
// the same metrics r_sma_single_run_output_golden pins for --harness r-sma
assert!(stdout.contains("\"expectancy_r\":1.2710005136982836"), "{stdout}");
assert!(stdout.contains("\"sqn\":3.141496526818299"), "{stdout}");
assert!(stdout.contains("\"total_pips\":0.34185000000002036"), "{stdout}");
assert!(stdout.contains("\"n_trades\":3"), "{stdout}");
}
/// `aura run` on the shipped closed r-meanrev example (#159 cut 3) actually loads
/// and grades through the real, separately-linked `aura` binary — not merely
/// in-process, as `r_meanrev_example_loaded_runs_identically_to_the_carved_signal`
/// (main.rs) and this test's own grade-equivalence check both do by calling
/// `run_signal_r`/`blueprint_from_json` inside the *test* binary. This is the
/// CLI-seam companion `shipped_r_sma_example_reproduces_the_builtin_grade` has for
/// r-sma: it pins that the `Scale`-based band, freshly added to the closed
/// vocabulary this iteration, resolves and runs cleanly through the production
/// vocabulary lookup + arg-parsing path a real invocation takes, not just through
/// the test harness's direct function calls. The synthetic stream never crosses
/// the band at this window/k, so the grade is genuinely all-zero (no trade) —
/// still a deterministic, meaningful pin: a regression that broke `Scale`'s CLI
/// wiring (e.g. a roster/registration mismatch) would surface as a non-zero exit
/// or a load error here, not as a metrics drift.
#[test]
fn shipped_r_meanrev_example_runs_end_to_end_via_aura_run() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "examples/r_meanrev.json"])
.output()
.expect("spawn aura run example");
assert_eq!(
out.status.code(),
Some(0),
"exit: {:?} stderr={}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
assert!(stdout.contains("\"topology_hash\":\""), "manifest carries the topology hash: {stdout}");
assert!(stdout.contains("\"expectancy_r\":0.0"), "{stdout}");
assert!(stdout.contains("\"n_trades\":0"), "{stdout}");
assert!(stdout.contains("\"total_pips\":0.0"), "{stdout}");
}