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:
@@ -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