test(graph_model): cover a producer backing >1 re-exported output field

#47 — close a coverage gap in the graph model serializer: a single interior
producer node backing more than one re-exported output field was never
exercised by a fixture (every existing one maps each output to a distinct
producer or a single field).

The issue was filed at cycle 0022 against `aura-cli/src/graph.rs`'s
`render_definition` / `output_binding` helper, which rendered a textual
`(n1, n2) := <producer>` tuple binding. That file and helper no longer exist —
the textual definition renderer was replaced by the JSON model emission now in
`aura-engine/src/graph_model.rs`, where the multi-OutField case is plain flat
iteration over `c.output()` (one `[name,kind]` entry + one `#N` boundary
binding per `OutField`, by `(node, field)`), not a special tuple-binding arm. So
the original branch is gone; this guards the flat emission that replaced it.

New test `multi_outfield_producer_emits_one_binding_per_field` (+ two fixtures):
a two-field producer (`hi`:f64, `lo`:i64 — distinct kinds, so the per-field
`field_kind` lookup is exercised) re-exported as both composite outputs, nested
in a root so it renders through `composite_def_json`. Asserts both fields
surface with their own kind (`"outputs":[["hi","f64"],["lo","i64"]]`) and both
bindings reference the same producer at distinct ordinals (`0.o0`/`#0`,
`0.o1`/`#1`).

Test-only; no production code touched (the behaviour was already correct).
Verified: cargo test -p aura-engine green (237 in the lib suite); clippy clean.

closes #47
This commit is contained in:
2026-06-30 16:03:29 +02:00
parent b8b83cf1c9
commit 4a3ae05db2
+61
View File
@@ -399,6 +399,49 @@ mod tests {
)
}
/// A primitive producer whose output record carries TWO fields of DIFFERENT
/// kinds (`hi`: f64, `lo`: i64) — the smallest interior node that can back more
/// than one re-exported composite output field, with distinct per-field kinds so
/// the `field_kind` lookup is meaningfully exercised (not two identical kinds).
fn two_field_producer() -> PrimitiveBuilder {
PrimitiveBuilder::new(
"Split",
NodeSchema {
inputs: vec![],
output: vec![
FieldSpec { name: "hi".into(), kind: ScalarKind::F64 },
FieldSpec { name: "lo".into(), kind: ScalarKind::I64 },
],
params: vec![],
},
|_| Box::new(Bare),
)
}
/// A root harness whose sole interior node is a composite (`splitter`) that
/// re-exports BOTH fields of one two-field producer (node 0). Nested in a root
/// so `model_to_json` renders `splitter` through the `composite_def_json` path
/// (which is what emits the `outputs` list and the `#N` boundary bindings).
fn multi_outfield_root() -> Composite {
let splitter = Composite::new(
"splitter",
vec![two_field_producer().into()],
vec![],
vec![],
vec![
OutField { node: 0, field: 0, name: "hi".into() },
OutField { node: 0, field: 1, name: "lo".into() },
],
);
Composite::new(
"root",
vec![BlueprintNode::Composite(splitter)],
vec![],
vec![],
vec![],
)
}
// -- Task 2: primitive record ------------------------------------------------
#[test]
@@ -518,6 +561,24 @@ mod tests {
assert!(def.starts_with(r#"{"inputs":[["f64","any","a"],["f64","any","b"]],"#), "{def}");
}
/// Property: a single interior producer node that backs MORE THAN ONE
/// re-exported output field is serialized as one boundary binding per field —
/// each `outputs` entry carries that field's OWN kind (`hi`→f64, `lo`→i64, not a
/// shared kind), and each `#N` binding endpoint references the SAME producer node
/// at a DISTINCT field ordinal (`o0` vs `o1`). Guards the flat per-`OutField`
/// model emission (`composite_def_json`) that replaced the retired textual
/// `(n1, n2) := <producer>` tuple-binding renderer (#47).
#[test]
fn multi_outfield_producer_emits_one_binding_per_field() {
let model = model_to_json(&multi_outfield_root());
// both fields surface, each with its OWN per-field kind (f64 vs i64):
assert!(model.contains(r#""outputs":[["hi","f64"],["lo","i64"]]"#), "{model}");
// both boundary bindings reference the SAME producer node (0) at distinct
// field ordinals — o0 → #0 and o1 → #1:
assert!(model.contains(r##"["0.o0","#0"]"##), "{model}");
assert!(model.contains(r##"["0.o1","#1"]"##), "{model}");
}
#[test]
fn structural_miswire_changes_the_model() {
let ok = model_to_json(&sample_root());