From f41e319b306bc38dd2de4d82f06764a054f2df63 Mon Sep 17 00:00:00 2001 From: znetsixe Date: Wed, 27 May 2026 16:24:22 +0200 Subject: [PATCH] =?UTF-8?q?test(mgc):=20cover=20fn=5Fstatus=5Fsplit=20outp?= =?UTF-8?q?ut=2017=20(%=20of=20capacity);=20fix=20stale=2017=E2=86=9218=20?= =?UTF-8?q?count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard fan-out grew to 18 outputs (output 17 = '% of capacity' chart) but dashboard-fanout.integration.test.js still asserted 17 and had no PORT entry or coverage for output 17. Add chart_pctcap (17) with populated (State C, flow/capMax×100) and degraded (State A → null-drop) assertions, fix the count assertion, and add the fan-out enumeration table to _output-manifest.md per .claude/rules/output-coverage.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/_output-manifest.md | 27 +++++++++++++++++++ .../dashboard-fanout.integration.test.js | 19 ++++++++++--- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/test/_output-manifest.md b/test/_output-manifest.md index fec6839..59729b4 100644 --- a/test/_output-manifest.md +++ b/test/_output-manifest.md @@ -112,6 +112,33 @@ Documented in `CONTRACT.md`; tested indirectly via `group-bep-cascade.integratio --- +## Example flow fan-out — `examples/02-Dashboard.json :: fn_status_split` (outputs: 18) + +Delta-caches Port 0 then fans one msg per dashboard widget. Charts return the +whole msg as `null` (drop the output) when their source is missing — never +`{ payload: null }`. All ports covered by `test/integration/dashboard-fanout.integration.test.js`. + +| # | Target widget | Topic / payload | Populated | Degraded (missing source) | +|---|---|---|---|---| +| 0 | ui_txt_mode | string | ✔ State C | ✔ State A → mode string | +| 1 | ui_txt_flow | `'… m³/h'` | ✔ | ✔ State A → `—` | +| 2 | ui_txt_power | `'… kW'` | ✔ | ✔ → `—` | +| 3 | ui_txt_capacity | `'min – max m³/h'` | ✔ State B | ✔ → `—` | +| 4 | ui_txt_machines | `'nAct / nTot'` | ✔ | ✔ → `—` | +| 5 | ui_txt_bep (rel%) | `'… %'` | ✔ | ✔ null/undefined → `—` | +| 6 | ui_txt_eta | `'… %'` | ✔ | ✔ → `—` | +| 7 | ui_txt_eta_peak | `'… %'` | ✔ | ✔ → `—` | +| 8 | ui_txt_bep_abs | `'…'` (η pts, 3dp) | ✔ | ✔ → `—` | +| 9 | ui_txt_ncog | `'… %'` (sum/nAct) | ✔ | ✔ nAct=0/missing → `—` | +| 10 | ui_chart_flow | `{topic:'Flow', payload:number}` | ✔ | ✔ → null (drop) | +| 11 | ui_chart_flow (capacity) | `{topic:'Capacity', …}` | ✔ | ✔ → null | +| 12 | ui_chart_power | `{topic:'Power', …}` | ✔ | ✔ → null | +| 13 | ui_chart_bep | `{topic:'BEP rel %', ×100}` | ✔ | ✔ → null | +| 14 | ui_chart_eta | `{topic:'η (%)', ×100}` | ✔ | ✔ → null | +| 15 | ui_tpl_raw | `[{key,value}]` rows | ✔ | ✔ | +| 16 | ui_chart_qh (passthrough) | raw `msg.payload` | ✔ | ✔ | +| 17 | ui_chart_mgc_pctcap | `{topic:'% of capacity', payload:flow/capMax×100}` | ✔ State C | ✔ State A → null (drop) | + ## Coverage gaps (open items) These are known holes flagged during the 2026-05-14 governance review; not yet diff --git a/test/integration/dashboard-fanout.integration.test.js b/test/integration/dashboard-fanout.integration.test.js index 241052c..42a4acd 100644 --- a/test/integration/dashboard-fanout.integration.test.js +++ b/test/integration/dashboard-fanout.integration.test.js @@ -22,7 +22,7 @@ function runFn(msgs) { return msgs.map(msg => fn_body(msg, context)); } -// Indices into the 17-output return array. Kept here as the manifest contract +// Indices into the 18-output return array. Kept here as the manifest contract // for this function — every test below references these names, never raw ints. const PORT = { text_mode: 0, text_flow: 1, text_power: 2, text_capacity: 3, @@ -31,6 +31,7 @@ const PORT = { chart_flow: 10, chart_capacity: 11, chart_power: 12, chart_bep_rel: 13, chart_eta: 14, raw_rows: 15, raw_passthrough: 16, + chart_pctcap: 17, }; const initialMsg = { @@ -64,9 +65,9 @@ const postDemandMsg = { }, }; -test('manifest: function has exactly 17 outputs and wires array matches', () => { - assert.equal(fn.outputs, 17); - assert.equal(fn.wires.length, 17); +test('manifest: function has exactly 18 outputs and wires array matches', () => { + assert.equal(fn.outputs, 18); + assert.equal(fn.wires.length, 18); }); test('State A (deploy-time): no AT_EQUIPMENT keys → flow/power text show em-dash', () => { @@ -113,6 +114,16 @@ test('State C (post-demand): every text/chart output has real value', () => { assert.equal(out[PORT.chart_flow].payload, 200); assert.equal(out[PORT.chart_power].payload, 11.4); assert.equal(out[PORT.chart_eta].payload, 62); + // % of capacity = flow / flowCapacityMax × 100 = 200 / 450 × 100 ≈ 44.44. + assert.equal(out[PORT.chart_pctcap].topic, '% of capacity'); + assert.ok(Math.abs(out[PORT.chart_pctcap].payload - (200 / 450) * 100) < 1e-6); +}); + +test('% of capacity chart: drops msg when flow or capacity missing (no payload:null)', () => { + // State A: no flow + flowCapacityMax=0 → pctCap undefined → chart() returns + // null so the function node skips the output, never { payload: null }. + const [out] = runFn([initialMsg]); + assert.equal(out[PORT.chart_pctcap], null, 'chart_pctcap must drop msg when source missing'); }); test('NCog formatter: SUM is normalized by machineCountActive before display', () => {