25e452aaf7
Iteration-2 §C of the construction service (#157), the CLI shell over the iteration-1 engine core (ea1ca32+27ac4dc): a declarative, replayable JSON op-list document drives the engine's per-op-fallible construction surface. - graph_construct.rs (new, aura-cli): an `OpDoc` serde DTO (`#[serde(tag="op", rename_all="lowercase")]` + field renames type/as) deserializes the document and maps 1:1 into the serde-free engine `Op` — the wire format is a CLI concern, the engine owns no second representation. Bind values use the typed Scalar form (`{"I64":2}`), kinds the capitalized #155 form (`"F64"`). - `aura graph build`: read the op-list from stdin, replay through std_vocabulary, emit the #155 blueprint JSON on success; on the first failing op print `op N (kind): cause` to stderr (the op-kind recovered from the retained parsed list) / `finalize: cause` for a holistic fault, and exit non-zero. A CLI-side format_op_error phrases each OpError (the engine error types stay Display-free). - `aura graph introspect`: `--vocabulary` (std_vocabulary_types), `--node <T>` (a type's ports/kinds + param paths off the pre-build schema), `--unwired` (a partial document's open slots via GraphSession::unwired) — all build-free. - Two new argv arms over the flat match; the bare `["graph"]` HTML-render arm (sample_blueprint, #159's concern) is left intact. #157 acceptance now demonstrated through the CLI (E2E in tests/graph_construct.rs, 8 tests): a document builds to the #155 blueprint that compiles identical to its Rust-built twin (C1); an invalid op is rejected at the op, named; introspection answers vocabulary / ports+kinds / unwired slots without a build. Full suite green (51 suites); clippy --all-targets -D warnings clean. The JSON op-list reuses #155's Scalar/ScalarKind serde shapes; full harnesses (SimBroker/Recorder construction-arg builders) await #156. Wire-format forks derived + recorded on #157. refs #157
143 lines
6.4 KiB
Rust
143 lines
6.4 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(),
|
|
)
|
|
}
|
|
|
|
const SIGNAL_DOC: &str = r#"[
|
|
{"op":"source","role":"price","kind":"F64"},
|
|
{"op":"add","type":"SMA","as":"fast","bind":{"length":{"I64":2}}},
|
|
{"op":"add","type":"SMA","as":"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","as":"fast"},
|
|
{"op":"add","type":"SMA","as":"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}");
|
|
}
|
|
|
|
#[test]
|
|
fn graph_build_fails_fast_at_the_offending_op() {
|
|
let doc = r#"[{"op":"add","type":"SMA","as":"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","as":"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("Unconnected"), "names the unconnected slot: {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}");
|
|
}
|