//! 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 ` 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(), ) } 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 `{"": }` 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\": })"), "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 `stage1_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}"); }