feat(0098): CLI clap migration (iteration 1) — scoped help, --version, GNU flags
Replace the hand-rolled aura-cli argv parser with a clap derive parser. Delivers, all from one declarative source: - scoped `aura <sub> --help` (stdout, exit 0) — each subcommand's own Options section; retires the #131 uniform-global-blob help - `--version` / `-V` → `aura 0.1.0` (stdout, exit 0) - GNU `--flag=value`, the `--` end-of-options terminator, and long-option abbreviation (`--harn` → `--harness`) Architecture: one root Cli/Command enum, one `*Cmd` struct per subcommand. The four dual-grammar subcommands (run/sweep/walkforward/mc) map the loaded-blueprint branch as an optional `[blueprint]` positional + a post-parse `is_file()` dispatch on a single-sourced predicate; each built-in handler re-asserts a stray-positional guard (refuse-don't-guess) so clap's optional positional cannot silently swallow a typo (pinned by two new E2E tests). The execution layer (run_*/emit_*/runs_*/reproduce_*) is untouched — only arg-plumbing changes, via thin `*_from` adapters reusing the existing value helpers. Exit codes behaviour-preserved: clap parse errors exit 2 (matching the old usage-error=2); domain refusals stay exit 2; reproduce-diverged stays exit 1. The exit-code split (2=usage / 1=runtime) is iteration 2. clap is admitted under the C16 per-case review (research-side CLI, invariant 8 untouched — a dev-loop compile tax, not a frozen-artifact tax). Forced plan corrections (implementer; verified against the diff): - Step 6's deletion of the `parse_*_args` fns orphaned ~34 grammar unit tests that called them; those were deleted and that grammar coverage moved to the renegotiated cli_run.rs E2E pins (`parse_select` kept + retested). - run_malformed_cost_value_...: a bad cost value is now a clap value-parse error (no "Usage:" line); renegotiated to pin exit 2 + empty stdout + the flag name "--cost-per-trade"; the units note stays pinned via `run --help`. - `allow_hyphen_values` on the three cost flags so `--cost-per-trade -0.5` reaches the non-negativity guard (else clap rejects -0.5 as an unknown option). - Deleted the now-dead `RealWindowGrammar` (clippy -D warnings). Orchestrator additions after the loop: - Enabled long-option abbreviation (`infer_long_args` on the root — one attribute, it propagates to subcommands) + a test, closing spec acceptance criterion 4 which the plan's RED tests did not cover. Decided to deliver rather than amend the spec: the two ratified decisions conflict on this low-tier item — Fork B (GNU compliance, getopt_long abbreviates) vs F5 (LLM/automation-first, human-only niceties deprioritized) — and the cost tiebreaker (one attribute, not per-struct work) resolves to deliver. Carry-on debt (not this iteration): the error-message "Usage:"/"usage:"/bare casing is now mixed (clap's capitalized "Usage:" beside preserved lowercase aura messages); a deliberate quality-hold since the casings are pinned by renegotiated/preserved tests. Cosmetic; a candidate for the iteration-2 exit-code-split cleanup. 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, including the renegotiated cli_run pins and the new --version / scoped-help / GNU-flag / dual-grammar tests). refs #175
This commit is contained in:
Generated
+136
@@ -37,6 +37,56 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
@@ -65,6 +115,7 @@ dependencies = [
|
||||
"aura-ingest",
|
||||
"aura-registry",
|
||||
"aura-std",
|
||||
"clap",
|
||||
"data-server",
|
||||
"libc",
|
||||
"serde",
|
||||
@@ -231,6 +282,52 @@ dependencies = [
|
||||
"inout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
@@ -427,6 +524,12 @@ version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
@@ -479,6 +582,12 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
@@ -577,6 +686,12 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
@@ -771,6 +886,12 @@ version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
@@ -839,6 +960,12 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@@ -958,6 +1085,15 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
|
||||
@@ -40,3 +40,9 @@ libc = "0.2"
|
||||
# SHA256 user-settled); lives in the research-side CLI, off the frozen engine
|
||||
# (invariant 8).
|
||||
sha2 = "0.10"
|
||||
# clap: the vetted standard argument parser. Adopted (C16 per-case review) to
|
||||
# replace the hand-rolled argv parser + ten duplicated help surfaces with one
|
||||
# declarative source, giving scoped --help, --version, --flag=value, and
|
||||
# long-option abbreviation. Research-side CLI only (invariant 8): a dev-loop
|
||||
# compile tax, never a frozen-artifact tax.
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
@@ -178,61 +178,64 @@ pub fn introspect_unwired(doc: &str) -> Result<String, String> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// `aura graph introspect`: dispatch the three read-only queries.
|
||||
pub fn introspect_cmd(rest: &[&str]) {
|
||||
match rest {
|
||||
["--vocabulary"] => {
|
||||
for t in std_vocabulary_types() {
|
||||
println!("{t}");
|
||||
}
|
||||
/// `aura graph introspect`: dispatch the read-only queries. Exactly one of
|
||||
/// `--vocabulary` / `--node <T>` / `--unwired` / `--content-id` must be set;
|
||||
/// zero or more than one is the usage error (exit 2).
|
||||
pub fn introspect_cmd(cmd: crate::GraphIntrospectCmd) {
|
||||
let count = cmd.vocabulary as usize
|
||||
+ cmd.node.is_some() as usize
|
||||
+ cmd.unwired as usize
|
||||
+ cmd.content_id as usize;
|
||||
if count != 1 {
|
||||
eprintln!(
|
||||
"aura: usage: aura graph introspect --vocabulary | --node <T> | --unwired | --content-id"
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
if cmd.vocabulary {
|
||||
for t in std_vocabulary_types() {
|
||||
println!("{t}");
|
||||
}
|
||||
["--node", type_id] => match introspect_node(type_id) {
|
||||
} else if let Some(type_id) = cmd.node.as_deref() {
|
||||
match introspect_node(type_id) {
|
||||
Ok(s) => print!("{s}"),
|
||||
Err(m) => {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
},
|
||||
["--unwired"] => {
|
||||
use std::io::Read;
|
||||
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);
|
||||
}
|
||||
match introspect_unwired(&doc) {
|
||||
Ok(s) => print!("{s}"),
|
||||
Err(m) => {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
["--content-id"] => {
|
||||
// the content id (#158) of the op-list's canonical blueprint: SHA256 (hex)
|
||||
// of the same `blueprint_to_json` bytes `graph build` emits, via the one
|
||||
// shared `crate::content_id` primitive `topology_hash` also uses — so this
|
||||
// surface and hashing a `graph build` output agree by construction.
|
||||
use std::io::Read;
|
||||
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);
|
||||
}
|
||||
match build_from_str(&doc) {
|
||||
Ok(json) => println!("{}", crate::content_id(&json)),
|
||||
Err(m) => {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
eprintln!(
|
||||
"aura: usage: aura graph introspect --vocabulary | --node <T> | --unwired | --content-id"
|
||||
);
|
||||
} else if cmd.unwired {
|
||||
use std::io::Read;
|
||||
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);
|
||||
}
|
||||
match introspect_unwired(&doc) {
|
||||
Ok(s) => print!("{s}"),
|
||||
Err(m) => {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// --content-id: the content id (#158) of the op-list's canonical blueprint:
|
||||
// SHA256 (hex) of the same `blueprint_to_json` bytes `graph build` emits, via
|
||||
// the one shared `crate::content_id` primitive `topology_hash` also uses — so
|
||||
// this surface and hashing a `graph build` output agree by construction.
|
||||
use std::io::Read;
|
||||
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);
|
||||
}
|
||||
match build_from_str(&doc) {
|
||||
Ok(json) => println!("{}", crate::content_id(&json)),
|
||||
Err(m) => {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+910
-1364
File diff suppressed because it is too large
Load Diff
@@ -113,7 +113,7 @@ fn run_with_trailing_token_is_strict_and_exits_two() {
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
);
|
||||
let stderr = String::from_utf8(out.stderr).expect("utf-8 stderr");
|
||||
assert!(stderr.contains("usage"), "stderr was: {stderr:?}");
|
||||
assert!(stderr.contains("Usage"), "stderr was: {stderr:?}");
|
||||
|
||||
// Positive-preservation: bare `aura run` still runs and prints the report.
|
||||
let ok = Command::new(BIN).arg("run").output().expect("spawn aura run");
|
||||
@@ -349,6 +349,43 @@ fn aura_run_rejects_an_unknown_node_blueprint() {
|
||||
assert!(stderr.contains("UnknownNodeType"), "names the cause: {stderr}");
|
||||
}
|
||||
|
||||
/// Property (#175, refuse-don't-guess): built-in-only flags on `aura run
|
||||
/// <blueprint.json>` 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` and the cost flags structurally parseable, so the
|
||||
/// dispatch guard must re-reject them — 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.
|
||||
#[test]
|
||||
fn run_blueprint_rejects_builtin_only_flags_exit_two() {
|
||||
for extra in [
|
||||
&["--trace", "foo"][..],
|
||||
&["--cost-per-trade", "1"][..],
|
||||
&["--slip-vol-mult", "1"][..],
|
||||
&["--carry-per-cycle", "1"][..],
|
||||
] {
|
||||
let mut args = vec!["run", "tests/fixtures/stage1_signal.json"];
|
||||
args.extend_from_slice(extra);
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_trace_persists_taps_and_plain_run_writes_no_traces() {
|
||||
let cwd = temp_cwd("run-trace");
|
||||
@@ -629,7 +666,7 @@ fn no_args_prints_usage_and_exits_two() {
|
||||
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:?}");
|
||||
assert!(stderr.contains("Usage"), "stderr was: {stderr:?}");
|
||||
}
|
||||
|
||||
/// Property: `--help`/`-h` is the success-path help affordance, not the error
|
||||
@@ -664,32 +701,21 @@ fn help_flag_prints_usage_to_stdout_and_exits_zero() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#131): `--help`/`-h` is UNIFORM across subcommands — `aura <sub> --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.
|
||||
/// Property (post-clap migration, #175): subcommand help is SCOPED — `aura <sub>
|
||||
/// --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_uniform_stdout_exit_zero() {
|
||||
fn per_subcommand_help_is_scoped_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 <sub> --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)
|
||||
);
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -703,17 +729,20 @@ fn run_help_surfaces_cost_flag_units_note() {
|
||||
let out = Command::new(BIN).args(["run", "--help"]).output().expect("spawn aura run --help");
|
||||
assert_eq!(out.status.code(), Some(0), "run --help exits 0: {:?}", out.status);
|
||||
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
|
||||
assert!(stdout.contains("per ENGINE cycle"), "help must explain carry units: {stdout}");
|
||||
assert!(stdout.contains("stage1-r only"), "help must state the R-harness requirement: {stdout}");
|
||||
// clap wraps `long_help` at the render width, which can split a multi-word phrase
|
||||
// across lines; pin the single tokens clap never hyphenates instead of the
|
||||
// multi-word "per ENGINE cycle".
|
||||
assert!(stdout.contains("ENGINE"), "help must explain carry units: {stdout}");
|
||||
assert!(stdout.contains("cycle"), "help must explain the per-cycle carry scale: {stdout}");
|
||||
assert!(stdout.contains("stage1-r"), "help must state the R-harness requirement: {stdout}");
|
||||
}
|
||||
|
||||
/// Property (#153): the cost-flag note reaches the user through the *grammar-error*
|
||||
/// path too, not only `--help`. A malformed cost value (`--cost-per-trade x`) is a
|
||||
/// usage error → exit 2 with the note on STDERR (nothing on stdout). The note is
|
||||
/// appended at two independent source sites (the `USAGE` const for `--help` and the
|
||||
/// `parse_run_args` usage closure for grammar errors); the help test pins the first,
|
||||
/// this pins the second — the more frequently hit surface — at the binary boundary,
|
||||
/// where the closure's String must actually route to stderr+exit-2.
|
||||
/// Property (#153/#175): a malformed cost value (`--cost-per-trade x`, not a f64) is
|
||||
/// a usage error — exit 2 with a Usage line on STDERR and nothing on stdout. Post-clap
|
||||
/// migration the terse parse error names the offending flag and points to `--help`
|
||||
/// (where the units note lives, pinned by `run_help_surfaces_cost_flag_units_note`);
|
||||
/// clap does not inline `long_help` into a parse error, so the multi-word units note
|
||||
/// no longer rides the grammar-error path — the pin is the exit code + Usage surface.
|
||||
#[test]
|
||||
fn run_malformed_cost_value_usage_error_carries_note_on_stderr() {
|
||||
let out = Command::new(BIN)
|
||||
@@ -724,8 +753,7 @@ fn run_malformed_cost_value_usage_error_carries_note_on_stderr() {
|
||||
String::from_utf8_lossy(&out.stderr));
|
||||
assert!(out.stdout.is_empty(), "a usage error must not emit a report on stdout: {:?}", out.stdout);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(stderr.contains("usage"), "the grammar-error path stays a usage message: {stderr:?}");
|
||||
assert!(stderr.contains("per ENGINE cycle"), "the grammar-error usage must carry the cost-flag note: {stderr:?}");
|
||||
assert!(stderr.contains("--cost-per-trade"), "the parse error names the offending flag: {stderr:?}");
|
||||
}
|
||||
|
||||
/// Property: `aura graph` emits a single self-contained HTML page — the
|
||||
@@ -3854,3 +3882,140 @@ fn aura_sweep_list_axes_names_round_trip_into_a_working_sweep() {
|
||||
|
||||
let _ = std::fs::remove_dir_all(&cwd);
|
||||
}
|
||||
|
||||
/// `--version` / `-V` surface the crate version on stdout and exit 0 (GNU MUST):
|
||||
/// a version query is a successful query, not a usage error. The expected string
|
||||
/// is the crate version itself (`CARGO_PKG_VERSION`, i.e. the workspace version),
|
||||
/// so this stays honest across version bumps and keeps one source of truth.
|
||||
#[test]
|
||||
fn version_flag_prints_version_to_stdout_and_exits_zero() {
|
||||
for flag in ["--version", "-V"] {
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.arg(flag)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
assert_eq!(out.status.code(), Some(0), "{flag}: exit {:?}", out.status);
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(stdout.contains(env!("CARGO_PKG_VERSION")), "{flag} stdout: {stdout:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Subcommand help is SCOPED: `aura sweep --help` names sweep's own flags and is
|
||||
/// NOT byte-identical to `aura run --help` — each subcommand documents its own
|
||||
/// options rather than reprinting one shared global blob (the #131 uniform-help
|
||||
/// surface retired).
|
||||
#[test]
|
||||
fn subcommand_help_is_scoped_not_uniform() {
|
||||
let sweep = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["sweep", "--help"]).output().expect("spawn");
|
||||
let run = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["run", "--help"]).output().expect("spawn");
|
||||
assert_eq!(sweep.status.code(), Some(0), "sweep --help exit");
|
||||
assert_eq!(run.status.code(), Some(0), "run --help exit");
|
||||
let sweep_out = String::from_utf8_lossy(&sweep.stdout);
|
||||
let run_out = String::from_utf8_lossy(&run.stdout);
|
||||
assert!(sweep_out.contains("--strategy"), "sweep help names its flags: {sweep_out:?}");
|
||||
assert_ne!(sweep_out, run_out, "per-subcommand help must be scoped, not uniform");
|
||||
}
|
||||
|
||||
/// The GNU `--flag=value` equals form is accepted, equivalent to the
|
||||
/// space-separated `--flag value`.
|
||||
#[test]
|
||||
fn gnu_equals_form_is_accepted() {
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["run", "--harness=sma"]).output().expect("spawn");
|
||||
assert_eq!(out.status.code(), Some(0),
|
||||
"run --harness=sma should run; stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
}
|
||||
|
||||
/// The `--` end-of-options terminator is recognized (a bare `--` after the flags
|
||||
/// is a no-op that still runs the default harness).
|
||||
#[test]
|
||||
fn double_dash_terminator_is_recognized() {
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["run", "--harness", "sma", "--"]).output().expect("spawn");
|
||||
assert_eq!(out.status.code(), Some(0),
|
||||
"run ... -- should run; stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
}
|
||||
|
||||
/// GNU long-option abbreviation (spec acceptance criterion 4): an unambiguous prefix
|
||||
/// of a long flag is accepted (`--harn` → `--harness`), via clap `infer_long_args`
|
||||
/// on the root command (which propagates to subcommands). An LLM/automation caller
|
||||
/// always writes the full flag, but GNU compliance (the cycle's ratified purpose)
|
||||
/// includes abbreviation, and clap delivers it with one root attribute.
|
||||
#[test]
|
||||
fn long_option_abbreviation_is_accepted() {
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["run", "--harn", "sma"]).output().expect("spawn");
|
||||
assert_eq!(out.status.code(), Some(0),
|
||||
"run --harn sma (abbrev of --harness) should run; stderr: {}",
|
||||
String::from_utf8_lossy(&out.stderr));
|
||||
}
|
||||
|
||||
/// Property (#175, dual-grammar built-in branch stays strict): a leading positional
|
||||
/// that is NOT an existing `.json` file is a usage error (exit 2), never silently
|
||||
/// swallowed — for every dual-grammar subcommand (run/sweep/walkforward/mc). The
|
||||
/// clap migration models the loaded-blueprint `[BLUEPRINT]` as an OPTIONAL positional
|
||||
/// on one shared args struct; that makes a typo'd leading token (`aura run bogus`,
|
||||
/// meant as `--harness bogus`) structurally parseable where the old hand-parser
|
||||
/// rejected any stray token. Each built-in handler therefore re-asserts the guard
|
||||
/// (`if a.blueprint.is_some() { usage; exit 2 }`). Dropping any one guard would
|
||||
/// silently run the default harness on a mistyped invocation — the exact regression
|
||||
/// clap's optional positional invites and this pins shut. Exit 2 preserved (the
|
||||
/// usage/runtime split is iteration 2); nothing on stdout on the refusal path.
|
||||
#[test]
|
||||
fn dual_grammar_stray_positional_is_a_usage_error_not_swallowed() {
|
||||
// A trailing flag is added where the built-in grammar needs one to reach the
|
||||
// guard rather than a bare-run default; `bogus` never ends in `.json`, so the
|
||||
// is_file() discriminator sends every case down the built-in branch.
|
||||
for argv in [
|
||||
&["run", "bogus"][..],
|
||||
&["sweep", "bogus", "--fast", "3"][..],
|
||||
&["walkforward", "bogus", "--strategy", "sma"][..],
|
||||
&["mc", "bogus"][..],
|
||||
] {
|
||||
let out = Command::new(BIN).args(argv).output().expect("spawn aura <sub> <stray-positional>");
|
||||
assert_eq!(
|
||||
out.status.code(),
|
||||
Some(2),
|
||||
"{argv:?} must be refused (exit 2), got {:?}; stderr: {}",
|
||||
out.status,
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(
|
||||
out.stdout.is_empty(),
|
||||
"{argv:?} must not emit a report on stdout: {:?}",
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Property (#175, dual-grammar discriminator keys on `is_file()`, not the suffix):
|
||||
/// `aura run <name>.json` where the file does NOT exist is treated as the built-in
|
||||
/// grammar with a stray positional (refused, exit 2, built-in usage naming
|
||||
/// `--harness`), NOT as a loaded blueprint. The discriminator is
|
||||
/// `ends_with(".json") && Path::is_file()`; a regression that dropped the `is_file()`
|
||||
/// conjunct (suffix-only) would send this down the blueprint-read branch and fail
|
||||
/// with a file-read error instead — a different, misleading refusal. The
|
||||
/// `--harness`-in-stderr assertion pins that the built-in branch was chosen; the
|
||||
/// positive `.json`-file path stays covered by `aura_run_loads_and_runs_a_blueprint_file`.
|
||||
#[test]
|
||||
fn dual_grammar_discriminator_requires_an_existing_file_not_just_json_suffix() {
|
||||
let out = Command::new(BIN)
|
||||
.args(["run", "definitely-not-a-real-file.json"])
|
||||
.output()
|
||||
.expect("spawn aura run <nonexistent>.json");
|
||||
assert_eq!(
|
||||
out.status.code(),
|
||||
Some(2),
|
||||
"a .json name that is not a file must be refused (exit 2), got {:?}; stderr: {}",
|
||||
out.status,
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(out.stdout.is_empty(), "no report on stdout: {:?}", String::from_utf8_lossy(&out.stdout));
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("--harness"),
|
||||
"the built-in run grammar (not the blueprint-read path) must handle it: {stderr:?}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user