Files
Aura/crates/aura-engine/tests/when_e2e.rs
T
claude c8dc57e2aa feat(std): CumSum and When cells; roster complete at 33
Second half of the #281 clock-enable set. CumSum is the generic
run-length accumulator with O(1) Neumaier-compensated state — the
branching Neumaier variant, not Sma's plain Kahan, so it stays exact
when a large running total absorbs a tiny sample (the standalone
accumulator has no windowed-magnitude guarantee; module doc explains
why the helper is not reused). When is the clock-enable driver: forwards
value iff gate, else a quiet cycle (eval->None), so every stateful
reducer composes gated unmodified — SMA(When(x,g),N) is the gated SMA.

Rosters CumSum and When (both count pins 31->33) and ships two engine
E2E fixtures: When forwarding/quiet-downstream through the real
GraphBuilder->compile->run seam, and a quiet When leg suppressing a
whole Resample Barrier(0) group (the deliberate stall semantics).

refs #281
2026-07-17 20:39:56 +02:00

134 lines
6.8 KiB
Rust

//! End-to-end coverage for the `When` standard cell (#281): the clock-enable
//! `f64 x bool -> f64` driver.
//!
//! The in-module tests in `aura-std/src/when.rs` drive `eval` directly — they
//! never go through a `PrimitiveBuilder` schema, a `GraphBuilder`-resolved
//! wiring, or a compiled+bootstrapped `FlatGraph`. That is the real seam a
//! data-authored op-script or a Rust builder actually exercises (C24); this
//! file proves `When` is reachable from the public `aura_engine`/`aura_std`
//! construction surface, wires cleanly by declared port name/kind (`value:
//! F64`, `gate: Bool`), and — the load-bearing property — that a quiet
//! (gate-false) cycle never reaches a downstream sink: the recorder's own
//! input only advances on a fresh push from `When`, so a quiet cycle leaves
//! it un-fired rather than recording a held/zero value.
//!
//! One `price` source feeds both `When`'s `value` leg and, via
//! `Gt(|price|, price)`, its `gate` leg (true exactly when `price < 0`) — the
//! same single-source-per-cycle shape `select_sign_e2e.rs` uses, so `gate`
//! and `value` are always computed in the same engine cycle (no cross-source
//! phase skew between two independently-clocked inputs).
use std::sync::mpsc;
use aura_core::{Firing, Scalar, ScalarKind, Timestamp};
use aura_engine::{GraphBuilder, VecSource};
use aura_std::{Abs, Gt, Recorder, Resample, When};
/// A deterministic `f64` tick stream, no timestamps/randomness beyond a fixed
/// literal sequence.
fn ticks(values: &[f64]) -> Vec<(Timestamp, Scalar)> {
values.iter().enumerate().map(|(i, &v)| (Timestamp(i as i64 + 1), Scalar::f64(v))).collect()
}
/// Property: **`When` correctly registers its `PrimitiveBuilder` schema
/// (`value: F64`, `gate: Bool` in, one `f64` out) and dispatches through a
/// compiled+bootstrapped `FlatGraph`, forwarding `value` on gate-true cycles
/// and going quiet — not merely holding or zeroing — on gate-false cycles,
/// all the way to a downstream sink.** A schema/wiring regression (wrong port
/// kind, wrong output arity) would fail at `GraphBuilder::build` or
/// `compile_with_params`, before a single cycle runs; a dispatch regression
/// would show up in the recorded trace below — either the wrong values or,
/// were quiet cycles wrongly forwarding a held/zero value, extra rows.
#[test]
fn when_dispatches_through_a_compiled_graph_and_is_quiet_downstream_on_gate_false() {
let (tx, rx) = mpsc::channel();
let mut g = GraphBuilder::new("root");
let price = g.source_role("price", ScalarKind::F64);
let abs = g.add(Abs::builder());
let gt = g.add(Gt::builder());
let when = g.add(When::builder());
let rec = g.add(Recorder::builder(vec![ScalarKind::F64], Firing::Any, tx));
g.feed(price, [abs.input("value"), gt.input("b"), when.input("value")]);
g.connect(abs.output("value"), gt.input("a"));
g.connect(gt.output("value"), when.input("gate"));
g.connect(when.output("value"), rec.input("col[0]"));
let mut h = g.build().expect("resolves by name").bootstrap_with_params(vec![]).expect("bootstraps");
h.run(vec![Box::new(VecSource::new(ticks(&[-3.0, 2.5, -1.0, 4.0])))]);
let trace: Vec<f64> = rx.try_iter().map(|(_, row)| row[0].as_f64()).collect();
assert_eq!(
trace,
vec![-3.0, -1.0],
"only the two negative-price (gate-true) rows reach the sink; the positive-price rows are quiet, not recorded as held/zero"
);
}
/// Property: **a quiet `When` (gate false) driving one leg of a `Firing::Barrier(N)`
/// group suppresses the WHOLE group that cycle — not merely its own leg.** This is
/// the composition the `when.rs` module doc calls out directly ("a quiet `When`
/// feeding a `Firing::Barrier(N)` group means the group does not fire that cycle").
/// `Resample`'s four OHLC inputs are `aura-std`'s only `Barrier(0)` consumer; here
/// `When` drives `close` while `open`/`high`/`low` are wired straight from the same
/// source (fresh every cycle, gate or no gate). On a gate-false tick the other three
/// legs still receive a fresh push, but `close` does not — since `Barrier(0)`
/// requires all four co-fresh, the node does not fire at all that cycle, so the
/// gate-false tick's price never enters the OHLC accumulator (not even via the
/// ungated legs), and a bucket rollover due exactly on a gate-false tick is
/// deferred to the next gate-true tick rather than firing early or partially.
#[test]
fn quiet_when_leg_suppresses_the_whole_barrier_group() {
let (tx, rx) = mpsc::channel();
let mut g = GraphBuilder::new("root");
let price = g.source_role("price", ScalarKind::F64);
let abs = g.add(Abs::builder());
let gt = g.add(Gt::builder());
let when = g.add(When::builder());
let resample = g.add(Resample::builder());
let rec = g.add(Recorder::builder(vec![ScalarKind::F64; 4], Firing::Any, tx));
g.feed(price, [
abs.input("value"),
gt.input("b"),
when.input("value"),
resample.input("open"),
resample.input("high"),
resample.input("low"),
]);
g.connect(abs.output("value"), gt.input("a"));
g.connect(gt.output("value"), when.input("gate"));
g.connect(when.output("value"), resample.input("close"));
g.connect(resample.output("open"), rec.input("col[0]"));
g.connect(resample.output("high"), rec.input("col[1]"));
g.connect(resample.output("low"), rec.input("col[2]"));
g.connect(resample.output("close"), rec.input("col[3]"));
let mut h = g
.build()
.expect("resolves by name")
.bootstrap_with_params(vec![Scalar::i64(1)]) // Resample's sole open param: period_minutes=1
.expect("bootstraps");
// gate = price < 0 (Gt(|price|, price)); 1-minute buckets (period_ns = 60e9).
let feed: [(i64, f64); 5] = [
(0, -1.0), // gate true: bucket 0 opens (o=h=l=c=-1.0)
(10_000_000_000, 2.0), // gate false: When quiet -> Resample does not fire at all
(20_000_000_000, -3.0), // gate true, still bucket 0: high stays -1.0, low->-3.0, close->-3.0
(60_000_000_000, 4.0), // gate false exactly on the bucket-1 boundary: rollover DEFERRED
(70_000_000_000, -6.0), // gate true, bucket 1: fires -> rolls over the completed bucket-0 bar
];
let ticks: Vec<(Timestamp, Scalar)> =
feed.iter().map(|&(t, v)| (Timestamp(t), Scalar::f64(v))).collect();
h.run(vec![Box::new(VecSource::new(ticks))]);
let trace: Vec<Vec<f64>> =
rx.try_iter().map(|(_, row)| row.iter().map(|c| c.as_f64()).collect()).collect();
assert_eq!(
trace,
vec![vec![-1.0, -1.0, -3.0, -3.0]],
"the two gate-false ticks (price 2.0, 4.0) must never enter the OHLC accumulator: \
a quiet When leg blocks the whole Barrier(0) group, not just its own field, and the \
bucket-1 rollover due at t=60e9 is deferred to the next gate-true tick (t=70e9)"
);
}