diff --git a/crates/aura-cli/src/graph_construct.rs b/crates/aura-cli/src/graph_construct.rs index 4571cd9..148a175 100644 --- a/crates/aura-cli/src/graph_construct.rs +++ b/crates/aura-cli/src/graph_construct.rs @@ -129,7 +129,7 @@ pub fn build_cmd() { let mut doc = String::new(); if let Err(e) = std::io::stdin().read_to_string(&mut doc) { eprintln!("aura: reading stdin: {e}"); - std::process::exit(2); + std::process::exit(1); } match build_from_str(&doc) { // `print!`, not `println!`: the canonical artifact is exactly the library @@ -139,7 +139,7 @@ pub fn build_cmd() { Ok(json) => print!("{json}"), Err(msg) => { eprintln!("aura: {msg}"); - std::process::exit(2); + std::process::exit(1); } } } @@ -209,13 +209,13 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd) { let mut doc = String::new(); if let Err(e) = std::io::stdin().read_to_string(&mut doc) { eprintln!("aura: reading stdin: {e}"); - std::process::exit(2); + std::process::exit(1); } match introspect_unwired(&doc) { Ok(s) => print!("{s}"), Err(m) => { eprintln!("aura: {m}"); - std::process::exit(2); + std::process::exit(1); } } } else { @@ -227,13 +227,13 @@ pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd) { let mut doc = String::new(); if let Err(e) = std::io::stdin().read_to_string(&mut doc) { eprintln!("aura: reading stdin: {e}"); - std::process::exit(2); + std::process::exit(1); } match build_from_str(&doc) { Ok(json) => println!("{}", crate::content_id(&json)), Err(m) => { eprintln!("aura: {m}"); - std::process::exit(2); + std::process::exit(1); } } } diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 7feccec..75ce59e 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -187,7 +187,7 @@ fn sim_optimal_manifest( /// (beside the run registry's `runs/`). Shared by every run form — all drain the same /// two f64 taps (equity off the broker, exposure off the strategy) and carry a /// `RunManifest`. Pure wiring over `ColumnarTrace` + `TraceStore`; the engine is -/// untouched. An I/O failure is a usage-level error (stderr + exit 2), per the spec. +/// untouched. An I/O failure is a runtime error (stderr + exit 1), per the spec. fn persist_traces( name: &str, manifest: &RunManifest, @@ -200,7 +200,7 @@ fn persist_traces( ]; if let Err(e) = TraceStore::open("runs").write(name, manifest, &taps) { eprintln!("aura: trace persist failed: {e}"); - std::process::exit(2); + std::process::exit(1); } } @@ -497,8 +497,8 @@ fn filter_to_tap(data: ChartData, tap: &str) -> Result { /// `aura chart [--tap ] [--panels]`: classify the name and render. A /// single run charts all its taps (or the one `--tap` selects); a family overlays -/// one tap (default `equity`) across its members; an unknown name is a usage error -/// (stderr + exit 2), never a panic. +/// one tap (default `equity`) across its members; an unknown name is a runtime error +/// (stderr + exit 1), never a panic. fn emit_chart(name: &str, tap: Option<&str>, mode: ChartMode) { let store = TraceStore::open("runs"); match store.name_kind(name) { @@ -507,7 +507,7 @@ fn emit_chart(name: &str, tap: Option<&str>, mode: ChartMode) { Ok(t) => t, Err(e) => { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } }; let mut data = build_chart_data(name, traces); @@ -516,7 +516,7 @@ fn emit_chart(name: &str, tap: Option<&str>, mode: ChartMode) { Ok(d) => d, Err(e) => { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } }; } @@ -528,14 +528,14 @@ fn emit_chart(name: &str, tap: Option<&str>, mode: ChartMode) { Ok(m) => m, Err(e) => { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } }; let data = match build_comparison_chart_data(name, &members, tap.unwrap_or("equity")) { Ok(d) => d, Err(e) => { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } }; let data = decimate(data, CHART_DECIMATE_BUCKETS); @@ -546,7 +546,7 @@ fn emit_chart(name: &str, tap: Option<&str>, mode: ChartMode) { "aura: no recorded run or family '{name}' under runs/traces \ (run `aura run --trace {name}` or `aura sweep --trace {name}` first)" ); - std::process::exit(2); + std::process::exit(1); } } } @@ -559,7 +559,7 @@ fn run_sample(trace: Option<&str>) -> RunReport { && let Err(e) = TraceStore::open("runs").ensure_name_free(n, WriteKind::Run) { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } let (mut h, rx_eq, rx_ex) = sample_harness(SYNTHETIC_PIP_SIZE); let sources: Vec> = @@ -592,13 +592,13 @@ fn run_sample(trace: Option<&str>) -> RunReport { /// fold as `run_sample`. The manifest window is read from a *separate* probe source /// (a Source is single-pass), so the run source streams the window untouched. /// A no-local-data condition (unknown symbol, or a window overlapping no file / no -/// bars) is a usage error: stderr + exit(2), not a panic. +/// bars) is a runtime error: stderr + exit(1), not a panic. fn run_sample_real(symbol: &str, from_ms: Option, to_ms: Option, trace: Option<&str>) -> RunReport { if let Some(n) = trace && let Err(e) = TraceStore::open("runs").ensure_name_free(n, WriteKind::Run) { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } let (source, window, pip_size) = open_real_source(symbol, from_ms, to_ms); let (mut h, rx_eq, rx_ex) = sample_harness(pip_size); @@ -655,14 +655,14 @@ enum DataSource { }, } -/// No-local-data refusal — stderr + exit(2), mirroring `run_sample_real`. +/// No-local-data refusal — stderr + exit(1), mirroring `run_sample_real`. fn no_real_data(symbol: &str) -> ! { eprintln!("aura: no local data for symbol '{symbol}' at {}", data_server::DEFAULT_DATA_PATH); - std::process::exit(2) + std::process::exit(1) } /// Resolve the per-instrument pip from the recorded geometry sidecar, or refuse -/// (stderr + exit 2) when the symbol has no recorded geometry — the single home of +/// (stderr + exit 1) when the symbol has no recorded geometry — the single home of /// the guessed-pip refusal, shared by `open_real_source` and `from_choice`. Reads /// the symbol's geometry metadata (not bar data) before the run source opens, so an /// instrument with no recorded geometry refuses without a guessed pip; the pip is @@ -676,7 +676,7 @@ fn pip_or_refuse(server: &std::sync::Arc, symbol: &str) refusing to run a real instrument with a guessed pip", data_server::DEFAULT_DATA_PATH ); - std::process::exit(2); + std::process::exit(1); } } } @@ -706,7 +706,7 @@ fn probe_window( /// the run source paired with its manifest `window` and per-instrument `pip_size`. /// Single home of the real-source construction the single-run handlers share — the /// sidecar-pip lookup, the `DataServer` `has_symbol` refusal, the probe-window pass, and -/// the run-source `open` (each refusal an stderr + exit 2). Pre-data refusals keep the +/// the run-source `open` (each refusal an stderr + exit 1). Pre-data refusals keep the /// pip honest by construction. Shared by `run_sample_real` and `run_stage1_r`. fn open_real_source( symbol: &str, @@ -731,7 +731,7 @@ fn open_real_source( } impl DataSource { - /// Build a provider from a parsed choice, or refuse (stderr + exit 2) on a symbol + /// Build a provider from a parsed choice, or refuse (stderr + exit 1) on a symbol /// with no recorded geometry / absent data — both BEFORE any member runs (Fork C/G), /// via the same `pip_or_refuse` / `no_real_data` helpers as /// `run_sample_real`. @@ -1613,7 +1613,7 @@ fn run_sweep(strategy: Strategy, name: &str, persist: bool, data: DataSource, gr && let Err(e) = TraceStore::open("runs").ensure_name_free(name, WriteKind::Family) { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } let reg = default_registry(); let family = match strategy { @@ -1627,7 +1627,7 @@ fn run_sweep(strategy: Strategy, name: &str, persist: bool, data: DataSource, gr Ok(id) => id, Err(e) => { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } }; for pt in &family.points { @@ -1690,7 +1690,7 @@ fn run_walkforward(strategy: Strategy, name: &str, persist: bool, data: DataSour && let Err(e) = TraceStore::open("runs").ensure_name_free(name, WriteKind::Family) { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } let reg = default_registry(); let result = walkforward_family(strategy, persist.then_some(name), &data, grid, select); @@ -1700,7 +1700,7 @@ fn run_walkforward(strategy: Strategy, name: &str, persist: bool, data: DataSour Ok(id) => id, Err(e) => { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } }; for w in &result.windows { @@ -2009,7 +2009,7 @@ fn run_mc(name: &str, persist: bool) { && let Err(e) = TraceStore::open("runs").ensure_name_free(name, WriteKind::Family) { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } let reg = default_registry(); let family = mc_family(persist.then_some(name)); @@ -2017,7 +2017,7 @@ fn run_mc(name: &str, persist: bool) { Ok(id) => id, Err(e) => { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } }; for draw in &family.draws { @@ -2102,7 +2102,7 @@ fn runs_families() { Ok(m) => m, Err(e) => { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } }; for fam in group_families(members) { @@ -2122,7 +2122,7 @@ fn runs_family(id: &str, rank: Option<&str>) { Ok(m) => m, Err(e) => { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } }; let Some(family) = group_families(members).into_iter().find(|f| f.id == id) else { @@ -2184,11 +2184,11 @@ fn point_from_params(space: &[ParamSpec], params: &[(String, Scalar)]) -> Vec Vec ReproduceReport { let members = reg.load_family_members().unwrap_or_else(|e| { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); }); let Some(family) = group_families(members).into_iter().find(|f| f.id == id) else { // reproduce is an action, not a lookup: an unknown id is a hard error (distinct // from `runs family `'s treat-as-empty exit 0). eprintln!("aura: no such family '{id}'"); - std::process::exit(2); + std::process::exit(1); }; let pip = data.pip_size(); let window = data.full_window(); @@ -2214,17 +2214,17 @@ fn reproduce_family_in(reg: &Registry, id: &str, data: &DataSource) -> Reproduce let stored = &member.report; let hash = stored.manifest.topology_hash.clone().unwrap_or_else(|| { eprintln!("aura: family member has no topology_hash; not a generated run"); - std::process::exit(2); + std::process::exit(1); }); let doc = reg .get_blueprint(&hash) .unwrap_or_else(|e| { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); }) .unwrap_or_else(|| { eprintln!("aura: blueprint {hash} missing from store"); - std::process::exit(2); + std::process::exit(1); }); // Reload the stored blueprint per use: a Composite is !Clone, and both the // param-space probe (below) and the re-run each consume one. The doc was @@ -2232,7 +2232,7 @@ fn reproduce_family_in(reg: &Registry, id: &str, data: &DataSource) -> Reproduce let reload = || { blueprint_from_json(&doc, &|t| std_vocabulary(t)).unwrap_or_else(|e| { eprintln!("aura: stored blueprint {hash} does not parse: {e:?}"); - std::process::exit(2); + std::process::exit(1); }) }; // The param_space of the WRAPPED signal — its knobs carry the `stage1_r` @@ -2401,7 +2401,7 @@ fn run_macd(trace: Option<&str>) -> RunReport { && let Err(e) = TraceStore::open("runs").ensure_name_free(n, WriteKind::Run) { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } let (tx_eq, rx_eq) = mpsc::channel(); let (tx_ex, rx_ex) = mpsc::channel(); @@ -3107,14 +3107,14 @@ fn run_blueprint_sweep(doc: &str, axes: &[(String, Vec)], name: &str, pe .expect("a loaded blueprint re-serializes"); reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); }); // Record the family unconditionally (C18/C21 lineage), exactly like `run_sweep`. let id = match reg.append_family(name, FamilyKind::Sweep, &sweep_member_reports(&family)) { Ok(id) => id, Err(e) => { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } }; for pt in &family.points { @@ -3141,10 +3141,10 @@ fn run_blueprint_walkforward(doc: &str, axes: &[(String, Vec)], name: &s &blueprint_from_json(doc, &|t| std_vocabulary(t)).expect("doc parse-validated at the dispatch boundary"), ) .expect("a loaded blueprint re-serializes"); - reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| { eprintln!("aura: {e}"); std::process::exit(2); }); + reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| { eprintln!("aura: {e}"); std::process::exit(1); }); let id = match reg.append_family(name, FamilyKind::WalkForward, &walkforward_member_reports(&result)) { Ok(id) => id, - Err(e) => { eprintln!("aura: {e}"); std::process::exit(2); } + Err(e) => { eprintln!("aura: {e}"); std::process::exit(1); } }; for w in &result.windows { println!("{}", family_member_line(&id, &w.run.oos_report)); @@ -3177,13 +3177,13 @@ fn run_blueprint_mc(doc: &str, n_seeds: u64, name: &str, data: DataSource) { .expect("a loaded blueprint re-serializes"); reg.put_blueprint(&topo, &canonical).unwrap_or_else(|e| { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); }); let id = match reg.append_family(name, FamilyKind::MonteCarlo, &mc_member_reports(&family)) { Ok(id) => id, Err(e) => { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } }; for draw in &family.draws { @@ -3422,7 +3422,7 @@ fn run_stage1_r( && let Err(e) = TraceStore::open("runs").ensure_name_free(n, WriteKind::Run) { eprintln!("aura: {e}"); - std::process::exit(2); + std::process::exit(1); } let (tx_eq, rx_eq) = mpsc::channel(); let (tx_ex, rx_ex) = mpsc::channel(); @@ -3521,7 +3521,7 @@ fn persist_traces_r( } if let Err(e) = TraceStore::open("runs").write(name, manifest, &taps) { eprintln!("aura: trace persist failed: {e}"); - std::process::exit(2); + std::process::exit(1); } } @@ -3596,7 +3596,7 @@ fn run_dispatch(args: RunArgs) -> Result { // `parse_param_cells`). The four dual-grammar subcommands carry an optional // `[blueprint]` positional; a first-positional that names an existing `.json` file // (`is_blueprint_file`) selects the loaded-blueprint branch, otherwise the built-in -// grammar. Exit codes are behaviour-preserving (usage errors stay exit 2). +// grammar. Usage errors (clap parse + argv-applicability guards) exit 2; runtime failures exit 1. /// The `aura` root parser. `#[command(version)]` reads `CARGO_PKG_VERSION` /// (the workspace `0.1.0`), so `aura --version` prints `aura 0.1.0`. @@ -5095,7 +5095,7 @@ mod tests { /// local Pepperstone archive is absent, so the test never fails on a machine /// without the data. Uses `GER40` — a *vetted* symbol (pip 1.0): the /// per-instrument-pip refusal makes `run_sample_real` reject an un-specced - /// symbol before any data access (`std::process::exit(2)`), so this CLI-level + /// symbol before any data access (`std::process::exit(1)`), so this CLI-level /// test must drive a symbol in the instrument table. The bounded-window AAPL.US /// streaming property still lives in the ingest `streaming_seam` test, which /// builds its source literally without the spec lookup. diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index 8ca3bda..87a26c4 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -131,7 +131,7 @@ fn run_with_trailing_token_is_strict_and_exits_two() { } #[test] -fn run_real_no_geometry_symbol_refuses_with_exit_2() { +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. @@ -139,7 +139,7 @@ fn run_real_no_geometry_symbol_refuses_with_exit_2() { .args(["run", "--real", "NONEXISTENT"]) .output() .expect("spawn aura"); - assert_eq!(out.status.code(), Some(2), "expected exit 2"); + 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'"), @@ -163,7 +163,7 @@ fn run_real_refusal_names_no_authored_floor() { .args(["run", "--real", "NONEXISTENT"]) .output() .expect("spawn aura"); - assert_eq!(out.status.code(), Some(2), "expected exit 2"); + assert_eq!(out.status.code(), Some(1), "expected exit 1"); let stderr = String::from_utf8_lossy(&out.stderr); assert!( !stderr.contains("instrument table"), @@ -182,7 +182,7 @@ fn run_real_refusal_names_no_authored_floor() { /// 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 +/// 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). @@ -192,7 +192,7 @@ fn run_real_no_geometry_symbol_emits_no_stdout_report() { .args(["run", "--real", "NONEXISTENT"]) .output() .expect("spawn aura"); - assert_eq!(out.status.code(), Some(2), "expected exit 2: {:?}", out.status); + 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: {:?}", @@ -221,7 +221,7 @@ fn run_real_sidecar_symbol_never_hits_the_pip_refusal() { "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 2). + // (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) @@ -230,9 +230,9 @@ fn run_real_sidecar_symbol_never_hits_the_pip_refusal() { "exit 0 must carry a JSON report, got: {:?}", String::from_utf8_lossy(&out.stdout) ), - Some(2) => assert!( + Some(1) => assert!( stderr.contains("no local data for symbol 'EURUSD'"), - "exit 2 for a symbol with a recorded sidecar must be the no-data path, got: {stderr}" + "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}"), } @@ -246,7 +246,7 @@ fn run_real_sidecar_symbol_never_hits_the_pip_refusal() { /// 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. +/// "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 @@ -259,12 +259,12 @@ fn run_real_sidecar_index_pip_reaches_the_emitted_manifest() { .expect("spawn aura"); // Skip on a data-less machine: a recorded-sidecar 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) { + // 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 2 for GER40 must be the no-data path, got: {stderr}" + "exit 1 for GER40 must be the no-data path, got: {stderr}" ); eprintln!("skip: no local GER40 data"); return; @@ -299,7 +299,7 @@ fn run_real_nonvetted_symbol_resolves_pip_from_sidecar() { // 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(2) { + 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"); @@ -511,12 +511,12 @@ fn run_real_with_trace_persists_taps_and_keeps_stdout_unchanged() { .expect("spawn aura run --real --trace"); // Skip on a data-less machine: GER40 with no local data takes the - // distinct no-data path (exit 2), never the pip-refusal, and writes no traces. - if out.status.code() == Some(2) { + // distinct no-data path (exit 1, runtime), never the pip-refusal, and writes no traces. + if out.status.code() == Some(1) { let stderr = String::from_utf8_lossy(&out.stderr); assert!( stderr.contains("no local data for symbol 'GER40'"), - "exit 2 for GER40 --trace must be the no-data path, got: {stderr}" + "exit 1 for GER40 --trace must be the no-data path, got: {stderr}" ); eprintln!("skip: no local GER40 data"); let _ = std::fs::remove_dir_all(&cwd); @@ -567,9 +567,9 @@ fn chart_emits_self_contained_html_for_a_persisted_run() { assert!(html.contains("\"equity\""), "equity series missing from the chart"); assert!(html.contains("uPlot=function"), "vendored uPlot not inlined"); - // a missing run is a usage error (stderr + exit 2), not a panic. + // a missing run is a runtime failure (stderr + exit 1), not a panic. let missing = Command::new(BIN).args(["chart", "nope"]).current_dir(&cwd).output().expect("spawn chart nope"); - assert_eq!(missing.status.code(), Some(2), "missing run must exit 2"); + assert_eq!(missing.status.code(), Some(1), "missing run must exit 1"); assert!(!missing.status.success()); let _ = std::fs::remove_dir_all(&cwd); @@ -608,11 +608,11 @@ fn chart_panels_flag_selects_panels_mode() { /// Property: `--tap ` on a single-run chart is a CLEAN refusal at the /// CLI seam — refuse-don't-guess (C10). The builder's no-tap `Err` is unit-tested, /// but this pins the user-facing wiring: `filter_to_tap` returning `Err` must reach -/// `eprintln!` + `exit(2)`, never a panic and never a chart of zero series. A +/// `eprintln!` + `exit(1)`, never a panic and never a chart of zero series. A /// regression that swallowed the `Err` (e.g. charting an empty `ChartData`) would /// exit 0 with an empty page; this fails it. Archive-local (persists its own run). #[test] -fn chart_tap_nonexistent_on_run_refuses_with_exit_2() { +fn chart_tap_nonexistent_on_run_refuses_with_exit_1() { let cwd = temp_cwd("chart-tap-missing"); // persist a single run; its taps are `equity` / `exposure`, never `nope`. @@ -620,7 +620,7 @@ fn chart_tap_nonexistent_on_run_refuses_with_exit_2() { assert!(traced.status.success(), "run --trace exit: {:?}", traced.status); let out = Command::new(BIN).args(["chart", "demo", "--tap", "nope"]).current_dir(&cwd).output().expect("spawn chart --tap"); - assert_eq!(out.status.code(), Some(2), "nonexistent tap must exit 2: {:?}", out.status); + assert_eq!(out.status.code(), Some(1), "nonexistent tap must exit 1: {:?}", out.status); assert!(out.stdout.is_empty(), "the refusal must not leak a chart page to stdout, got: {:?}", String::from_utf8_lossy(&out.stdout)); let stderr = String::from_utf8_lossy(&out.stderr); assert!( @@ -637,19 +637,19 @@ fn chart_tap_nonexistent_on_run_refuses_with_exit_2() { let _ = std::fs::remove_dir_all(&cwd); } -/// Property: an unknown `chart ` is a usage error pinned at BOTH the exit code -/// and the user-facing message — exit 2 (never a panic) AND the NotFound branch's +/// 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_2_and_message() { +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(2), "unknown name must exit 2: {:?}", out.status); + 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!( @@ -1345,16 +1345,16 @@ fn walkforward_real_persists_one_oos_member_per_window() { /// Property: the per-instrument pip refusal fires for `aura sweep --real /// ` exactly as it does for `aura run --real` — a symbol with no -/// recorded geometry (`NONEXISTENT`, no sidecar on any host) is rejected with exit 2 +/// recorded geometry (`NONEXISTENT`, no sidecar on any host) is rejected with exit 1 /// and the "no recorded geometry" message BEFORE any bar-data access. NOT gated: the /// refusal precedes the archive entirely, so it is CI-safe and runs on every machine. #[test] -fn sweep_real_no_geometry_symbol_refuses_with_exit_2() { +fn sweep_real_no_geometry_symbol_refuses_with_exit_1() { // not gated: the pip refusal happens before any data access. let dir = temp_cwd("sweep_real_refuse"); let out = std::process::Command::new(BIN) .args(["sweep", "--real", "NONEXISTENT"]).current_dir(&dir).output().unwrap(); - assert_eq!(out.status.code(), Some(2)); + assert_eq!(out.status.code(), Some(1)); assert!(String::from_utf8_lossy(&out.stderr).contains("no recorded geometry")); let _ = std::fs::remove_dir_all(&dir); @@ -1511,13 +1511,13 @@ fn trace_name_collision_across_kinds_is_refused() { let run = Command::new(BIN).args(["run", "--trace", "t"]).current_dir(&cwd).output().expect("spawn run --trace"); assert!(run.status.success(), "run --trace exit: {:?}", run.status); let sweep = Command::new(BIN).args(["sweep", "--trace", "t"]).current_dir(&cwd).output().expect("spawn sweep --trace"); - assert_eq!(sweep.status.code(), Some(2), "sweep onto a run name must exit 2"); + assert_eq!(sweep.status.code(), Some(1), "sweep onto a run name must exit 1"); // reverse: sweep then run on a fresh name -> the run is refused. let sweep2 = Command::new(BIN).args(["sweep", "--trace", "u"]).current_dir(&cwd).output().expect("spawn sweep --trace u"); assert!(sweep2.status.success(), "sweep --trace u exit: {:?}", sweep2.status); let run2 = Command::new(BIN).args(["run", "--trace", "u"]).current_dir(&cwd).output().expect("spawn run --trace u"); - assert_eq!(run2.status.code(), Some(2), "run onto a family name must exit 2"); + assert_eq!(run2.status.code(), Some(1), "run onto a family name must exit 1"); let _ = std::fs::remove_dir_all(&cwd); } @@ -3115,11 +3115,11 @@ fn generalize_grades_a_candidate_across_two_instruments() { .current_dir(&cwd) .output() .expect("spawn aura"); - if out.status.code() == Some(2) { + 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 2 must be a data refusal, got: {stderr}" + "exit 1 must be a data refusal, got: {stderr}" ); eprintln!("skip: no local GER40/USDJPY data"); return; @@ -3262,11 +3262,11 @@ fn generalize_persists_a_discoverable_cross_instrument_family() { .current_dir(&cwd) .output() .expect("spawn aura"); - if out.status.code() == Some(2) { + 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 2 must be a data refusal, got: {stderr}" + "exit 1 must be a data refusal, got: {stderr}" ); eprintln!("skip: no local GER40/USDJPY data"); let _ = std::fs::remove_dir_all(&cwd); @@ -4019,3 +4019,21 @@ fn dual_grammar_discriminator_requires_an_existing_file_not_just_json_suffix() { "the built-in run grammar (not the blueprint-read path) must handle it: {stderr:?}" ); } + +/// The exit-code partition (#175 iteration 2, deviation #8): a USAGE error (a +/// malformed command line) exits 2; a RUNTIME failure (a well-formed command +/// whose needed data/state is missing) exits 1. Pins the partition as a property. +#[test] +fn exit_codes_partition_usage_two_from_runtime_one() { + // usage: an unknown flag is a command-line error → exit 2 + let usage = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args(["run", "--bogus"]).output().expect("spawn"); + assert_eq!(usage.status.code(), Some(2), + "unknown flag is a usage error → 2; stderr: {}", String::from_utf8_lossy(&usage.stderr)); + // runtime: a well-formed command whose symbol has no recorded data → exit 1 + let runtime = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args(["run", "--real", "NONEXISTENT"]).output().expect("spawn"); + assert_eq!(runtime.status.code(), Some(1), + "no data for a valid command is a runtime failure → 1; stderr: {}", + String::from_utf8_lossy(&runtime.stderr)); +} diff --git a/crates/aura-cli/tests/graph_construct.rs b/crates/aura-cli/tests/graph_construct.rs index 31efbd0..8c123fc 100644 --- a/crates/aura-cli/tests/graph_construct.rs +++ b/crates/aura-cli/tests/graph_construct.rs @@ -25,6 +25,22 @@ fn run(args: &[&str], stdin_doc: &str) -> (String, String, bool) { ) } +/// Like `run`, but returns the exact process exit CODE (not just success/failure). +/// The exit-code partition (#175 iteration 2) is a property of the integer, so its +/// tests must assert `Some(1)` vs `Some(2)`, which `run`'s bool cannot express. +fn run_code(args: &[&str], stdin_doc: &str) -> (String, Option) { + let mut child = Command::new(BIN) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn aura"); + child.stdin.take().unwrap().write_all(stdin_doc.as_bytes()).unwrap(); + let out = child.wait_with_output().expect("wait aura"); + (String::from_utf8_lossy(&out.stderr).into_owned(), out.status.code()) +} + const SIGNAL_DOC: &str = r#"[ {"op":"source","role":"price","kind":"F64"}, {"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}}, @@ -251,3 +267,32 @@ fn graph_introspect_content_id_rejects_a_bad_document() { assert!(stdout.is_empty(), "no content id emitted on error: {stdout}"); assert!(stderr.contains("Nope"), "names the cause: {stderr}"); } + +/// Property (exit-code partition, #175 iteration 2, deviation #8): a malformed +/// op-script piped to `aura graph build` is a RUNTIME failure — bad stdin *content*, +/// exit 1 — NOT the usage code 2. The attribution principle classifies bad +/// piped-stdin data as environment/runtime (the command line was well-formed), +/// distinct from a command-line fault. The sibling `graph_build_fails_fast_at_the_ +/// offending_op` pins only `!ok` (non-zero); this pins the exact integer, so a +/// regression that reverted the graph_construct stdin site to the pre-split exit 2 +/// fails here. +#[test] +fn graph_build_bad_stdin_content_is_runtime_exit_1() { + let (stderr, code) = run_code(&["graph", "build"], r#"[{"op":"add","type":"Nope"}]"#); + assert_eq!(code, Some(1), "bad stdin content is a runtime failure -> exit 1; stderr: {stderr}"); + assert!(stderr.contains("Nope"), "still names the cause: {stderr}"); +} + +/// Property (exit-code partition, #175 iteration 2): within the SAME `graph +/// introspect` subcommand, an invalid `--node ` flag VALUE is a USAGE error (a +/// command-line fault the user must fix in argv) — exit 2 — distinct from bad stdin +/// content on the `--unwired`/`--content-id` branches, which is runtime exit 1. This +/// pins the argv-fault-vs-stdin-fault boundary the attribution principle draws at +/// the exact integer; the sibling `graph_introspect_node_rejects_an_unknown_type` +/// checks only non-zero, and would pass whether the code were 1 or 2. +#[test] +fn graph_introspect_unknown_node_flag_is_usage_exit_2() { + let (stderr, code) = run_code(&["graph", "introspect", "--node", "Bogus"], ""); + assert_eq!(code, Some(2), "an invalid --node flag value is a usage error -> exit 2; stderr: {stderr}"); + assert!(stderr.contains("Bogus"), "names the bad type: {stderr}"); +}