//! End-to-end coverage for the construction op-script (#157, cycle 0088): a //! declarative, replayable by-identifier op-script (`replay` over a `Vec`) //! builds a runnable blueprint through validated ops, reusing the engine's //! existing gates at two cadences — eager (per-op) and holistic (finalize). //! //! The in-module tests in `construction.rs` reach into crate internals //! (`crate::GraphBuilder`, `crate::CompileError`, `super::replay`). These tests //! use only the **exported** surface — `aura_engine::{replay, Op, OpError, //! GraphBuilder, Composite, FlatGraph, Scalar, ScalarKind}` plus the injected //! `aura_vocabulary::std_vocabulary` — the exact seam the iteration-2 CLI (`aura graph //! build`/`introspect`) and the World consume. A refactor that made `Op`, //! `OpError`, `replay`, or a needed re-export (`ScalarKind`, `Scalar`) //! unreachable across the crate boundary would break that consumer while leaving //! every in-crate test green; that is the regression class this file pins, in //! addition to the per-property invariants below. use aura_engine::{replay, GraphBuilder, Op, OpError, Scalar, ScalarKind}; use aura_std::{Sma, Sub}; use aura_strategy::Bias; use aura_vocabulary::std_vocabulary; /// The flat root's open param point: fast.length(i64), slow.length(i64), /// bias.scale(f64), in param-space traversal order. The same vector is applied to /// both construction paths, so it cancels and the only thing under test is /// construction identity. Deterministic — no time, no randomness. fn params() -> [Scalar; 3] { [Scalar::i64(2), Scalar::i64(4), Scalar::f64(0.5)] } /// The SMA-cross signal as a construction op-script: a bound `price` source, a /// fast/slow SMA pair fed from it, their spread through `Sub`, a `Bias`, and the /// `bias` field exposed at the boundary. The smallest script that exercises every /// op kind (`Source`/`Add`/`Feed`/`Connect`/`Expose`) at once. fn signal_ops() -> Vec { vec![ Op::Source { role: "price".into(), kind: ScalarKind::F64 }, Op::Add { type_id: "SMA".into(), as_name: Some("fast".into()), bind: vec![] }, Op::Add { type_id: "SMA".into(), as_name: Some("slow".into()), bind: vec![] }, Op::Add { type_id: "Sub".into(), as_name: None, bind: vec![] }, Op::Add { type_id: "Bias".into(), as_name: None, bind: vec![] }, Op::Feed { role: "price".into(), into: vec!["fast.series".into(), "slow.series".into()] }, Op::Connect { from: "fast.value".into(), to: "sub.lhs".into() }, Op::Connect { from: "slow.value".into(), to: "sub.rhs".into() }, Op::Connect { from: "sub.value".into(), to: "bias.signal".into() }, Op::Expose { from: "bias.bias".into(), as_name: "bias".into() }, ] } /// Property (C24 acceptance-1, through the public seam): an op-script replayed via /// the exported `replay` + the injected `aura_vocabulary::std_vocabulary` compiles to a /// `FlatGraph` whose index-wired topology (`edges`) and lowered bound sources /// (`sources`) are **identical** to the same signal hand-wired on the exported /// `GraphBuilder` surface and compiled with the same param point. The two /// authoring surfaces are one construction semantics (C24) — proven entirely /// through `aura_engine`/`aura_std` public exports (the CLI/World seam). The /// non-empty `edges` guard keeps the equality from being the vacuous match of two /// empty graphs. #[test] fn replayed_construction_compiles_identically_to_graphbuilder() { // (a) op-script replay through the public construction surface. let replay_flat = replay("root", signal_ops(), &std_vocabulary, &|_: &str| None) .expect("op-script resolves") .compile_with_params(¶ms()) .expect("replay compiles"); // (b) the GraphBuilder twin — identical topology, identical param point. let mut g = GraphBuilder::new("root"); let fast = g.add(Sma::builder().named("fast")); let slow = g.add(Sma::builder().named("slow")); let sub = g.add(Sub::builder()); let bias = g.add(Bias::builder()); let price = g.source_role("price", ScalarKind::F64); g.feed(price, [fast.input("series"), slow.input("series")]); g.connect(fast.output("value"), sub.input("lhs")); g.connect(slow.output("value"), sub.input("rhs")); g.connect(sub.output("value"), bias.input("signal")); g.expose(bias.output("bias"), "bias"); let builder_flat = g .build() .expect("root resolves") .compile_with_params(¶ms()) .expect("twin compiles"); assert!(!replay_flat.edges.is_empty(), "the signal must wire a non-trivial topology"); assert_eq!(replay_flat.edges, builder_flat.edges, "construction topology diverged from the builder twin"); assert_eq!(replay_flat.sources, builder_flat.sources, "lowered bound sources diverged from the builder twin"); } /// Property (eager fail-fast cadence, at the public boundary): an eagerly-decidable /// fault — here an `Add` of a `type_id` outside the closed vocabulary — stops /// `replay` AT the offending op and is reported by `(op_index, OpError)`, the /// op_index being the position of the bad op (2 here), not the end of the script. /// The earlier valid ops do not mask it and the later ops never run. This is the /// by-index error attribution the CLI/World relies on to point an author at the /// exact failing op, asserted through the exported `replay`/`Op`/`OpError`. #[test] fn replay_attributes_eager_fault_to_its_op_index() { let ops = vec![ Op::Add { type_id: "SMA".into(), as_name: Some("fast".into()), bind: vec![] }, // 0 ok Op::Add { type_id: "Sub".into(), as_name: None, bind: vec![] }, // 1 ok Op::Add { type_id: "Nope".into(), as_name: None, bind: vec![] }, // 2 fails Op::Add { type_id: "Bias".into(), as_name: None, bind: vec![] }, // 3 never runs ]; // `.err().unwrap()` (not `unwrap_err`): the Ok arm `Composite` holds build // closures and is not `Debug`, which `unwrap_err`'s bound would require. let err = replay("g", ops, &std_vocabulary, &|_: &str| None).err().unwrap(); assert_eq!(err, (2, OpError::UnknownNodeType("Nope".into()))); } /// Property (holistic finalize cadence, at the public boundary): a fault that is /// only decidable at end-of-document — input-slot totality (the 0-cover arm of /// exactly-once wiring) — passes every per-op check and is caught by the implicit /// finalize step, attributed to index `ops.len()` (6 here), strictly past any real /// op index. Every op below applies cleanly; `sub.rhs` is left unwired, so only the /// holistic `validate_wiring` gate at finalize can reject it, as the by-identifier /// `OpError::UnconnectedPort { .. }`. Together with the /// eager-cadence test, this pins the iteration's eager/holistic gate split as /// observable error attribution across the crate boundary. #[test] fn replay_attributes_holistic_fault_to_finalize_step() { let ops = vec![ Op::Source { role: "price".into(), kind: ScalarKind::F64 }, // 0 Op::Add { type_id: "SMA".into(), as_name: Some("fast".into()), bind: vec![] }, // 1 Op::Add { type_id: "SMA".into(), as_name: Some("slow".into()), bind: vec![] }, // 2 Op::Add { type_id: "Sub".into(), as_name: None, bind: vec![] }, // 3 Op::Feed { role: "price".into(), into: vec!["fast.series".into(), "slow.series".into()] }, // 4 Op::Connect { from: "fast.value".into(), to: "sub.lhs".into() }, // 5 // sub.rhs deliberately left unwired -> only finalize can decide totality. ]; let finalize_index = ops.len(); // 6 — the implicit finalize step let err = replay("g", ops, &std_vocabulary, &|_: &str| None).err().unwrap(); assert!(matches!(&err, (i, OpError::UnconnectedPort { .. }) if *i == finalize_index), "the holistic fault is still attributed to the finalize step, got {err:?}"); }