//! 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_market::Resample; use aura_std::{Abs, Gt, Recorder, 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 = 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> = 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)" ); }