257ab0b9f2
Cut 3 of the hard-wired demo retirement. Unlike r-sma/r-breakout, r-meanrev could
not round-trip as data: its band computes band_k*sigma (a constant times a stream)
and the closed 22-node std_vocabulary had no way to express it (EqConst is i64->bool,
Bias is clamp(signal/scale,±1), Mul is binary, LinComb is excluded as a
construction-arg node; no Div/Const/constant-emitter). Standard operators are missing
by chance, not by design, so this adds the one r-meanrev needs.
Scale node (aura-std): out = input * factor — one f64 input, one f64 `factor` param,
stateless, warm-up-filtered (the EqConst/Bias shape). A zero-input `Const` emitter was
rejected: the per-cycle eval loop gates every node through an input-firing test
(harness.rs `fires()`), so a node with no input never fires without an engine-core
change; `Scale` fits the existing model and, by IEEE-754 commutativity, reproduces the
retiring LinComb's band byte-for-byte. Rostered (`"Scale" => Scale`); the two
vocabulary count-pins bump 22 -> 23.
r-meanrev migration: r_meanrev_signal(window, band_k) is carved out of the fused
r_meanrev_graph as a #[cfg(test)] price->bias Composite whose band is `Scale` in place
of `LinComb(1)`; production loads the shipped JSON (examples/r_meanrev{,_open}.json),
never a builder. Before the fused builder was deleted, an equivalence test proved the
carved Scale-band signal reproduces its grade byte-for-byte on the synthetic stream
(window=3, band_k=2). Durable survivors: the byte-identity of the examples to the
carve, the loaded-grades-identically-to-carve proof, the closed/open introspect
anchors (mean_window.length, var_window.length, band.factor — band_k is now a
first-class sweepable param), and — re-pointed onto the carve rather than dropped —
the fades-short-above/long-below behavioural test, which becomes the durable
carve-correctness gate the deleted equivalence test used to be.
Deletions + the dead-code cascade the retirement opened: r_meanrev_graph,
r_meanrev_sweep_family, Strategy::RMeanRev (+ all arms). r-meanrev was the last reader
of run_sweep's grid, so the whole vestigial built-in-sweep grid apparatus retires with
it: run_sweep's `grid` param, the sweep call-site grid build, and SweepCmd's
--fast/--slow/--stop-length/--stop-k/--window/--band-k (sma/momentum sweeps build their
own blueprints and ignored these; the fast/slow/stop-* had been vestigial since r-sma's
cut 1b). RGrid the struct SURVIVES — the dissolved `generalize` verb resolves its
candidate from its fast/slow/stop_length/stop_k via generalize_args_from — so only its
window/band_k fields go; its stale doc-comment (r-sma-sweep / persist_traces_r / CLI
flags, all now gone) is rewritten to its true generalize-only role. Also retired as
transitively dead: persist_traces_r and the metrics_object test helper (their only
callers were the deleted families/tests); Add/Gt/Latch/Mul/Sqrt imports are now
#[cfg(test)] (their last production caller was r_meanrev_graph).
Test surface: the two built-in --strategy r-meanrev sweep tests drop; a new negative
pins that --strategy r-meanrev on both sweep and walkforward now falls into the generic
usage error; the grid-flag stray-positional negative is re-pointed to a surviving flag.
Verification (own, not the workflow's report): full `cargo test --workspace` green
(0 failed across 62 result lines); `cargo clippy --workspace --all-targets -- -D
warnings` clean, no dead-code residue; the r-sma + r-breakout anchors and the
dissolved-verb real goldens (generalize still reads RGrid) stay green. Bundled as one
commit (the Scale node's count-pin and the r-meanrev anchors share graph_construct.rs,
so a clean two-commit split was not path-separable); the implement-loop ran all three
plan tasks in one pass and the tree was verified by hand.
refs #159
784 lines
40 KiB
Rust
784 lines
40 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}");
|
||
}
|
||
|
||
/// The #160 roster macro's whole point is that `std_vocabulary`'s match arms and
|
||
/// `std_vocabulary_types`'s enumerable list can never drift apart — but that
|
||
/// guarantee is checked in-process, inside `aura-std`'s own unit tests. This pins
|
||
/// the SAME closed-roster count one layer further out, through a code path the
|
||
/// roster macro cannot see: `Env::type_ids()` (project ∪ std concatenation) and
|
||
/// the CLI's println loop, observed across the real process boundary. A bug in
|
||
/// that CLI-side plumbing (a dropped or duplicated entry in the concatenation or
|
||
/// the print loop) would pass every roster-internal test yet still change what a
|
||
/// user actually sees from `aura graph introspect --vocabulary` with no project
|
||
/// loaded — this test is the only thing that would catch it.
|
||
#[test]
|
||
fn graph_introspect_vocabulary_lists_exactly_the_closed_roster_count() {
|
||
let (stdout, _stderr, ok) = run(&["graph", "introspect", "--vocabulary"], "");
|
||
assert!(ok, "exit success");
|
||
let lines: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect();
|
||
assert_eq!(
|
||
lines.len(),
|
||
23,
|
||
"the std-only (no project) vocabulary has exactly the roster's 23 entries: {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}");
|
||
}
|
||
|
||
/// Property (#171 acc 1 at the binary seam): `--identity-id` bridges op-scripts
|
||
/// that differ only in debug names — the renamed twin keeps the same identity id
|
||
/// while its content id moves. Rename via blanket replace: "fast" occurs as the
|
||
/// instance name and in every `fast.*` reference, so the replaced document stays
|
||
/// internally consistent.
|
||
#[test]
|
||
fn graph_introspect_identity_id_bridges_renamed_op_scripts() {
|
||
let renamed = SIGNAL_DOC.replace("fast", "speedy");
|
||
let (ia, _e, ok) = run(&["graph", "introspect", "--identity-id"], SIGNAL_DOC);
|
||
assert!(ok, "exit success");
|
||
let ia = ia.trim().to_string();
|
||
assert_eq!(ia.len(), 64, "a 64-hex identity id: {ia:?}");
|
||
assert!(ia.chars().all(|c| c.is_ascii_hexdigit()), "hex only: {ia:?}");
|
||
let (ib, _e2, ok2) = run(&["graph", "introspect", "--identity-id"], &renamed);
|
||
assert!(ok2, "exit success");
|
||
assert_eq!(ia, ib.trim(), "renamed twin -> same identity id");
|
||
let (ca, _e3, ok3) = run(&["graph", "introspect", "--content-id"], SIGNAL_DOC);
|
||
let (cb, _e4, ok4) = run(&["graph", "introspect", "--content-id"], &renamed);
|
||
assert!(ok3 && ok4, "exit success");
|
||
assert_ne!(ca.trim(), cb.trim(), "renamed twin -> different content ids");
|
||
}
|
||
|
||
/// Property (combinable id flags): `--content-id --identity-id` prints both ids,
|
||
/// one per line, content id first — each byte-equal to its single-flag output,
|
||
/// and the two differ (SIGNAL_DOC carries debug names the identity form blanks).
|
||
#[test]
|
||
fn graph_introspect_content_and_identity_id_combine() {
|
||
let (both, _e, ok) = run(&["graph", "introspect", "--content-id", "--identity-id"], SIGNAL_DOC);
|
||
assert!(ok, "exit success");
|
||
let lines: Vec<&str> = both.lines().collect();
|
||
assert_eq!(lines.len(), 2, "two lines, one id each: {both:?}");
|
||
let (content, _e2, ok2) = run(&["graph", "introspect", "--content-id"], SIGNAL_DOC);
|
||
let (identity, _e3, ok3) = run(&["graph", "introspect", "--identity-id"], SIGNAL_DOC);
|
||
assert!(ok2 && ok3, "exit success");
|
||
assert_eq!(lines[0], content.trim(), "content id first");
|
||
assert_eq!(lines[1], identity.trim(), "identity id second");
|
||
assert_ne!(lines[0], lines[1], "the two ids differ on a debug-named document");
|
||
}
|
||
|
||
/// Property (negative, mirror of the content-id rejection): `--identity-id` on a
|
||
/// bad op-list exits non-zero with the cause on stderr and prints no id.
|
||
#[test]
|
||
fn graph_introspect_identity_id_rejects_a_bad_document() {
|
||
let (stdout, stderr, ok) =
|
||
run(&["graph", "introspect", "--identity-id"], r#"[{"op":"add","type":"Nope"}]"#);
|
||
assert!(!ok, "non-zero exit on a bad op-list");
|
||
assert!(stdout.is_empty(), "no identity id emitted on error: {stdout}");
|
||
assert!(stderr.contains("Nope"), "names the cause: {stderr}");
|
||
}
|
||
|
||
/// Property (usage gate — the previously untested count!=1 dispatch path):
|
||
/// `graph introspect` with no selection flag is a usage fault — exit 2 with the
|
||
/// usage line on stderr.
|
||
#[test]
|
||
fn graph_introspect_no_flag_is_usage_exit_2() {
|
||
let (stderr, code) = run_code(&["graph", "introspect"], "");
|
||
assert_eq!(code, Some(2), "no selection flag is a usage error -> exit 2; stderr: {stderr}");
|
||
assert!(stderr.contains("Usage"), "prints the usage line: {stderr}");
|
||
}
|
||
|
||
/// A fresh, unique working directory for a test that persists content-addressed
|
||
/// blueprints under `./runs/` (mirrors `research_docs.rs`'s `temp_cwd`).
|
||
fn temp_cwd(name: &str) -> std::path::PathBuf {
|
||
let dir = std::env::temp_dir().join(format!("aura-graph-{}-{}", std::process::id(), name));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
std::fs::create_dir_all(&dir).expect("create temp cwd");
|
||
dir
|
||
}
|
||
|
||
/// Run `aura <args>` in `dir` (no stdin); return (stdout, stderr, exit code).
|
||
fn run_in(dir: &std::path::Path, args: &[&str]) -> (String, String, Option<i32>) {
|
||
let out = Command::new(BIN)
|
||
.args(args)
|
||
.current_dir(dir)
|
||
.output()
|
||
.expect("binary runs");
|
||
(
|
||
String::from_utf8_lossy(&out.stdout).into_owned(),
|
||
String::from_utf8_lossy(&out.stderr).into_owned(),
|
||
out.status.code(),
|
||
)
|
||
}
|
||
|
||
/// The absolute path of a blueprint fixture shipped with this test crate.
|
||
fn fixture(name: &str) -> String {
|
||
format!("{}/tests/fixtures/{name}", env!("CARGO_MANIFEST_DIR"))
|
||
}
|
||
|
||
/// The absolute path of a demo blueprint shipped in the public examples/
|
||
/// gallery (#159 copy+prove) — distinct from `fixture()`, which reads the
|
||
/// internal tests/fixtures/ copies the emitter builds from.
|
||
fn example(name: &str) -> String {
|
||
format!("{}/examples/{name}", env!("CARGO_MANIFEST_DIR"))
|
||
}
|
||
|
||
/// The id extracted from a `registered blueprint {id} ({path})` line.
|
||
/// Prefix-tolerant across the #194 convention change (the display form dropped
|
||
/// the glued `content:` prefix): the id is bare either way.
|
||
fn registered_id(stdout: &str) -> String {
|
||
stdout
|
||
.lines()
|
||
.find(|l| l.starts_with("registered blueprint "))
|
||
.expect("register line")
|
||
.trim_start_matches("registered blueprint ")
|
||
.split(' ')
|
||
.next()
|
||
.expect("id")
|
||
.trim_start_matches("content:")
|
||
.to_string()
|
||
}
|
||
|
||
/// Property (#196, the on-ramp): `aura graph register <blueprint.json>` parses
|
||
/// the document through the vocabulary, canonicalizes, and stores it content-
|
||
/// addressed under `runs/blueprints/<id>.json`, printing the id + store path.
|
||
#[test]
|
||
fn graph_register_stores_and_prints_content_id() {
|
||
let dir = temp_cwd("register");
|
||
let (stdout, stderr, code) = run_in(&dir, &["graph", "register", &example("r_sma.json")]);
|
||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||
// #194: the register verb prints the bare id — `registered blueprint <id> (<path>)`.
|
||
assert!(!stdout.contains("content:"), "register prints the bare id, no content: prefix: {stdout}");
|
||
let id = registered_id(&stdout);
|
||
assert_eq!(id.len(), 64, "a 64-hex content id: {id:?}");
|
||
assert!(id.chars().all(|c| c.is_ascii_hexdigit()), "hex only: {id:?}");
|
||
assert!(
|
||
dir.join("runs").join("blueprints").join(format!("{id}.json")).is_file(),
|
||
"stored under runs/blueprints/<id>.json: {stdout}"
|
||
);
|
||
}
|
||
|
||
/// Register is write-once content-addressed: the same document registers to the
|
||
/// same id, exit 0 both times (the idempotent `put_blueprint` contract).
|
||
#[test]
|
||
fn graph_register_is_idempotent() {
|
||
let dir = temp_cwd("register-idempotent");
|
||
let bp = example("r_sma.json");
|
||
let (out1, err1, code1) = run_in(&dir, &["graph", "register", &bp]);
|
||
let (out2, err2, code2) = run_in(&dir, &["graph", "register", &bp]);
|
||
assert_eq!(code1, Some(0), "first register: {out1} {err1}");
|
||
assert_eq!(code2, Some(0), "second register: {out2} {err2}");
|
||
assert_eq!(registered_id(&out1), registered_id(&out2), "same id twice");
|
||
}
|
||
|
||
/// Property (#196, the campaign-axis namespace): `--params <FILE>` prints the
|
||
/// RAW composite param space — one `{name}:{kind:?}` line per open param, in
|
||
/// lowering order, WITHOUT the harness-wrap prefix `aura sweep --list-axes`
|
||
/// shows. The open fixture leaves exactly the two SMA lengths unbound
|
||
/// (`bias.scale` is bound in the document).
|
||
#[test]
|
||
fn graph_params_lists_raw_axis_namespace() {
|
||
let dir = temp_cwd("params");
|
||
let (stdout, stderr, code) =
|
||
run_in(&dir, &["graph", "introspect", "--params", &example("r_sma_open.json")]);
|
||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||
assert_eq!(stdout, "fast.length:I64\nslow.length:I64\n", "the raw open params, in order");
|
||
}
|
||
|
||
/// Property (#196, file-mode --content-id): a blueprint FILE's printed content
|
||
/// id is exactly the store key `graph register` prints — the file is shape-
|
||
/// discriminated from the op-list form and canonicalized by the blueprint rules.
|
||
#[test]
|
||
fn graph_content_id_accepts_a_blueprint_file() {
|
||
let dir = temp_cwd("content-id-file");
|
||
let bp = example("r_sma.json");
|
||
let (reg_out, reg_err, reg_code) = run_in(&dir, &["graph", "register", &bp]);
|
||
assert_eq!(reg_code, Some(0), "register: {reg_out} {reg_err}");
|
||
let (id_out, id_err, id_code) = run_in(&dir, &["graph", "introspect", "--content-id", &bp]);
|
||
assert_eq!(id_code, Some(0), "content-id: {id_out} {id_err}");
|
||
assert_eq!(id_out.trim(), registered_id(®_out), "file content id == store key");
|
||
}
|
||
|
||
/// The one-mode guard extends over the new `--params` mode: zero modes and two
|
||
/// modes both refuse usage-exit-2, and the usage line names the new mode.
|
||
#[test]
|
||
fn graph_introspect_mode_guard_still_exits_two_on_zero_or_two_modes() {
|
||
let dir = temp_cwd("mode-guard");
|
||
let (_out0, err0, code0) = run_in(&dir, &["graph", "introspect"]);
|
||
assert_eq!(code0, Some(2), "zero modes is usage exit 2: {err0}");
|
||
assert!(err0.contains("--params <FILE|ID>"), "usage line names --params: {err0}");
|
||
let (_out2, err2, code2) =
|
||
run_in(&dir, &["graph", "introspect", "--vocabulary", "--params", "x.json"]);
|
||
assert_eq!(code2, Some(2), "two modes is usage exit 2: {err2}");
|
||
assert!(err2.contains("Usage: aura graph introspect"), "prints the usage line: {err2}");
|
||
}
|
||
|
||
/// Property (#196, the register->params-by-id round trip): `--params <ID>`
|
||
/// with a registered blueprint's content id resolves through the project
|
||
/// store and prints exactly the same raw axis namespace `--params <FILE>`
|
||
/// prints for the same document — the advertised `<FILE|ID>` addressing is
|
||
/// two paths to the identical answer, not merely two accepted shapes.
|
||
#[test]
|
||
fn graph_params_by_content_id_matches_params_by_file() {
|
||
let dir = temp_cwd("params-by-id");
|
||
let bp = example("r_sma_open.json");
|
||
let (reg_out, reg_err, reg_code) = run_in(&dir, &["graph", "register", &bp]);
|
||
assert_eq!(reg_code, Some(0), "register: {reg_out} {reg_err}");
|
||
let id = registered_id(®_out);
|
||
let (by_id_out, by_id_err, by_id_code) =
|
||
run_in(&dir, &["graph", "introspect", "--params", &id]);
|
||
assert_eq!(by_id_code, Some(0), "stdout: {by_id_out} stderr: {by_id_err}");
|
||
let (by_file_out, by_file_err, by_file_code) =
|
||
run_in(&dir, &["graph", "introspect", "--params", &bp]);
|
||
assert_eq!(by_file_code, Some(0), "stdout: {by_file_out} stderr: {by_file_err}");
|
||
assert_eq!(by_id_out, by_file_out, "by-id and by-file agree on the raw axis namespace");
|
||
assert_eq!(by_id_out, "fast.length:I64\nslow.length:I64\n", "the raw open params, in order");
|
||
}
|
||
|
||
/// #194 (the prefix trap): a `--params <ID>` target may carry the display
|
||
/// `content:` prefix copied verbatim from register/introspect output — the CLI
|
||
/// strips it and resolves the bare store address, printing exactly the same raw
|
||
/// axis namespace the bare id (and the file) print.
|
||
#[test]
|
||
fn graph_params_tolerates_content_prefix_on_target() {
|
||
let dir = temp_cwd("params-content-prefix");
|
||
let bp = example("r_sma_open.json");
|
||
let (reg_out, reg_err, reg_code) = run_in(&dir, &["graph", "register", &bp]);
|
||
assert_eq!(reg_code, Some(0), "register: {reg_out} {reg_err}");
|
||
let id = registered_id(®_out);
|
||
let (pfx_out, pfx_err, pfx_code) =
|
||
run_in(&dir, &["graph", "introspect", "--params", &format!("content:{id}")]);
|
||
assert_eq!(pfx_code, Some(0), "content:-prefixed --params must resolve: stdout {pfx_out} stderr {pfx_err}");
|
||
assert_eq!(pfx_out, "fast.length:I64\nslow.length:I64\n", "same raw axis namespace as the bare id");
|
||
}
|
||
|
||
/// Property (#196, negative): `--params <ID>` with a well-shaped 64-hex id
|
||
/// that was never registered refuses with a store-miss message, not a panic
|
||
/// or a silent empty namespace — the registry lookup branch of
|
||
/// `resolve_blueprint_text` is exercised.
|
||
#[test]
|
||
fn graph_params_by_unregistered_id_reports_a_store_miss() {
|
||
let dir = temp_cwd("params-missing-id");
|
||
let bogus_id = "a".repeat(64);
|
||
let (stdout, stderr, code) = run_in(&dir, &["graph", "introspect", "--params", &bogus_id]);
|
||
assert_ne!(code, Some(0), "non-zero exit on a store miss: {stdout} {stderr}");
|
||
assert!(stdout.is_empty(), "no partial namespace on a miss: {stdout}");
|
||
assert!(
|
||
stderr.contains(&format!("no blueprint {bogus_id} in the project store")),
|
||
"names the miss: {stderr}"
|
||
);
|
||
}
|
||
|
||
/// Property (#196, negative): `--params <TARGET>` where TARGET is neither an
|
||
/// existing file nor a 64-hex content id refuses naming both readings — the
|
||
/// `resolve_blueprint_text` fall-through branch, shared with `campaign run`'s
|
||
/// target resolution via `is_content_id`.
|
||
#[test]
|
||
fn graph_params_target_neither_file_nor_id_is_refused() {
|
||
let dir = temp_cwd("params-neither");
|
||
let (stdout, stderr, code) = run_in(&dir, &["graph", "introspect", "--params", "not-a-file-or-id"]);
|
||
assert_ne!(code, Some(0), "non-zero exit: {stdout} {stderr}");
|
||
assert!(stdout.is_empty(), "no partial namespace on refusal: {stdout}");
|
||
assert!(
|
||
stderr.contains("'not-a-file-or-id' is neither a readable .json file nor a 64-hex content id"),
|
||
"names both readings: {stderr}"
|
||
);
|
||
}
|
||
|
||
/// Property (#196, the op-list branch of file-mode --content-id): a FILE that
|
||
/// is a JSON *array* (a construction op-list, not a blueprint envelope) is
|
||
/// shape-discriminated correctly and produces the SAME content id as piping
|
||
/// the identical document to stdin — `composite_from_any`'s `Array` arm is
|
||
/// exercised end-to-end, not just its `Object` sibling.
|
||
#[test]
|
||
fn graph_content_id_accepts_an_op_list_file() {
|
||
let dir = temp_cwd("content-id-op-list-file");
|
||
let op_list_path = dir.join("op-list.json");
|
||
std::fs::write(&op_list_path, SIGNAL_DOC).expect("write op-list fixture");
|
||
let (file_out, file_err, file_code) = run_in(
|
||
&dir,
|
||
&["graph", "introspect", "--content-id", op_list_path.to_str().unwrap()],
|
||
);
|
||
assert_eq!(file_code, Some(0), "stdout: {file_out} stderr: {file_err}");
|
||
let (stdin_out, _e, stdin_ok) = run(&["graph", "introspect", "--content-id"], SIGNAL_DOC);
|
||
assert!(stdin_ok, "stdin variant succeeds");
|
||
assert_eq!(file_out.trim(), stdin_out.trim(), "op-list file and stdin agree on content id");
|
||
}
|
||
|
||
/// Property (#196, negative): a FILE whose top-level JSON value is neither an
|
||
/// object (blueprint envelope) nor an array (op-list) — e.g. a bare string —
|
||
/// is refused, exercising `composite_from_any`'s fall-through arm.
|
||
#[test]
|
||
fn graph_content_id_rejects_a_file_that_is_neither_object_nor_array() {
|
||
let dir = temp_cwd("content-id-neither-shape");
|
||
let bad_path = dir.join("bad.json");
|
||
std::fs::write(&bad_path, "\"just a string\"").expect("write bad fixture");
|
||
let (stdout, stderr, code) = run_in(
|
||
&dir,
|
||
&["graph", "introspect", "--content-id", bad_path.to_str().unwrap()],
|
||
);
|
||
assert_ne!(code, Some(0), "non-zero exit: {stdout} {stderr}");
|
||
assert!(stdout.is_empty(), "no content id emitted: {stdout}");
|
||
assert!(
|
||
stderr.contains("document is neither a blueprint envelope (object) nor an op-list (array)"),
|
||
"names the shape refusal: {stderr}"
|
||
);
|
||
}
|
||
|
||
/// Property (#196, negative): `register` on a syntactically-valid blueprint
|
||
/// envelope that names an unknown node type refuses with the `blueprint_load_prose`
|
||
/// `UnknownNodeType` prose (not a panic, not a bare Debug dump) — this refusal
|
||
/// path is shared by `register`, `--params`, and file-mode `--content-id` (all
|
||
/// route through `blueprint_from_json` -> `blueprint_load_prose`), so exercising
|
||
/// it once through `register` closes the whole family.
|
||
#[test]
|
||
fn graph_register_rejects_an_unknown_node_type_with_prose() {
|
||
let dir = temp_cwd("register-unknown-node");
|
||
let (stdout, stderr, code) =
|
||
run_in(&dir, &["graph", "register", &fixture("unknown_node.json")]);
|
||
assert_ne!(code, Some(0), "non-zero exit: {stdout} {stderr}");
|
||
assert!(stdout.is_empty(), "no registration line on refusal: {stdout}");
|
||
assert!(
|
||
stderr.contains("unknown node type \"Nope\""),
|
||
"names the unknown node type: {stderr}"
|
||
);
|
||
}
|
||
|
||
/// Property (#202 headline, the on-ramp is shape-invariant): `aura graph register`
|
||
/// accepts an op-script document (a JSON array) exactly as it accepts a #155
|
||
/// blueprint envelope (a JSON object) — it shape-discriminates the array form,
|
||
/// builds it through the same one-build tail, and stores it under the SAME content
|
||
/// id it stores that op-script's `graph build` envelope under. The content id is
|
||
/// shape-invariant (an op-script and its built envelope hash identically), so the
|
||
/// two on-ramp shapes must agree on the store key; `register` must not reject the
|
||
/// op-script with the raw serde `invalid type: map, expected u32` message the
|
||
/// direct-`blueprint_from_json` path currently leaks.
|
||
#[test]
|
||
fn graph_register_accepts_an_op_script_and_stores_the_envelope_content_id() {
|
||
let dir = temp_cwd("register-op-script");
|
||
// The op-script form (a JSON array of ops), written to a file.
|
||
let op_script = dir.join("op-script.json");
|
||
std::fs::write(&op_script, SIGNAL_DOC).expect("write op-script fixture");
|
||
// Its `graph build` envelope (a JSON object), the shape `register` already accepts.
|
||
let (envelope_bytes, _e, built) = run(&["graph", "build"], SIGNAL_DOC);
|
||
assert!(built, "graph build produces the envelope");
|
||
let envelope = dir.join("envelope.json");
|
||
std::fs::write(&envelope, &envelope_bytes).expect("write envelope fixture");
|
||
|
||
// The envelope shape registers today (green sibling); the op-script must too.
|
||
let (env_out, env_err, env_code) =
|
||
run_in(&dir, &["graph", "register", envelope.to_str().unwrap()]);
|
||
assert_eq!(env_code, Some(0), "register envelope: {env_out} {env_err}");
|
||
let (op_out, op_err, op_code) =
|
||
run_in(&dir, &["graph", "register", op_script.to_str().unwrap()]);
|
||
assert_eq!(op_code, Some(0), "register op-script exits 0: {op_out} {op_err}");
|
||
|
||
assert_eq!(
|
||
registered_id(&op_out),
|
||
registered_id(&env_out),
|
||
"op-script and its built envelope register to the same content id"
|
||
);
|
||
}
|
||
|
||
/// Property (#202, the same on-ramp seam at the sibling verb): `aura graph
|
||
/// introspect --params` accepts an op-script exactly as it accepts a blueprint
|
||
/// envelope — the raw axis namespace it prints for the op-script equals the one it
|
||
/// prints for that op-script's built envelope, both exit 0. Currently `--params`
|
||
/// rejects the op-script with the same raw serde `invalid type: map, expected u32`
|
||
/// message `register` does (both route through the direct `blueprint_from_json`).
|
||
#[test]
|
||
fn graph_params_accepts_an_op_script_matching_its_envelope() {
|
||
let dir = temp_cwd("params-op-script");
|
||
let op_script = dir.join("op-script.json");
|
||
std::fs::write(&op_script, SIGNAL_DOC).expect("write op-script fixture");
|
||
let (envelope_bytes, _e, built) = run(&["graph", "build"], SIGNAL_DOC);
|
||
assert!(built, "graph build produces the envelope");
|
||
let envelope = dir.join("envelope.json");
|
||
std::fs::write(&envelope, &envelope_bytes).expect("write envelope fixture");
|
||
|
||
let (env_out, env_err, env_code) =
|
||
run_in(&dir, &["graph", "introspect", "--params", envelope.to_str().unwrap()]);
|
||
assert_eq!(env_code, Some(0), "params on envelope: {env_out} {env_err}");
|
||
let (op_out, op_err, op_code) =
|
||
run_in(&dir, &["graph", "introspect", "--params", op_script.to_str().unwrap()]);
|
||
assert_eq!(op_code, Some(0), "params on op-script exits 0: {op_out} {op_err}");
|
||
assert_eq!(op_out, env_out, "op-script and envelope agree on the raw axis namespace");
|
||
}
|
||
|
||
/// Property (#159): the shipped closed example (`examples/r_sma.json`) is
|
||
/// genuinely closed, not merely byte-identical to the builder — `graph
|
||
/// introspect --params` on it reports zero unbound params. The Cut-1
|
||
/// byte-fidelity proof (`shipped_r_sma_examples_are_byte_identical_to_the_builder`,
|
||
/// main.rs) only pins the example's bytes to the builder's serialization; it says
|
||
/// nothing about whether that serialization is actually closed. This pins the
|
||
/// "closed" claim the demo makes to its audience.
|
||
#[test]
|
||
fn shipped_r_sma_example_is_genuinely_closed() {
|
||
let dir = temp_cwd("example-closed-params");
|
||
let (stdout, stderr, code) =
|
||
run_in(&dir, &["graph", "introspect", "--params", &example("r_sma.json")]);
|
||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||
assert_eq!(stdout, "", "the closed example must leave zero params unbound");
|
||
}
|
||
|
||
/// Property (#159): the shipped open example (`examples/r_sma_open.json`) is a
|
||
/// usable sweep-axis template from its public gallery location, not just a
|
||
/// byte-identical artifact — `graph introspect --params` lists exactly the two
|
||
/// unbound SMA lengths, in lowering order, matching the raw axis namespace the
|
||
/// internal `sma_signal_open.json` fixture prints (#196). The byte-fidelity
|
||
/// proof only checks the example's bytes match the builder; this checks the
|
||
/// shipped copy is actually introspectable/bindable from where a consumer runs
|
||
/// it.
|
||
#[test]
|
||
fn shipped_r_sma_open_example_lists_its_axis_namespace() {
|
||
let dir = temp_cwd("example-open-params");
|
||
let (stdout, stderr, code) =
|
||
run_in(&dir, &["graph", "introspect", "--params", &example("r_sma_open.json")]);
|
||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||
assert_eq!(
|
||
stdout, "fast.length:I64\nslow.length:I64\n",
|
||
"the raw open params, in order, from the public gallery copy"
|
||
);
|
||
}
|
||
|
||
/// Property (#159 cut 2): the shipped closed example (`examples/r_breakout.json`) is
|
||
/// genuinely closed — `graph introspect --params` reports zero unbound params.
|
||
#[test]
|
||
fn shipped_r_breakout_example_is_genuinely_closed() {
|
||
let dir = temp_cwd("r-breakout-example-closed-params");
|
||
let (stdout, stderr, code) =
|
||
run_in(&dir, &["graph", "introspect", "--params", &example("r_breakout.json")]);
|
||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||
assert_eq!(stdout, "", "the closed example must leave zero params unbound");
|
||
}
|
||
|
||
/// Property (#159 cut 2): the shipped open example (`examples/r_breakout_open.json`)
|
||
/// lists its two channel axes, in lowering order (per-node exposure; ganging is #61).
|
||
#[test]
|
||
fn shipped_r_breakout_open_example_lists_its_axis_namespace() {
|
||
let dir = temp_cwd("r-breakout-example-open-params");
|
||
let (stdout, stderr, code) =
|
||
run_in(&dir, &["graph", "introspect", "--params", &example("r_breakout_open.json")]);
|
||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||
assert_eq!(
|
||
stdout, "channel_hi.length:I64\nchannel_lo.length:I64\n",
|
||
"the raw open params, in order, from the public gallery copy"
|
||
);
|
||
}
|
||
|
||
/// Property (#159 cut 3): the shipped closed example (`examples/r_meanrev.json`) is
|
||
/// genuinely closed — `graph introspect --params` reports zero unbound params.
|
||
#[test]
|
||
fn shipped_r_meanrev_example_is_genuinely_closed() {
|
||
let dir = temp_cwd("r-meanrev-example-closed-params");
|
||
let (stdout, stderr, code) =
|
||
run_in(&dir, &["graph", "introspect", "--params", &example("r_meanrev.json")]);
|
||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||
assert_eq!(stdout, "", "the closed example must leave zero params unbound");
|
||
}
|
||
|
||
/// Property (#159 cut 3): the shipped open example (`examples/r_meanrev_open.json`)
|
||
/// lists its raw axis namespace, in lowering (node-add) order.
|
||
#[test]
|
||
fn shipped_r_meanrev_open_example_lists_its_axis_namespace() {
|
||
let dir = temp_cwd("r-meanrev-example-open-params");
|
||
let (stdout, stderr, code) =
|
||
run_in(&dir, &["graph", "introspect", "--params", &example("r_meanrev_open.json")]);
|
||
assert_eq!(code, Some(0), "stdout: {stdout} stderr: {stderr}");
|
||
assert_eq!(
|
||
stdout, "mean_window.length:I64\nvar_window.length:I64\nband.factor:F64\n",
|
||
"the raw open params, in lowering order"
|
||
);
|
||
}
|