From 5c6de45b6e8240b8349d073540dc11ed60f10f0e Mon Sep 17 00:00:00 2001 From: Brummel Date: Wed, 24 Jun 2026 14:35:51 +0200 Subject: [PATCH] fix(cli): list valid taps on bad --tap, make --help uniform across subcommands Two CLI-UX defects the Stage-1 R fieldtest surfaced (#131): 1. Tap discovery. `aura chart --tap ` rejected an unknown tap but the error omitted the valid names. filter_to_tap now enumerates the run's available taps in the message ("... (available: equity, exposure)"), so the user can correct a typo instead of guessing -- refuse, but help. 2. Uniform --help. `aura chart --help` errored with "unexpected chart argument '--help'" (exit 2) and `aura run --help` leaked usage to stderr via the error path, while bare `aura --help` printed to stdout (exit 0). main() now intercepts `--help`/`-h` anywhere in the invocation, prints usage to stdout, and exits 0 -- uniform for every subcommand. The now-dead bare-flag match arm is removed (the early intercept covers it). RED-first: the existing chart_tap_nonexistent test gains an available-taps assertion (was: message had no names), and a new per_subcommand_help test pins `aura --help` -> stdout usage + exit 0 for run/chart/sweep/walkforward/ mc/graph/runs (was: chart --help exited 2). Unknown-subcommand stays exit 2. Verified: clippy --all-targets -D warnings clean; full suite 514 passed / 0 failed (one new test); live `aura chart --help` prints usage to stdout, exit 0. closes #131 --- crates/aura-cli/src/main.rs | 16 ++++++++++---- crates/aura-cli/tests/cli_run.rs | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 5f316e9..b6c28ce 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -467,10 +467,12 @@ fn build_comparison_chart_data( /// run has no such tap (refuse-don't-guess). Used by the `--tap` flag on the /// single-run chart path; without `--tap` the single-run page is unchanged. fn filter_to_tap(data: ChartData, tap: &str) -> Result { - let series: Vec = data.series.into_iter().filter(|s| s.name == tap).collect(); - if series.is_empty() { - return Err(format!("run has no tap named '{tap}'")); + if !data.series.iter().any(|s| s.name == tap) { + // List the valid taps so the user can correct a typo (#131) — refuse, but help. + let available: Vec<&str> = data.series.iter().map(|s| s.name.as_str()).collect(); + return Err(format!("run has no tap named '{tap}' (available: {})", available.join(", "))); } + let series: Vec = data.series.into_iter().filter(|s| s.name == tap).collect(); let mut meta = data.meta; meta.taps = vec![tap.to_string()]; Ok(ChartData { xs: data.xs, series, meta }) @@ -1966,6 +1968,13 @@ fn main() { // so an unexpected trailing token falls through to the usage-error path rather // than masquerading as a successful run (#16 strict reading). let args: Vec = std::env::args().skip(1).collect(); + // A `--help` / `-h` anywhere prints usage to stdout and exits 0, uniformly across + // subcommands (#131): `aura chart --help` shows usage instead of erroring + // "unexpected argument", and the bare `aura --help` is the same affordance. + if args.iter().any(|a| a == "--help" || a == "-h") { + println!("{USAGE}"); + return; + } match args.iter().map(String::as_str).collect::>().as_slice() { ["run", rest @ ..] => match parse_run_args(rest) { Ok(args) => match run_dispatch(args) { @@ -2012,7 +2021,6 @@ fn main() { ["runs", "families"] => runs_families(), ["runs", "family", id] => runs_family(id, None), ["runs", "family", id, "rank", metric] => runs_family(id, Some(metric)), - ["--help"] | ["-h"] => println!("{USAGE}"), _ => { eprintln!("aura: {USAGE}"); std::process::exit(2); diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index b92d485..c155268 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -489,6 +489,12 @@ fn chart_tap_nonexistent_on_run_refuses_with_exit_2() { stderr.contains("run has no tap named 'nope'"), "expected the no-such-tap refusal message, got: {stderr}" ); + // #131: the refusal lists the valid taps so the user can correct the typo + // (the demo run's taps are `equity` and `exposure`). + assert!( + stderr.contains("available:") && stderr.contains("equity") && stderr.contains("exposure"), + "the refusal should enumerate the available taps, got: {stderr}" + ); let _ = std::fs::remove_dir_all(&cwd); } @@ -557,6 +563,36 @@ fn help_flag_prints_usage_to_stdout_and_exits_zero() { ); } +/// Property (#131): `--help`/`-h` is UNIFORM across subcommands — `aura --help` +/// prints usage to stdout and exits 0 for every subcommand, not only the bare `aura +/// --help`. Pre-fix, `aura chart --help` errored with "unexpected chart argument" +/// (exit 2) while `aura run --help` leaked usage to stderr; this pins the uniform +/// success-path help affordance for all subcommands. +#[test] +fn per_subcommand_help_is_uniform_stdout_exit_zero() { + for sub in ["run", "chart", "sweep", "walkforward", "mc", "graph", "runs"] { + for flag in ["--help", "-h"] { + let out = Command::new(BIN).args([sub, flag]).output().expect("spawn aura --help"); + assert_eq!( + out.status.code(), + Some(0), + "`aura {sub} {flag}` should exit 0, got: {:?}", + out.status + ); + let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout"); + assert!( + stdout.contains("usage"), + "`aura {sub} {flag}` should print usage to stdout, got: {stdout:?}" + ); + assert!( + out.stderr.is_empty(), + "`aura {sub} {flag}` should not write to stderr, got: {:?}", + 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