4de6d5cbad
The stage1 ordinal has been dead vocabulary since the C10 reframe dropped the two-stage research model: a 1 structurally implies a 2 that no longer exists. The family is renamed by its live discriminator - the R yardstick - with members named by their signal, uniform with their signal-named siblings (sma, macd, momentum): - selectors: stage1-r -> r-sma, stage1-breakout -> r-breakout, stage1-meanrev -> r-meanrev (old tokens are usage errors, exit 2 - no silent alias) - identifiers: Strategy::RSma/RBreakout/RMeanRev, HarnessKind::RSma, r_sma_*/r_breakout_*/r_meanrev_*, wrap_r, run_signal_r, RGrid, R_SMA_* - persisted identity: the sma_signal composite (param prefix sma_signal.fast.length / .slow.length; fixtures regenerated; content-ids shift - no test pins a literal hash, the registry parses no record names) - e2e test files git-mv'd to r_sma_e2e / r_breakout_e2e / r_meanrev_e2e - dead Stage-1/Stage-2 prose reworded to post-C10 vocabulary across rustdoc, Cargo.tomls, ledger live lines, and the glossary (historical entries stay; fieldtests corpus untouched) - CLAUDE.md invariant 7 rewritten to record the ratified C10 reframe faithfully (the token-swap alone would have laundered the retired gated-currency/realistic-broker design into unmarked live prose); the unbacked account-mode clause dropped Verification: cargo build/test --workspace green (51 targets), clippy -D warnings clean, doc build clean, acceptance grep gate leaves exactly the one resampling-stage false positive (harness.rs), smoke: --harness r-sma runs, --harness stage1-r exits 2 with the new usage line. Decision log: forks and rationale recorded on the issue (reconciliation + implementation-phase comments). closes #174
299 lines
15 KiB
Rust
299 lines
15 KiB
Rust
//! End-to-end coverage for the `aura graph build` / `aura graph introspect`
|
|
//! op-script CLI (#157, §C): drives the built `aura` binary over stdin/argv,
|
|
//! the exact surface a data-level author uses.
|
|
|
|
use std::io::Write;
|
|
use std::process::{Command, Stdio};
|
|
|
|
const BIN: &str = env!("CARGO_BIN_EXE_aura");
|
|
|
|
/// Run `aura <args>` with `stdin_doc` piped in; return (stdout, stderr, success).
|
|
fn run(args: &[&str], stdin_doc: &str) -> (String, String, bool) {
|
|
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.stdout).into_owned(),
|
|
String::from_utf8_lossy(&out.stderr).into_owned(),
|
|
out.status.success(),
|
|
)
|
|
}
|
|
|
|
/// 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}}},
|
|
{"op":"add","type":"SMA","name":"slow","bind":{"length":{"I64":4}}},
|
|
{"op":"add","type":"Sub"},
|
|
{"op":"add","type":"Bias"},
|
|
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
|
|
{"op":"connect","from":"fast.value","to":"sub.lhs"},
|
|
{"op":"connect","from":"slow.value","to":"sub.rhs"},
|
|
{"op":"connect","from":"sub.value","to":"bias.signal"},
|
|
{"op":"expose","from":"bias.bias","as":"bias"}
|
|
]"#;
|
|
|
|
/// Every op below applies cleanly (all eager checks pass), but `sub.rhs` is
|
|
/// never wired — a 0-cover input slot only the holistic finalize gate can
|
|
/// reject. The smallest document that is op-valid yet whole-document-invalid.
|
|
const UNCONNECTED_DOC: &str = r#"[
|
|
{"op":"source","role":"price","kind":"F64"},
|
|
{"op":"add","type":"SMA","name":"fast"},
|
|
{"op":"add","type":"SMA","name":"slow"},
|
|
{"op":"add","type":"Sub"},
|
|
{"op":"feed","role":"price","into":["fast.series","slow.series"]},
|
|
{"op":"connect","from":"fast.value","to":"sub.lhs"}
|
|
]"#;
|
|
|
|
#[test]
|
|
fn graph_build_emits_blueprint_for_a_valid_document() {
|
|
let (stdout, _stderr, ok) = run(&["graph", "build"], SIGNAL_DOC);
|
|
assert!(ok, "exit success");
|
|
assert!(stdout.contains("\"format_version\":1"), "emits the #155 envelope: {stdout}");
|
|
assert!(stdout.contains("\"SMA\""), "carries the nodes: {stdout}");
|
|
}
|
|
|
|
/// The canonical blueprint artifact carries no trailing newline: `aura graph build`
|
|
/// emits exactly the library `blueprint_to_json` bytes (the form #158 content-addresses),
|
|
/// not a display-framed line. Byte-canonical across the CLI and library surfaces (#164).
|
|
#[test]
|
|
fn graph_build_emit_is_byte_canonical_no_trailing_newline() {
|
|
let (stdout, _stderr, ok) = run(&["graph", "build"], SIGNAL_DOC);
|
|
assert!(ok, "exit success");
|
|
assert!(!stdout.ends_with('\n'), "no trailing newline on the canonical emit: {stdout:?}");
|
|
assert!(stdout.contains("\"format_version\":1"), "still the #155 envelope: {stdout}");
|
|
}
|
|
|
|
#[test]
|
|
fn graph_build_fails_fast_at_the_offending_op() {
|
|
let doc = r#"[{"op":"add","type":"SMA","name":"fast"},{"op":"add","type":"Nope"}]"#;
|
|
let (stdout, stderr, ok) = run(&["graph", "build"], doc);
|
|
assert!(!ok, "non-zero exit");
|
|
assert!(stdout.is_empty(), "no blueprint emitted on error: {stdout}");
|
|
assert!(stderr.contains("op 1 (add)"), "names the failing op: {stderr}");
|
|
assert!(stderr.contains("Nope"), "names the cause: {stderr}");
|
|
}
|
|
|
|
#[test]
|
|
fn graph_introspect_vocabulary_lists_the_node_types() {
|
|
let (stdout, _stderr, ok) = run(&["graph", "introspect", "--vocabulary"], "");
|
|
assert!(ok, "exit success");
|
|
assert!(stdout.contains("SMA"), "lists SMA: {stdout}");
|
|
assert!(stdout.contains("Bias"), "lists Bias: {stdout}");
|
|
assert!(!stdout.contains("Recorder"), "a sink is not in the zero-arg vocabulary: {stdout}");
|
|
}
|
|
|
|
#[test]
|
|
fn graph_introspect_node_answers_ports_without_a_build() {
|
|
let (stdout, _stderr, ok) = run(&["graph", "introspect", "--node", "SMA"], "");
|
|
assert!(ok, "exit success");
|
|
assert!(stdout.contains("series"), "lists the input port: {stdout}");
|
|
assert!(stdout.contains("value"), "lists the output field: {stdout}");
|
|
}
|
|
|
|
#[test]
|
|
fn graph_introspect_unwired_reports_open_slots() {
|
|
let doc = r#"[
|
|
{"op":"add","type":"SMA","name":"fast"},
|
|
{"op":"add","type":"Sub"},
|
|
{"op":"connect","from":"fast.value","to":"sub.lhs"}
|
|
]"#;
|
|
let (stdout, _stderr, ok) = run(&["graph", "introspect", "--unwired"], doc);
|
|
assert!(ok, "exit success");
|
|
assert!(stdout.contains("sub.rhs"), "sub.rhs still open: {stdout}");
|
|
assert!(!stdout.contains("sub.lhs"), "sub.lhs is covered: {stdout}");
|
|
}
|
|
|
|
/// Property (holistic finalize path, binary contract): a fault only decidable
|
|
/// at end-of-document (a 0-covered interior slot) makes `aura graph build` exit
|
|
/// non-zero, route the cause to *stderr* as a `finalize:` fault (NOT attributed
|
|
/// to any op index), and emit nothing on stdout. The eager fail-fast path (above)
|
|
/// already exercises a per-op fault; this pins the iteration's eager/holistic
|
|
/// gate split as the binary's observable exit-code + stream-routing contract,
|
|
/// not merely the engine `replay` return value.
|
|
#[test]
|
|
fn graph_build_reports_a_holistic_fault_at_finalize() {
|
|
let (stdout, stderr, ok) = run(&["graph", "build"], UNCONNECTED_DOC);
|
|
assert!(!ok, "non-zero exit on a finalize fault");
|
|
assert!(stdout.is_empty(), "no blueprint emitted on a fault: {stdout}");
|
|
assert!(stderr.contains("finalize:"), "attributes the fault to finalize, not an op: {stderr}");
|
|
assert!(stderr.contains("finalize: slot") && stderr.contains("is unconnected"),
|
|
"the holistic fault renders by-identifier, got: {stderr}");
|
|
}
|
|
|
|
/// Property (determinism, domain invariant 1, at the CLI seam): the same op-list
|
|
/// document piped to `aura graph build` twice yields byte-identical blueprint
|
|
/// JSON on stdout. The #155 canonical form has no run-to-run ordering or
|
|
/// formatting jitter — a precondition for content-addressing a blueprint (#158).
|
|
/// The non-empty guard rules out two equal-but-empty error paths masquerading as
|
|
/// agreement.
|
|
#[test]
|
|
fn graph_build_is_byte_identical_across_runs() {
|
|
let (first, _e1, ok1) = run(&["graph", "build"], SIGNAL_DOC);
|
|
let (second, _e2, ok2) = run(&["graph", "build"], SIGNAL_DOC);
|
|
assert!(ok1 && ok2, "both runs succeed");
|
|
assert!(!first.is_empty(), "non-empty blueprint output");
|
|
assert_eq!(first, second, "byte-identical blueprint across runs");
|
|
}
|
|
|
|
/// Property (read-only query, clean rejection): `aura graph introspect --node`
|
|
/// with a type outside the closed vocabulary exits non-zero, names the offending
|
|
/// type on stderr, and prints no partial port listing on stdout. The introspect
|
|
/// surface's only negative-path E2E — it fails cleanly rather than half-answering.
|
|
#[test]
|
|
fn graph_introspect_node_rejects_an_unknown_type() {
|
|
let (stdout, stderr, ok) = run(&["graph", "introspect", "--node", "Bogus"], "");
|
|
assert!(!ok, "non-zero exit for an unknown type");
|
|
assert!(stdout.is_empty(), "no partial port listing emitted: {stdout}");
|
|
assert!(stderr.contains("Bogus"), "names the offending type: {stderr}");
|
|
}
|
|
|
|
/// Property (#162 item 2, bind-mismatch prose at the binary seam): an `aura graph
|
|
/// build` whose bind value-kind disagrees with the param's declared kind exits
|
|
/// non-zero and writes the cause as prose to stderr — naming the param path and
|
|
/// both the expected and actual kind — and never leaks the raw `KindMismatch`
|
|
/// Debug struct the pre-#162 `{err:?}` presenter exposed. SMA's `length` is i64,
|
|
/// so binding an `F64` is the smallest kind disagreement. Pins the user-facing
|
|
/// bind-error ergonomics at the binary, not merely the in-process presenter.
|
|
#[test]
|
|
fn graph_build_renders_a_bind_kind_mismatch_as_prose() {
|
|
let doc = r#"[{"op":"add","type":"SMA","name":"fast","bind":{"length":{"F64":2.0}}}]"#;
|
|
let (stdout, stderr, ok) = run(&["graph", "build"], doc);
|
|
assert!(!ok, "non-zero exit on a bad bind");
|
|
assert!(stdout.is_empty(), "no blueprint emitted on a fault: {stdout}");
|
|
assert!(stderr.contains("param fast.length expects I64 but got F64"),
|
|
"phrases the bind mismatch as prose: {stderr}");
|
|
assert!(!stderr.contains("KindMismatch"),
|
|
"does not leak the Debug struct name: {stderr}");
|
|
}
|
|
|
|
/// Property (#162 item 3, discoverable bind form at the binary seam): `aura graph
|
|
/// introspect --node` annotates each param with its typed-Scalar bind wrapper, so
|
|
/// a hand-author learns the `{"<Kind>": <v>}` value form from the CLI alone
|
|
/// without reading source. SMA's `length` is i64, so the shown form is the `I64`
|
|
/// wrapper.
|
|
#[test]
|
|
fn graph_introspect_node_shows_the_typed_bind_form() {
|
|
let (stdout, _stderr, ok) = run(&["graph", "introspect", "--node", "SMA"], "");
|
|
assert!(ok, "exit success");
|
|
assert!(stdout.contains("(bind {\"I64\": <v>})"),
|
|
"shows the typed-Scalar bind-value form for the i64 param: {stdout}");
|
|
}
|
|
|
|
/// Property (#162 item 1, by-identifier finalize fault at the binary seam, a
|
|
/// non-unconnected variant): a fault decidable only at finalize that is NOT a
|
|
/// 0-covered slot — here one source role fanning into two slots of differing
|
|
/// scalar kind (an `f64` `Sub.lhs` and a `bool` `And.a`); `feed` runs no eager
|
|
/// kind check, so the disagreement surfaces only at the holistic finalize gate —
|
|
/// renders by *identifier*: `aura graph build` exits non-zero, routes a
|
|
/// `finalize:` fault to stderr, and names the offending role ("price") in prose,
|
|
/// never a raw machine role index. The companion unconnected-slot test pins one
|
|
/// finalize variant; this pins that the by-identifier translation generalises
|
|
/// across finalize variants.
|
|
#[test]
|
|
fn graph_build_renders_role_kind_mismatch_at_finalize_by_identifier() {
|
|
let doc = r#"[
|
|
{"op":"source","role":"price","kind":"F64"},
|
|
{"op":"add","type":"Sub","name":"sub"},
|
|
{"op":"add","type":"And","name":"conj"},
|
|
{"op":"feed","role":"price","into":["sub.lhs","conj.a"]}
|
|
]"#;
|
|
let (stdout, stderr, ok) = run(&["graph", "build"], doc);
|
|
assert!(!ok, "non-zero exit on a finalize fault");
|
|
assert!(stdout.is_empty(), "no blueprint emitted on a fault: {stdout}");
|
|
assert!(stderr.contains("finalize:"), "attributes the fault to finalize, not an op: {stderr}");
|
|
assert!(stderr.contains("input role price fans into slots of differing kinds"),
|
|
"names the offending role by-identifier in prose: {stderr}");
|
|
assert!(!stderr.contains("RoleKindMismatch"),
|
|
"does not leak the Debug variant name: {stderr}");
|
|
}
|
|
|
|
/// Property (#158 acc 1, the content-id surface): `aura graph introspect --content-id`
|
|
/// prints the 64-hex SHA256 of the op-script's canonical blueprint — deterministic across
|
|
/// runs and distinct for a structurally different topology. It is the same hash `aura run`
|
|
/// stamps as `topology_hash` (both via the one `content_id` primitive over the canonical
|
|
/// `blueprint_to_json` bytes), so a data-authored topology has a stable, comparable content
|
|
/// id. (Note: an op-script and the Rust `sma_signal` builder produce *different*
|
|
/// canonical forms — composite debug-name, an unbound `Bias.scale` — so they are different
|
|
/// topologies by the byte definition; content-addressing keys on the canonical form.)
|
|
#[test]
|
|
fn graph_introspect_content_id_is_deterministic_and_distinguishes() {
|
|
let (a, _e, ok) = run(&["graph", "introspect", "--content-id"], SIGNAL_DOC);
|
|
assert!(ok, "exit success");
|
|
let id = a.trim();
|
|
assert_eq!(id.len(), 64, "a 64-hex SHA256 content id: {id:?}");
|
|
assert!(id.chars().all(|c| c.is_ascii_hexdigit()), "hex only: {id:?}");
|
|
|
|
// deterministic: the same op-script yields the same content id.
|
|
let (b, _e2, ok2) = run(&["graph", "introspect", "--content-id"], SIGNAL_DOC);
|
|
assert!(ok2, "exit success");
|
|
assert_eq!(id, b.trim(), "same op-script -> same content id");
|
|
|
|
// distinguishes: a structurally different topology (slow length 4 -> 5) differs.
|
|
let other = SIGNAL_DOC.replace("\"I64\":4", "\"I64\":5");
|
|
let (c, _e3, ok3) = run(&["graph", "introspect", "--content-id"], &other);
|
|
assert!(ok3, "exit success");
|
|
assert_ne!(id, c.trim(), "a different topology -> a different content id");
|
|
}
|
|
|
|
/// Property (negative): `aura graph introspect --content-id` on a malformed op-list exits
|
|
/// non-zero with the cause on stderr and prints no content id — it fails cleanly, never a
|
|
/// partial/garbage hash.
|
|
#[test]
|
|
fn graph_introspect_content_id_rejects_a_bad_document() {
|
|
let (stdout, stderr, ok) = run(&["graph", "introspect", "--content-id"], r#"[{"op":"add","type":"Nope"}]"#);
|
|
assert!(!ok, "non-zero exit on a bad op-list");
|
|
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}");
|
|
}
|