Files
Aura/crates/aura-cli/tests/cli_run.rs
T
Brummel 26bec6d64a feat: per-instrument pip metadata channel for the sim-optimal broker
The data-server symbol stream carries only price bars, no instrument metadata.
So SimBroker was constructed with one global pip_size literal (0.0001, correct
only for 5-decimal FX), and `aura run --real <symbol>` emitted wrong-unit pip
equity for any non-FX instrument (AAPL.US, BTCUSD off by orders of magnitude).

Add the missing channel to *specify* per-instrument pip:

- aura-ingest: `InstrumentSpec { pip_size }` + `instrument_spec(symbol)` — a
  typed, Rust-authored vetted lookup (NOT Aura.toml; the later Aura.toml schema
  can subsume it). Seeded GER40/FRA40 = 1.0 (index points), EURUSD/GBPUSD/USDCAD
  = 0.0001 (5-dp FX majors); None for an un-specced symbol.
- aura-cli: `sample_harness` and `sim_optimal_manifest` gain a `pip_size`
  parameter; `run_sample_real` looks the pip up by symbol BEFORE any data access
  and refuses (exit 2) an un-specced instrument rather than guessing — honest by
  construction. The looked-up pip reaches both the broker divisor and the
  manifest broker label (`format!`). Every synthetic caller passes the unchanged
  0.0001, now named `SYNTHETIC_PIP_SIZE`. SimBroker (aura-std) is untouched.

Scope: narrowed to the CLI real path — the actual bug. The runnable GER40
examples already bake the correct 1.0 and are NOT buggy; centralizing them
through the lookup is deferred to #98.

Decisions (user-directed, recorded on #22): metadata channel = typed
InstrumentSpec at the source edge; un-specced real symbol refuses; seed =
GER40/FRA40 + FX majors; example refactor deferred. The spec went through the
grounding-check gate; the auto-sign panel surfaced the refusal-behaviour and
scope as genuine design decisions, escalated and resolved by the user.

Tests: instrument_spec unit; manifest pip rendering (1.0 -> "1", 0.0001
unchanged); non-gated CLI refusal (exit 2, no stdout leak, vetted symbol clears
the lookup); gated GER40 binary-boundary honesty (pip_size=1 in the emitted
manifest, deterministic). The pre-existing run_sample_real determinism test was
retargeted AAPL.US -> GER40 (AAPL.US is un-specced and would now refuse at the
lookup); the AAPL.US bounded-window streaming property still lives in
aura-ingest/tests/streaming_seam.rs (literal source, no spec lookup).

Verified: cargo build --workspace, clippy --all-targets -D warnings, and
cargo test --workspace all green; the gated GER40 path ran with local data.

closes #22
2026-06-18 19:36:43 +02:00

460 lines
22 KiB
Rust

//! Integration test: drive the built `aura` binary as a downstream user would,
//! asserting the `run` subcommand's stdout/exit contract and the bad-args path.
use std::process::Command;
/// Path to the freshly-built `aura` binary (Cargo sets this env var for the test
/// crate; the binary is named `aura` in `Cargo.toml`).
const BIN: &str = env!("CARGO_BIN_EXE_aura");
/// A fresh, unique working directory for a process test that persists run
/// records (so `aura sweep`'s `./runs/runs.jsonl` never dirties the repo).
/// Unique per test + per process; no external tempfile dependency.
fn temp_cwd(name: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("aura-cli-{}-{}", std::process::id(), name));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create temp cwd");
dir
}
#[test]
fn run_prints_json_and_exits_zero() {
let out = Command::new(BIN).arg("run").output().expect("spawn aura run");
assert!(out.status.success(), "exit status: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
// exactly one line (the JSON object + a trailing newline from println!).
assert_eq!(stdout.lines().count(), 1, "stdout was: {stdout:?}");
let line = stdout.trim_end();
// canonical cycle-0009 JSON shape: nested manifest + metrics, stable keys.
// The manifest leads with a `commit` field; its value is the build's git
// identity (asserted by `run_manifest_commit_carries_real_git_head`), so
// this shape check pins only the surrounding structure, not the value.
assert!(line.starts_with("{\"manifest\":{\"commit\":\""), "got: {line}");
assert!(line.contains("\"broker\":\"sim-optimal(pip_size=0.0001)\""), "got: {line}");
assert!(line.contains("\"window\":[1,7]"), "got: {line}");
assert!(line.contains("\"metrics\":{\"total_pips\":"), "got: {line}");
// the integer sign-flip count is stable across float renderings.
assert!(line.ends_with("\"exposure_sign_flips\":1}}"), "got: {line}");
}
/// Property: the RunManifest is self-identifying — its `commit` carries the
/// real code identity that produced the run, not a placeholder. C8/C18 audit
/// trail (this run = this commit): once runs are archived, `manifest.commit`
/// must point back at the source. The build captures the current git HEAD at
/// compile time; the `"unknown"` literal survives only when there is no git.
///
/// Structural assertion (not exact-equality) on purpose: the working tree may
/// be dirty when this runs (e.g. this very test file uncommitted), so a build
/// that appends a `-dirty` suffix would not equal `rev-parse HEAD` verbatim.
/// The honest contract is: the field CONTAINS the current HEAD sha AND is not
/// the bare `"unknown"` placeholder — true regardless of dirtiness/suffix.
#[test]
fn run_manifest_commit_carries_real_git_head() {
// The current HEAD sha, obtained the same way the build is expected to.
let head = Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.expect("spawn git rev-parse HEAD");
assert!(head.status.success(), "git rev-parse HEAD failed");
let head_sha = String::from_utf8(head.stdout).expect("utf-8 sha");
let head_sha = head_sha.trim();
assert!(!head_sha.is_empty(), "empty HEAD sha");
// Drive the binary and pull `manifest.commit` out of the single JSON line.
let out = Command::new(BIN).arg("run").output().expect("spawn aura run");
assert!(out.status.success(), "exit status: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let line = stdout.trim_end();
// Locate the `"commit":"<value>"` value without a JSON dependency (the
// sibling tests parse this output by substring too).
let key = "\"commit\":\"";
let start = line.find(key).expect("manifest has a commit field") + key.len();
let rest = &line[start..];
let end = rest.find('"').expect("commit field is a closed string");
let commit = &rest[..end];
assert_ne!(
commit, "unknown",
"manifest.commit is still the placeholder: {line}"
);
assert!(
commit.contains(head_sha),
"manifest.commit {commit:?} should contain the current HEAD sha {head_sha:?}; line: {line}"
);
}
/// Property: `aura run` is strict about its argument vector — a trailing
/// unexpected token (e.g. a typo'd flag like `aura run --sweep`) takes the
/// error path (usage on stderr, exit 2), never a silent full run. This is the
/// strict reading ratified in #16: keying only on the first token being `run`
/// and ignoring the rest would let a typo masquerade as a successful run.
/// The same test pins positive-preservation — bare `aura run` still emits the
/// JSON report on stdout and exits 0 — so the strict guard cannot regress the
/// happy path.
#[test]
fn run_with_trailing_token_is_strict_and_exits_two() {
// Negative: an unexpected trailing token is rejected like a bad-args call.
let out = Command::new(BIN)
.args(["run", "extra-garbage"])
.output()
.expect("spawn aura run extra-garbage");
assert_eq!(
out.status.code(),
Some(2),
"`aura run extra-garbage` must reject the trailing token: {:?}",
out.status
);
assert!(
out.stdout.is_empty(),
"stdout should be empty on the usage path, got: {:?}",
String::from_utf8_lossy(&out.stdout)
);
let stderr = String::from_utf8(out.stderr).expect("utf-8 stderr");
assert!(stderr.contains("usage"), "stderr was: {stderr:?}");
// Positive-preservation: bare `aura run` still runs and prints the report.
let ok = Command::new(BIN).arg("run").output().expect("spawn aura run");
assert_eq!(
ok.status.code(),
Some(0),
"bare `aura run` must still succeed: {:?}",
ok.status
);
let ok_stdout = String::from_utf8(ok.stdout).expect("utf-8 stdout");
assert!(
ok_stdout.trim_start().starts_with("{\"manifest\":"),
"bare `aura run` should print the JSON report, got: {ok_stdout:?}"
);
}
#[test]
fn run_real_unspecced_symbol_refuses_with_exit_2() {
// The instrument_spec lookup precedes any data access, so an un-specced symbol
// refuses with NO local data required (CI-safe). "NONEXISTENT" is not in the
// vetted table.
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--real", "NONEXISTENT"])
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "expected exit 2");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no vetted pip/instrument spec for symbol 'NONEXISTENT'"),
"expected the per-instrument-pip refusal message, 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(2)` 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_unspecced_symbol_emits_no_stdout_report() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--real", "NONEXISTENT"])
.output()
.expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "expected exit 2: {:?}", 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 being un-specced, not on
/// `--real` being taken at all. A *vetted* symbol (`EURUSD`, in the instrument
/// table) gets PAST the `instrument_spec` 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 vetted pip/instrument spec" 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_vetted_symbol_never_hits_the_pip_refusal() {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--real", "EURUSD"])
.output()
.expect("spawn aura");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("no vetted pip/instrument spec"),
"a vetted symbol 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 2).
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(2) => assert!(
stderr.contains("no local data for symbol 'EURUSD'"),
"exit 2 for a vetted symbol must be the no-data path, got: {stderr}"
),
other => panic!("unexpected exit for a vetted real run: {other:?}; stderr: {stderr}"),
}
}
/// Property: a vetted 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 un-specced→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 2), not the pip-refusal, asserted above.
#[test]
fn run_real_vetted_index_pip_reaches_the_emitted_manifest() {
// The vetted GER40 Sept-2024 window (inclusive Unix-ms), the same calendar
// month the gated ingest path drives; bounding it keeps the run fast.
const FROM_MS: &str = "1725148800000";
const TO_MS: &str = "1727740799999";
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(["run", "--real", "GER40", "--from", FROM_MS, "--to", TO_MS])
.output()
.expect("spawn aura");
// Skip on a data-less machine: a vetted symbol with no local data takes the
// distinct no-data path (exit 2, "no local data"), never the pip-refusal.
if out.status.code() == Some(2) {
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no local data for symbol 'GER40'"),
"exit 2 for vetted GER40 must be the no-data path, got: {stderr}"
);
eprintln!("skip: no local GER40 data");
return;
}
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let line = stdout.trim_end();
// the looked-up index pip (1.0 -> "1") reaches the manifest, not the FX 0.0001.
assert!(
line.contains("\"broker\":\"sim-optimal(pip_size=1)\""),
"GER40 must render the index pip in the manifest, got: {line}"
);
}
#[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: `aura graph` emits a single self-contained HTML page — the
/// interactive pin-graph viewer with the deterministic model inlined and the
/// vendored Graphviz-WASM + pan-zoom inlined (no remote fetch). Driven through
/// the built binary so it pins the observable CLI contract. The DOT/SVG are
/// Graphviz-version-dependent and not asserted (the prototype is the by-hand
/// visual reference); this pins the page envelope + the retirement of the old
/// ascii-dag notation.
#[test]
fn graph_emits_self_contained_html_viewer() {
let out = Command::new(BIN).arg("graph").output().expect("spawn aura graph");
assert_eq!(out.status.code(), Some(0), "`aura graph` exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
// a self-contained HTML document...
assert!(
stdout.trim_start().starts_with("<!doctype html>"),
"not an HTML doc: {:?}",
&stdout[..stdout.len().min(80)]
);
// ...with the deterministic model inlined as the viewer's data source...
assert!(stdout.contains("window.AURA_MODEL = {\"root\":"), "model not inlined: {stdout:?}");
// ...the Graphviz-WASM bootstrap present, and no remote asset fetch.
assert!(stdout.contains("Viz.instance"), "viewer bootstrap missing");
assert!(!stdout.contains("<script src"), "assets must be inlined, found remote <script src>");
// the retired ascii-dag invented notation is gone.
assert!(!stdout.contains("Sub(#Sf,#Ss)"), "retired ascii-dag notation must be gone: {stdout}");
}
#[test]
fn sweep_prints_four_family_json_lines_and_exits_zero() {
let cwd = temp_cwd("sweep4");
let out = Command::new(BIN).arg("sweep").current_dir(&cwd).output().expect("spawn aura sweep");
assert!(out.status.success(), "exit status: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert_eq!(stdout.lines().count(), 4, "stdout was: {stdout:?}");
let first = stdout.lines().next().unwrap();
// each line is now a family member: the assigned `family_id` plus the nested
// RunReport (C18/C21 lineage). The first run assigns `sweep-0`; the nested
// report is serialized via `serde_json`, so the manifest's fields lead with
// `broker` (the commit value is the volatile git HEAD, pinned by params below).
assert!(first.starts_with("{\"family_id\":\"sweep-0\",\"report\":{\"manifest\":{"), "got: {stdout}");
assert!(
first.contains("\"params\":[[\"signals.trend.fast.length\",{\"I64\":2}],[\"signals.trend.slow.length\",{\"I64\":4}],[\"signals.momentum.fast.length\",{\"I64\":2}],[\"signals.momentum.slow.length\",{\"I64\":4}],[\"signals.momentum.signal.length\",{\"I64\":3}],[\"signals.blend.weights[0]\",{\"F64\":1.0}],[\"signals.blend.weights[1]\",{\"F64\":1.0}],[\"exposure.scale\",{\"F64\":0.5}]]"),
"got: {stdout}"
);
let _ = std::fs::remove_dir_all(&cwd);
}
#[test]
fn sweep_with_trailing_token_is_strict_and_exits_two() {
// strict-arg reading (#16): an unexpected trailing token is a usage error.
let out = Command::new(BIN).args(["sweep", "extra"]).output().expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "exit status: {:?}", out.status);
}
/// Property: `aura mc` runs the built-in Monte-Carlo family end-to-end through the
/// binary — it prints one member line per seed (carrying the assigned `family_id`)
/// plus one `mc_aggregate` line, exits 0, and persists the draws as a discoverable
/// `MonteCarlo` family (C12 axis 4 / C18/C21). Driven through the built binary so
/// it pins the observable CLI contract, not just the in-crate render helper.
#[test]
fn mc_runs_persists_a_monte_carlo_family_and_lists_it() {
let cwd = temp_cwd("mc-family");
let out = Command::new(BIN).arg("mc").current_dir(&cwd).output().expect("spawn aura mc");
assert!(out.status.success(), "mc exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let lines: Vec<&str> = stdout.lines().collect();
// three built-in seeds -> three member lines + one aggregate line.
assert_eq!(lines.len(), 4, "mc stdout: {stdout:?}");
for line in &lines[..3] {
assert!(line.contains("\"family_id\":\"mc-0\""), "member missing family_id: {line}");
assert!(line.contains("\"seed\":"), "member missing seed: {line}");
}
assert!(lines[3].contains("\"mc_aggregate\":"), "missing aggregate line: {}", lines[3]);
// the run persisted the draws as a discoverable MonteCarlo family.
let fams = Command::new(BIN).args(["runs", "families"]).current_dir(&cwd).output().expect("spawn families");
assert!(fams.status.success(), "families exit: {:?}", fams.status);
let fams_out = String::from_utf8(fams.stdout).expect("utf-8");
assert_eq!(fams_out.lines().count(), 1, "families: {fams_out:?}");
assert!(fams_out.contains("\"family_id\":\"mc-0\""), "families: {fams_out:?}");
assert!(fams_out.contains("\"kind\":\"MonteCarlo\""), "families: {fams_out:?}");
assert!(fams_out.contains("\"members\":3"), "families: {fams_out:?}");
let _ = std::fs::remove_dir_all(&cwd);
}
#[test]
fn runs_families_list_and_per_family_rank_across_invocations() {
let cwd = temp_cwd("runs-flow");
// two sweeps accumulate two families (sweep-0, sweep-1) over time, each a
// related set of 4 records (C18/C21 lineage), in the sibling family store.
for _ in 0..2 {
let out = Command::new(BIN).arg("sweep").current_dir(&cwd).output().expect("spawn sweep");
assert!(out.status.success(), "sweep exit: {:?}", out.status);
}
// `runs families` shows both stored families in first-seen order, each with
// its kind + member count.
let fams = Command::new(BIN).args(["runs", "families"]).current_dir(&cwd).output().expect("spawn families");
assert!(fams.status.success(), "families exit: {:?}", fams.status);
let fams_out = String::from_utf8(fams.stdout).expect("utf-8");
let fam_lines: Vec<&str> = fams_out.lines().collect();
assert_eq!(fam_lines.len(), 2, "families: {fams_out:?}");
assert!(fam_lines[0].contains("\"family_id\":\"sweep-0\""), "fam0: {fams_out:?}");
assert!(fam_lines[1].contains("\"family_id\":\"sweep-1\""), "fam1: {fams_out:?}");
assert!(fam_lines.iter().all(|l| l.contains("\"members\":4")), "members: {fams_out:?}");
// `runs family sweep-0 rank total_pips`: the family's 4 members, highest
// total_pips first.
let rank = Command::new(BIN)
.args(["runs", "family", "sweep-0", "rank", "total_pips"])
.current_dir(&cwd)
.output()
.expect("spawn rank");
assert!(rank.status.success(), "rank exit: {:?}", rank.status);
let rank_out = String::from_utf8(rank.stdout).expect("utf-8");
assert_eq!(rank_out.lines().count(), 4, "rank: {rank_out:?}");
assert!(rank_out.lines().all(|l| l.starts_with("{\"manifest\":{")), "rank shape: {rank_out:?}");
let pips: Vec<f64> = rank_out
.lines()
.map(|l| {
let key = "\"total_pips\":";
let start = l.find(key).expect("line has total_pips") + key.len();
let tail = &l[start..];
let end = tail.find([',', '}']).expect("token end");
tail[..end].parse().expect("total_pips is an f64")
})
.collect();
assert!(pips.windows(2).all(|w| w[0] >= w[1]), "rank not descending: {pips:?}");
// an unknown metric is a usage error
let bogus = Command::new(BIN)
.args(["runs", "family", "sweep-0", "rank", "bogus"])
.current_dir(&cwd)
.output()
.expect("spawn bogus");
assert_eq!(bogus.status.code(), Some(2), "bogus exit: {:?}", bogus.status);
// an unknown family id is an empty family: zero lines, exit 0 (consistent
// with `runs list` over an empty store).
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_on_empty_registry_is_zero_lines_exit_zero() {
let cwd = temp_cwd("runs-empty");
let out = Command::new(BIN).args(["runs", "list"]).current_dir(&cwd).output().expect("spawn list");
assert!(out.status.success(), "list exit: {:?}", out.status);
assert_eq!(out.stdout.len(), 0, "expected no output, got: {:?}", out.stdout);
let _ = std::fs::remove_dir_all(&cwd);
}
#[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);
}