test(mgc): cover fn_status_split output 17 (% of capacity); fix stale 17→18 count

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) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-27 16:24:22 +02:00
parent 551ee6d70e
commit f41e319b30
2 changed files with 42 additions and 4 deletions

View File

@@ -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) ## Coverage gaps (open items)
These are known holes flagged during the 2026-05-14 governance review; not yet These are known holes flagged during the 2026-05-14 governance review; not yet

View File

@@ -22,7 +22,7 @@ function runFn(msgs) {
return msgs.map(msg => fn_body(msg, context)); 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. // for this function — every test below references these names, never raw ints.
const PORT = { const PORT = {
text_mode: 0, text_flow: 1, text_power: 2, text_capacity: 3, 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_flow: 10, chart_capacity: 11, chart_power: 12, chart_bep_rel: 13,
chart_eta: 14, chart_eta: 14,
raw_rows: 15, raw_passthrough: 16, raw_rows: 15, raw_passthrough: 16,
chart_pctcap: 17,
}; };
const initialMsg = { const initialMsg = {
@@ -64,9 +65,9 @@ const postDemandMsg = {
}, },
}; };
test('manifest: function has exactly 17 outputs and wires array matches', () => { test('manifest: function has exactly 18 outputs and wires array matches', () => {
assert.equal(fn.outputs, 17); assert.equal(fn.outputs, 18);
assert.equal(fn.wires.length, 17); assert.equal(fn.wires.length, 18);
}); });
test('State A (deploy-time): no AT_EQUIPMENT keys → flow/power text show em-dash', () => { 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_flow].payload, 200);
assert.equal(out[PORT.chart_power].payload, 11.4); assert.equal(out[PORT.chart_power].payload, 11.4);
assert.equal(out[PORT.chart_eta].payload, 62); 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', () => { test('NCog formatter: SUM is normalized by machineCountActive before display', () => {