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:
@@ -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
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user