Files
Aura/crates/aura-engine/tests/construction_e2e.rs
T
claude b39fd63396 refactor: split the aura-std roster into C28 layer crates
Phase 4 of the Stratification milestone. aura-std held four C28 ladder
layers in one roster; this cuts them into layer-aligned, aura-core-only
node crates so the import direction is enforced by the crate graph:

- aura-std        — engine nodes only (arithmetic/logic/rolling + sinks)
- aura-market     — session, resample
- aura-strategy   — bias, stops, sizer, cost-model machinery
- aura-backtest   — sim_broker, position_management
- aura-vocabulary — the relocated closed std_vocabulary roster

Node modules move verbatim (byte-identical renames); consumers are
rewired by import path only. A new structural test
(aura-vocabulary/tests/c28_layering.rs) asserts each node crate's
[dependencies] stay within its C28-permitted inner set, catching the
acyclic-but-outward violation the compiler misses.

Behaviour byte-identical: full workspace suite green (1448 tests), no
golden edited, clippy -D warnings clean. C28 Status block updated.

closes #288
2026-07-19 20:28:20 +02:00

135 lines
7.8 KiB
Rust

//! End-to-end coverage for the construction op-script (#157, cycle 0088): a
//! declarative, replayable by-identifier op-script (`replay` over a `Vec<Op>`)
//! 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<Op> {
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)
.expect("op-script resolves")
.compile_with_params(&params())
.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(&params())
.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).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).err().unwrap();
assert!(matches!(&err, (i, OpError::UnconnectedPort { .. }) if *i == finalize_index),
"the holistic fault is still attributed to the finalize step, got {err:?}");
}