Files
dashboardAPI/test/basic/slice44-recursive-discovery.basic.test.js
znetsixe 990a8c09ea feat(dashboardapi): recursive subtree discovery + measurement-name/template parity
Generate dashboards for an entire parent-child subtree from a single root
registration (pre-order, cycle/diamond-safe), so wiring only the subtree root
(e.g. pumpingStation) to dashboardAPI yields dashboards for every descendant.

Fix two contract drifts that left generated panels blank against live telemetry:
- _measurement var now mirrors outputUtils.formatMsg (general.name ||
  <softwareType>_<id>); previously it always used the fallback form, so any
  named node's dashboard queried a non-existent series.
- pumpingStation template field keys realigned to emitted telemetry
  (flow.*.{upstream,out,overflow}, netFlowRate.measured, inflowLevel/
  outflowLevel/overflowLevel, maxVolAtOverflow/minVolAt{Inflow,Outflow}).

Adds template alias resolution (softwareType -> shared template file) and
locks parity with slice44/45/46 tests + output manifest. 67/67 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 09:45:37 +02:00

105 lines
4.6 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const DashboardApi = require('../../src/specificClass.js');
// Build a source node with an optional registered-child Map. `children` is an
// array of source nodes; each is wrapped in the { child, position, softwareType }
// entry shape that childRegistrationUtils.registeredChildren uses at runtime.
function makeNode(id, softwareType, children = [], positionVsParent = 'downstream') {
const map = new Map();
for (const c of children) {
map.set(c.config.general.id, {
child: c,
softwareType: c.config.functionality.softwareType,
position: c.config.functionality.positionVsParent || 'downstream',
});
}
return {
config: {
general: { id, name: id },
functionality: { softwareType, positionVsParent },
},
childRegistrationUtils: { registeredChildren: map },
};
}
test('recurses a 3-level tree from a single wired root', () => {
const api = new DashboardApi({});
// dashboardapi(root) -> machineGroup(child) -> machine(grandchild)
const grandchild = makeNode('rm-1', 'machine');
const child = makeNode('mgc-1', 'machineGroupControl', [grandchild]);
const root = makeNode('ps-1', 'pumpingStation', [child], 'atequipment');
const dashboards = api.generateDashboardsForGraph(root);
const ids = dashboards.map((d) => d.nodeId);
assert.deepEqual(ids, ['ps-1', 'mgc-1', 'rm-1'], 'pre-order: root, child, grandchild');
assert.equal(dashboards[0].nodeId, 'ps-1', 'root composed first');
});
test('each parent links only to its own direct children (per-level links)', () => {
const api = new DashboardApi({});
const grandchild = makeNode('rm-1', 'machine');
const child = makeNode('mgc-1', 'machineGroupControl', [grandchild]);
const root = makeNode('ps-1', 'pumpingStation', [child], 'atequipment');
const dashboards = api.generateDashboardsForGraph(root);
const byId = Object.fromEntries(dashboards.map((d) => [d.nodeId, d.dashboard]));
assert.equal(byId['ps-1'].links.length, 1, 'root links to its one direct child');
assert.equal(byId['ps-1'].links[0].title, 'mgc-1');
assert.equal(byId['mgc-1'].links.length, 1, 'child links to its one grandchild');
assert.equal(byId['mgc-1'].links[0].title, 'rm-1');
assert.ok(!byId['rm-1'].links || byId['rm-1'].links.length === 0, 'leaf has no child links');
});
test('cycle protection: a node reachable twice is composed once', () => {
const api = new DashboardApi({});
const a = makeNode('a', 'pumpingStation', [], 'atequipment');
const b = makeNode('b', 'machineGroupControl');
// wire a -> b and b -> a (cycle)
a.childRegistrationUtils.registeredChildren.set('b', { child: b, softwareType: 'machineGroupControl', position: 'downstream' });
b.childRegistrationUtils.registeredChildren.set('a', { child: a, softwareType: 'pumpingStation', position: 'downstream' });
const dashboards = api.generateDashboardsForGraph(a);
const ids = dashboards.map((d) => d.nodeId).sort();
assert.deepEqual(ids, ['a', 'b'], 'each node composed exactly once despite the cycle');
});
test('diamond topology: shared descendant composed once', () => {
const api = new DashboardApi({});
const shared = makeNode('shared', 'machine');
const left = makeNode('left', 'machineGroupControl', [shared]);
const right = makeNode('right', 'machineGroupControl', [shared]);
const root = makeNode('root', 'pumpingStation', [left, right], 'atequipment');
const dashboards = api.generateDashboardsForGraph(root);
const sharedCount = dashboards.filter((d) => d.nodeId === 'shared').length;
assert.equal(sharedCount, 1, 'shared grandchild gets a single dashboard');
});
test('subtreeIdsFor recurses the full subtree (great-grandchildren included)', () => {
const api = new DashboardApi({});
const ggc = makeNode('ggc-1', 'measurement');
const gc = makeNode('gc-1', 'machine', [ggc]);
const child = makeNode('child-1', 'machineGroupControl', [gc]);
const ids = api.subtreeIdsFor('dApi-1', child);
assert.ok(ids.has('dApi-1') && ids.has('child-1') && ids.has('gc-1') && ids.has('ggc-1'));
assert.equal(ids.size, 4, 'dashboardAPI + child + grandchild + great-grandchild');
});
test('includeChildren:false composes only the root (no recursion)', () => {
const api = new DashboardApi({});
const grandchild = makeNode('rm-1', 'machine');
const child = makeNode('mgc-1', 'machineGroupControl', [grandchild]);
const root = makeNode('ps-1', 'pumpingStation', [child], 'atequipment');
const dashboards = api.generateDashboardsForGraph(root, { includeChildren: false });
assert.equal(dashboards.length, 1);
assert.equal(dashboards[0].nodeId, 'ps-1');
});