fd303de94d
All 14 process::exit sites in aura-runner (member x8, measure x5,
translate x1) convert to RunnerError { exit_code, message } propagation;
run_signal_r / run_measurement return Result tuples carrying the
skipped-tap names beside the C18 reports (record shapes untouched), and
the CLI owns printing via the shared exit_on_runner_error arm (the
dispatch_reproduce pattern — reproduce now uses the same helper).
cost_knob becomes fallible: the sweep worker maps it into the cell's
MemberFault (closing the one hole in 'a worker never process-exits'),
the persist path keeps its string channel. The skipped-tap note is
CLI-printed from returned data, byte-identical.
The one deliberate behaviour change is the minuted C14 adjudication:
refusals in the content of what argv named (binding, synthetic mismatch,
compile, tap-bind content, exposes-neither) move exit 1 -> 2; the
boundary is FORM vs VALUE — an out-of-domain override value keeps the
runtime class via the panic containment, matching the campaign leg.
Environment/data/IO refusals stay 1 (TapPlanError::exit_class is the
single source). Named re-pins in exec.rs, run_refuses_unrunnable_
blueprint.rs, tap_recording.rs, graph_construct.rs; new library-level
test pins the milestone promise (a refusal returns in-process, the
embedding host survives). Prose is byte-identical everywhere.
Docs in lockstep: C28 deferred block closed (14 sites, hole closed),
C14 partition + form-vs-value boundary, C27 note emission, guide exit
classes. Fork minutes: issues/297#issuecomment-4868 and -4873.
closes #297
118 lines
5.3 KiB
Rust
118 lines
5.3 KiB
Rust
//! `aura run <blueprint.json>` must REFUSE a user-authorable blueprint it
|
|
//! cannot run — a clean `aura:`-prefixed message on stderr and exit 2, the
|
|
//! argv-named blueprint-content class (C14) — never a process panic. Both
|
|
//! cases are user mistakes on the hand-authored single-run path, so each
|
|
//! deserves a diagnosis, not a stack trace.
|
|
//!
|
|
//! Two independent unrunnable shapes, each pinned as its own minimal,
|
|
//! self-contained blueprint (inlined here — no shared fixture file):
|
|
//! 1. a declared tap whose `from` wire references an out-of-range node
|
|
//! index (taps are authored by raw node index, so a miscount is the
|
|
//! expected mistake) — compiles to `CompileError::TapWireOutOfRange`;
|
|
//! 2. an exposed output that is not a `bias` stream (a measurement-only
|
|
//! blueprint) — `wrap_r` cannot weld it to the R-evaluator.
|
|
|
|
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
/// Path to the freshly-built `aura` binary (Cargo sets this env var for the
|
|
/// test crate; the binary is named `aura` in `Cargo.toml`).
|
|
const BIN: &str = env!("CARGO_BIN_EXE_aura");
|
|
|
|
/// A fresh, unique working directory per test (so nothing a run might emit
|
|
/// dirties the repo). Unique per test + per process.
|
|
fn temp_cwd(name: &str) -> std::path::PathBuf {
|
|
let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("aura-cli-refuse-{name}"));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
std::fs::create_dir_all(&dir).expect("create temp cwd");
|
|
dir
|
|
}
|
|
|
|
/// Run `aura run <path>` in an isolated cwd and assert the CLEAN-REFUSAL
|
|
/// contract: exit code 1, an `aura:`-prefixed stderr message, and NO panic —
|
|
/// the property both cases share. `what` names the offending shape for the
|
|
/// failure message.
|
|
fn assert_clean_refusal(bp_json: &str, tag: &str, what: &str) {
|
|
let cwd = temp_cwd(tag);
|
|
let bp_path = cwd.join("blueprint.json");
|
|
std::fs::write(&bp_path, bp_json).expect("write blueprint");
|
|
|
|
let out = Command::new(BIN)
|
|
.args(["exec", bp_path.to_str().expect("utf-8 path")])
|
|
.current_dir(&cwd)
|
|
.output()
|
|
.expect("spawn aura run");
|
|
|
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
|
|
// The defining symptom of the bug: the process panics (Rust's default
|
|
// unwind exits 101 and prints "panicked at ...") instead of refusing.
|
|
assert!(
|
|
!stderr.to_lowercase().contains("panic"),
|
|
"{what}: expected a clean refusal, got a PANIC:\n{stderr}"
|
|
);
|
|
// The clean-refusal register: `aura: <msg>` on stderr — the prefix every
|
|
// refusal class (C14) shares regardless of exit code.
|
|
assert!(
|
|
stderr.contains("aura:"),
|
|
"{what}: refusal must carry the `aura:` prefix:\n{stderr}"
|
|
);
|
|
// C14 class 2: fault in argv-named content (#297) — both callers of this
|
|
// helper name a compile-time / exposed-output content fault.
|
|
assert_eq!(
|
|
out.status.code(),
|
|
Some(2),
|
|
"{what}: a refusal exits 2 (blueprint-content fault, not 101 panic, not 1 env/data fault):\n{stderr}"
|
|
);
|
|
}
|
|
|
|
/// Property: a blueprint whose declared tap wire points at an out-of-range
|
|
/// node index is refused, not crashed. Minimal trigger — a single-node closed
|
|
/// strategy (`price → Bias → "bias"`, so the R-wrap welds cleanly) carrying
|
|
/// one tap whose `from.node` (5) is out of range for a one-node graph. This
|
|
/// compiles to `CompileError::TapWireOutOfRange`, which `run_signal_r`'s
|
|
/// `compile_with_params(...).expect(...)` currently turns into a panic.
|
|
#[test]
|
|
fn run_refuses_a_tap_wire_pointing_out_of_range() {
|
|
let bp = r#"{
|
|
"format_version": 1,
|
|
"blueprint": {
|
|
"name": "bias_with_bad_tap",
|
|
"nodes": [
|
|
{"primitive": {"type": "Bias", "name": "bias",
|
|
"bound": [{"pos": 0, "name": "scale", "kind": "F64", "value": {"F64": 1.0}}]}}
|
|
],
|
|
"edges": [],
|
|
"input_roles": [{"name": "price", "targets": [{"node": 0, "slot": 0}], "source": "F64"}],
|
|
"output": [{"node": 0, "field": 0, "name": "bias"}],
|
|
"taps": [{"name": "bad", "from": {"node": 5, "field": 0}}]
|
|
}
|
|
}"#;
|
|
assert_clean_refusal(bp, "bad-tap-wire", "out-of-range tap wire");
|
|
}
|
|
|
|
/// Property: a blueprint whose single exposed output is not a `bias` stream
|
|
/// (a measurement-only blueprint — here `price → SMA → "measurement"`) is
|
|
/// refused, not crashed. `wrap_r` welds the nested signal by its `"bias"`
|
|
/// output port; a non-bias output makes that weld resolve to
|
|
/// `BuildError::UnknownOutPort`, which `wrap_r`'s `g.build().expect(...)`
|
|
/// currently turns into a panic. (This pins the refusal only — teaching
|
|
/// `aura run` to accept bias-less blueprints is a separate feature.)
|
|
#[test]
|
|
fn run_refuses_a_non_bias_output_blueprint() {
|
|
let bp = r#"{
|
|
"format_version": 1,
|
|
"blueprint": {
|
|
"name": "measurement_only",
|
|
"nodes": [
|
|
{"primitive": {"type": "SMA", "name": "avg",
|
|
"bound": [{"pos": 0, "name": "length", "kind": "I64", "value": {"I64": 2}}]}}
|
|
],
|
|
"edges": [],
|
|
"input_roles": [{"name": "price", "targets": [{"node": 0, "slot": 0}], "source": "F64"}],
|
|
"output": [{"node": 0, "field": 0, "name": "measurement"}]
|
|
}
|
|
}"#;
|
|
assert_clean_refusal(bp, "non-bias-output", "non-bias exposed output");
|
|
}
|