//! 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 = 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 /// `/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 `/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 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 ` /// 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 ` 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 ` 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 /// ` 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 ` 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 ''" 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 /// --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(""), "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("