feat(0099): CLI exit-code split (iteration 2) — usage=2, runtime=1
Apply the clean exit-code partition (#175 deviation #8) on top of the clap migration: runtime failures now exit 1, usage errors stay exit 2, with no same-class inconsistency. Partition (attribution principle): exit 2 = a command-line fault (a bad flag value, an inapplicable combination, or the content of an argv-named file); exit 1 = the command was well-formed but the environment / recorded state it needs is missing, or piped stdin data is bad. The 6 boundary cases the spec's "parse-time vs run-time" rule under-specified are resolved on #175 (malformed/open argv blueprint -> usage 2; stdin op-script content -> runtime 1; applicability refusals -> usage 2; unknown --axis / --metric -> usage 2). Flipped 2->1 (runtime): no local data, no recorded geometry, no recorded run/family, "run has no tap named", trace-name collision, family/trace persist-write failures (normalizing the lone generalize inconsistency -- all persist -> 1 together), missing/corrupt content-addressed store state (the reproduce path), chart-read failures, and graph stdin op-script content + stdin-read I/O. reproduce-diverged stays exit 1. Usage sites (clap parse, argv-applicability guards, the dual-grammar blueprint-file read/parse) stay exit 2. Tests: ~8 domain-refusal pins flipped Some(2)->Some(1) (message substrings unchanged), the four runtime tests renamed _exit_2 -> _exit_1, and a partition property test added. Orchestrator fix after the loop: 3 sibling no-data skip-guards (run_real_with_trace + the two generalize cross-instrument tests) that the plan's Task-1 list missed -- left at Some(2), they would fall through to assert Some(0) and FAIL on a data-less machine; flipped to Some(1) for consistency with the flipped code. Deferred (cosmetic, filed forward): full "Usage:" / "usage:" / bare error-message casing normalization -- no functional impact, high pin-churn. Verified green (orchestrator, this session): cargo build --workspace, cargo clippy --workspace --all-targets -- -D warnings, and cargo test --workspace all clean (0 failed across every test binary). This completes the #175 clap-adoption cycle (iteration 1 = clap migration at 366170a; iteration 2 = this exit-code split). refs #175
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+45
-45
@@ -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<ChartData, String> {
|
||||
|
||||
/// `aura chart <name> [--tap <t>] [--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<Box<dyn aura_engine::Source>> =
|
||||
@@ -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<i64>, to_ms: Option<i64>, 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<data_server::DataServer>, 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<Ce
|
||||
.map(|(_, s)| s.cell())
|
||||
.unwrap_or_else(|| {
|
||||
// A manifest missing a param the reloaded space expects is
|
||||
// corrupted-on-disk data — exit cleanly (`aura:` + exit 2) like
|
||||
// corrupted-on-disk data — exit cleanly (`aura:` + exit 1) like
|
||||
// every other persisted-data failure on the reproduce path, not
|
||||
// a panic.
|
||||
eprintln!("aura: manifest is missing param {}", ps.name);
|
||||
std::process::exit(2);
|
||||
std::process::exit(1);
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
@@ -2199,13 +2199,13 @@ fn point_from_params(space: &[ParamSpec], params: &[(String, Scalar)]) -> Vec<Ce
|
||||
fn reproduce_family_in(reg: &Registry, id: &str, data: &DataSource) -> 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 <id>`'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<Scalar>)], 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<Scalar>)], 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<RunReport, String> {
|
||||
// `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.
|
||||
|
||||
@@ -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 <nonexistent>` on a single-run chart is a CLEAN refusal at the
|
||||
/// CLI seam — refuse-don't-guess (C10). The builder's no-tap `Err` is unit-tested,
|
||||
/// but this pins the user-facing wiring: `filter_to_tap` returning `Err` must reach
|
||||
/// `eprintln!` + `exit(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 <name>` 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 <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_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
|
||||
/// <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 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));
|
||||
}
|
||||
|
||||
@@ -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<i32>) {
|
||||
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 <T>` 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}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user