Files
Aura/crates/aura-engine/tests/gang_e2e.rs
T
Brummel 123620442f feat(engine): gang param-space projection + compile-front point expansion (#61 task 2)
param_space()/derive_signature project each frame's gang table: member
addresses are skipped, the gang's single public address (prefixed like any
knob at that frame) is emitted once at its FIRST member's raw position — so
every consumer of param_space (binders, grids, campaign axes, --list-axes,
registry coverage) inherits the gang with zero changes.

compile_with_cells expands the public point back onto the raw layout via
expansion_map (the collect_params mirror walk): a gang's cell is duplicated
into every member slot, un-ganged slots pass through. For a gang-free
blueprint the map is the identity and behaviour is byte-identical; the
lower_items cursor walk and all build closures are untouched.

The load-bearing equivalence is pinned in-module (ganged compile == the
member-bound un-ganged twin, bit-identical wiring and run output) and again
from OUTSIDE the crate in tests/gang_e2e.rs — the C24 consumer seam a
project crate or the World actually uses.

Verified: cargo test -p aura-engine 253/0 lib + 5/0 gang_e2e (all groups
green), workspace clippy -D warnings clean. RED-first evidenced by the loop
via temporary call-site reversion (all 4 new tests failed for the expected
reason, then the implementation was restored).

refs #61
2026-07-10 10:37:51 +02:00

200 lines
8.2 KiB
Rust

//! End-to-end coverage for parameter ganging's compile-front (#61 task 2): a
//! composite's `Gang` table (installed via `Composite::with_gangs`, task 1) is
//! projected by `param_space()` into ONE public address per gang, and expanded
//! back onto every member's raw slot at `compile_with_params` time.
//!
//! The in-module tests in `blueprint.rs` cover this through the same public
//! `Composite`/`aura_std` surface but stay inside the crate. This file proves
//! the identical properties reachable from OUTSIDE `aura-engine` — the exact
//! seam a project crate or the World consumes (C24) — so a refactor that made
//! `Gang`, `GangMember`, or `Composite::with_gangs` unreachable across the
//! crate boundary would break a real consumer while every in-crate test stayed
//! green.
use std::sync::mpsc;
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{
BlueprintNode, CompileError, Composite, Edge, Gang, GangMember, OutField, Role, Target,
VecSource,
};
use aura_std::{Recorder, Sma, Sub};
/// Five synthetic F64 ticks: short, but long enough that a length-2 or length-3
/// SMA warms up and the spread fires at least once. Deterministic — no time, no
/// randomness.
fn synthetic_prices() -> Vec<(Timestamp, Scalar)> {
[(1_i64, 1.0000_f64), (2, 1.0010), (3, 1.0030), (4, 1.0060), (5, 1.0040)]
.iter()
.map(|&(t, p)| (Timestamp(t), Scalar::f64(p)))
.collect()
}
/// Two open-`length` SMAs fed from one bound `price` role, spread via `Sub`,
/// with both `length` params fused into one gang named `"length"`.
fn ganged_pair() -> Composite {
Composite::new(
"sig",
vec![
Sma::builder().named("a").into(),
Sma::builder().named("b").into(),
Sub::builder().into(),
],
vec![
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
],
vec![Role {
name: "price".into(),
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
source: Some(ScalarKind::F64),
}],
vec![OutField { node: 2, field: 0, name: "out".into() }],
)
.with_gangs(vec![Gang {
name: "length".into(),
kind: ScalarKind::I64,
members: vec![
GangMember { node: 0, pos: 0, name: "length".into() },
GangMember { node: 1, pos: 0, name: "length".into() },
],
}])
.expect("two open siblings gang")
}
/// The un-ganged twin of [`ganged_pair`]: identical topology, both `length`s
/// stay independently open in `param_space()`.
fn unganged_pair() -> Composite {
Composite::new(
"sig",
vec![
Sma::builder().named("a").into(),
Sma::builder().named("b").into(),
Sub::builder().into(),
],
vec![
Edge { from: 0, to: 2, slot: 0, from_field: 0 },
Edge { from: 1, to: 2, slot: 1, from_field: 0 },
],
vec![Role {
name: "price".into(),
targets: vec![Target { node: 0, slot: 0 }, Target { node: 1, slot: 0 }],
source: Some(ScalarKind::F64),
}],
vec![OutField { node: 2, field: 0, name: "out".into() }],
)
}
/// Wrap one signal composite under a recording root and run the synthetic price
/// fixture, collecting the recorded `(ts, out)` trace — the only observable
/// behaviour these tests assert on.
fn run_recording(signal: Composite, params: &[Scalar]) -> Vec<(Timestamp, Vec<Scalar>)> {
let (tx, rx) = mpsc::channel();
let root = Composite::new(
"h",
vec![
BlueprintNode::Composite(signal),
Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx).into(),
],
vec![Edge { from: 0, to: 1, slot: 0, from_field: 0 }],
vec![Role {
name: "src".into(),
targets: vec![Target { node: 0, slot: 0 }],
source: Some(ScalarKind::F64),
}],
vec![],
);
let mut h = root.bootstrap_with_params(params.to_vec()).expect("bootstraps");
h.run(vec![Box::new(VecSource::new(synthetic_prices()))]);
rx.try_iter().collect()
}
/// Property (projection, at the public boundary): `param_space()` collapses two
/// ganged sibling addresses (`a.length`, `b.length`) into ONE public address
/// (`length`), reachable using only `Composite`/`Gang`/`GangMember` exported
/// from `aura_engine`. A regression that made the projection leak both member
/// addresses (or drop the gang address entirely) would desync every downstream
/// consumer of `param_space()` — sweep grids, campaign axes, the CLI's
/// introspect output — from the point vector `compile_with_params` actually
/// expects.
#[test]
fn ganged_param_space_projects_to_one_public_address() {
let names: Vec<String> = ganged_pair().param_space().into_iter().map(|p| p.name).collect();
assert_eq!(names, ["length"], "one public knob must replace a.length/b.length");
let twin_names: Vec<String> =
unganged_pair().param_space().into_iter().map(|p| p.name).collect();
assert_eq!(twin_names, ["a.length", "b.length"], "twin keeps both addresses independently open");
}
/// THE load-bearing property (compile-front point expansion, at the public
/// boundary): binding the gang's single public value via `compile_with_params`
/// expands to the identical raw per-member value the un-ganged twin gets when
/// EVERY member is bound explicitly to that same value — both the flat wiring
/// (`FlatGraph::edges`) and the recorded run trace match, byte-for-byte,
/// through only the exported `aura_engine`/`aura_std` surface. A regression in
/// `expansion_map`'s raw-slot projection would either desync the compiled
/// wiring from the un-ganged twin or silently apply the gang's value to only
/// one member — this pins both away.
#[test]
fn ganged_compile_expands_to_member_bound_twin() {
let ganged = ganged_pair().compile_with_params(&[Scalar::i64(3)]).expect("ganged compiles");
let twin = unganged_pair()
.compile_with_params(&[Scalar::i64(3), Scalar::i64(3)])
.expect("twin compiles");
assert_eq!(ganged.edges, twin.edges, "ganged wiring diverged from the member-bound twin");
let ganged_trace = run_recording(ganged_pair(), &[Scalar::i64(3)]);
let twin_trace = run_recording(unganged_pair(), &[Scalar::i64(3), Scalar::i64(3)]);
assert!(!ganged_trace.is_empty(), "the price fixture must warm up both SMAs and record a spread");
assert_eq!(ganged_trace, twin_trace, "ganged run diverged from the member-bound twin");
}
/// Property (injectivity extends to gang addresses, at the public boundary):
/// two structurally-valid, disjoint gangs that happen to share a public name
/// are refused by `compile_with_params` with the named
/// `CompileError::DuplicateParamPath`, exactly like two colliding non-ganged
/// addresses — never silently merged into one knob driving two unrelated
/// gangs. Reachable using only the exported `Composite`/`Gang`/`GangMember`/
/// `CompileError` surface.
#[test]
fn gang_name_collision_refused_at_compile() {
let c = Composite::new(
"sig",
vec![
Sma::builder().named("a").into(),
Sma::builder().named("b").into(),
Sma::builder().named("c").into(),
Sma::builder().named("d").into(),
],
vec![],
vec![],
vec![],
)
.with_gangs(vec![
Gang {
name: "length".into(),
kind: ScalarKind::I64,
members: vec![
GangMember { node: 0, pos: 0, name: "length".into() },
GangMember { node: 1, pos: 0, name: "length".into() },
],
},
Gang {
name: "length".into(),
kind: ScalarKind::I64,
members: vec![
GangMember { node: 2, pos: 0, name: "length".into() },
GangMember { node: 3, pos: 0, name: "length".into() },
],
},
])
.expect("two structurally-valid gangs, same name, disjoint members");
let err = c.compile_with_params(&[Scalar::i64(2), Scalar::i64(2)]).err().unwrap();
assert!(
matches!(err, CompileError::DuplicateParamPath(ref p) if p == "length"),
"expected a named DuplicateParamPath collision, got {err:?}",
);
}