Files
generalFunctions/test/basic/ChildRouter.basic.test.js

269 lines
9.6 KiB
JavaScript
Raw Normal View History

const { test } = require('node:test');
const assert = require('node:assert/strict');
const { EventEmitter } = require('events');
const ChildRouter = require('../../src/domain/ChildRouter');
// ── helpers ────────────────────────────────────────────────────────
function makeDomain() {
const logs = [];
return {
logger: {
debug: (...a) => logs.push(['debug', ...a]),
info: (...a) => logs.push(['info', ...a]),
warn: (...a) => logs.push(['warn', ...a]),
error: (...a) => logs.push(['error', ...a]),
},
_logs: logs,
};
}
function makeChild({ id = 'c1', name = id, softwareType = 'measurement' } = {}) {
return {
config: {
general: { id, name },
functionality: { softwareType },
asset: { type: 'pressure' },
},
measurements: { emitter: new EventEmitter() },
};
}
function emitMeasured(child, type, position, value, extra = {}) {
child.measurements.emitter.emit(`${type}.measured.${position}`, { value, ...extra });
}
function emitPredicted(child, type, position, value, extra = {}) {
child.measurements.emitter.emit(`${type}.predicted.${position}`, { value, ...extra });
}
// ── tests ─────────────────────────────────────────────────────────
test('onRegister fires for the matching softwareType', () => {
const domain = makeDomain();
const router = new ChildRouter(domain);
const seen = [];
router.onRegister('measurement', (child, st) => seen.push({ id: child.config.general.id, st }));
const ch = makeChild({ id: 'm1' });
router.dispatchRegister(ch, 'measurement');
assert.equal(seen.length, 1);
assert.equal(seen[0].id, 'm1');
assert.equal(seen[0].st, 'measurement');
});
test('onMeasurement with full filter only fires for matching events', () => {
const router = new ChildRouter(makeDomain());
const hits = [];
router.onMeasurement('measurement', { type: 'pressure', position: 'upstream' },
(data, child) => hits.push({ v: data.value, id: child.config.general.id }));
const ch = makeChild({ id: 'p-up' });
router.dispatchRegister(ch, 'measurement');
emitMeasured(ch, 'pressure', 'upstream', 100);
emitMeasured(ch, 'pressure', 'downstream', 200); // ignored: wrong position
emitMeasured(ch, 'flow', 'upstream', 5); // ignored: wrong type
emitPredicted(ch, 'pressure', 'upstream', 999); // ignored: wrong variant
assert.deepEqual(hits, [{ v: 100, id: 'p-up' }]);
});
test('onMeasurement without position filter fires for all positions of the type', () => {
const router = new ChildRouter(makeDomain());
const hits = [];
router.onMeasurement('measurement', { type: 'pressure' },
(data) => hits.push(data.value));
const ch = makeChild();
router.dispatchRegister(ch, 'measurement');
emitMeasured(ch, 'pressure', 'upstream', 1);
emitMeasured(ch, 'pressure', 'downstream', 2);
emitMeasured(ch, 'pressure', 'atequipment', 3);
emitMeasured(ch, 'flow', 'upstream', 99); // ignored: wrong type
emitPredicted(ch, 'pressure', 'upstream', 50); // ignored: wrong variant
assert.deepEqual(hits.sort(), [1, 2, 3]);
});
test('onPrediction works analogously to onMeasurement', () => {
const router = new ChildRouter(makeDomain());
const hits = [];
router.onPrediction('machinegroup', { type: 'flow', position: 'downstream' },
(data) => hits.push(data.value));
const ch = makeChild({ softwareType: 'machinegroupcontrol' });
router.dispatchRegister(ch, 'machinegroupcontrol');
emitPredicted(ch, 'flow', 'downstream', 42);
emitPredicted(ch, 'flow', 'upstream', 7); // ignored: wrong position
emitMeasured(ch, 'flow', 'downstream', 99); // ignored: wrong variant
assert.deepEqual(hits, [42]);
});
test('software-type alias resolution: onRegister("machine") matches softwareType="rotatingmachine"', () => {
const router = new ChildRouter(makeDomain());
const seen = [];
router.onRegister('machine', (child) => seen.push(child.config.general.id));
const rm = makeChild({ id: 'rm-1', softwareType: 'rotatingmachine' });
router.dispatchRegister(rm, 'rotatingmachine');
assert.deepEqual(seen, ['rm-1']);
});
test('alias resolution also flows through measurement subscriptions', () => {
const router = new ChildRouter(makeDomain());
const hits = [];
// Declare with the canonical 'machine' alias.
router.onMeasurement('machine', { type: 'flow', position: 'downstream' },
(data) => hits.push(data.value));
// Child reports the raw, non-canonical softwareType.
const rm = makeChild({ id: 'rm-1', softwareType: 'rotatingmachine' });
router.dispatchRegister(rm, 'rotatingmachine');
emitMeasured(rm, 'flow', 'downstream', 17);
assert.deepEqual(hits, [17]);
});
test('tearDown removes listeners — re-emitting after tearDown does not invoke handler', () => {
const router = new ChildRouter(makeDomain());
const hits = [];
router.onMeasurement('measurement', { type: 'pressure', position: 'upstream' },
(data) => hits.push(['concrete', data.value]));
router.onMeasurement('measurement', { type: 'pressure' }, // wildcard branch
(data) => hits.push(['wild', data.value]));
const ch = makeChild();
router.dispatchRegister(ch, 'measurement');
emitMeasured(ch, 'pressure', 'upstream', 1);
assert.equal(hits.length, 2);
router.tearDown();
emitMeasured(ch, 'pressure', 'upstream', 2);
emitMeasured(ch, 'pressure', 'downstream', 3);
assert.equal(hits.length, 2, 'no further hits after tearDown');
// Original emit should be restored after teardown — sanity-check it still works
// for unrelated listeners on the same emitter.
let other = 0;
ch.measurements.emitter.on('flow.measured.upstream', () => other++);
emitMeasured(ch, 'flow', 'upstream', 9);
assert.equal(other, 1);
});
test('multiple onMeasurement subscriptions for same softwareType all fire', () => {
const router = new ChildRouter(makeDomain());
const a = []; const b = []; const c = [];
router.onMeasurement('measurement', { type: 'pressure', position: 'upstream' },
(d) => a.push(d.value));
router.onMeasurement('measurement', { type: 'pressure', position: 'upstream' },
(d) => b.push(d.value)); // duplicate concrete sub
router.onMeasurement('measurement', { type: 'pressure' },
(d) => c.push(d.value)); // wildcard-position sub
const ch = makeChild();
router.dispatchRegister(ch, 'measurement');
emitMeasured(ch, 'pressure', 'upstream', 7);
assert.deepEqual(a, [7]);
assert.deepEqual(b, [7]);
assert.deepEqual(c, [7]);
});
test('chainable API returns the router instance', () => {
const router = new ChildRouter(makeDomain());
const r = router
.onRegister('measurement', () => {})
.onMeasurement('measurement', { type: 'flow' }, () => {})
.onPrediction('machine', { type: 'flow', position: 'downstream' }, () => {});
assert.equal(r, router);
});
test('multi-parent: two routers on the same child both receive every event and tear down independently', () => {
// Regression for the pre-2026-05-11 emit-patching stack: two parents
// subscribing partial-filter wildcards on the same child must compose
// without stacking wrappers, and either teardown order must work.
const routerA = new ChildRouter(makeDomain());
const routerB = new ChildRouter(makeDomain());
const a = []; const b = [];
routerA.onMeasurement('measurement', { type: 'pressure' },
(data) => a.push(data.value));
routerB.onMeasurement('measurement', { type: 'pressure' },
(data) => b.push(data.value));
const ch = makeChild();
routerA.dispatchRegister(ch, 'measurement');
routerB.dispatchRegister(ch, 'measurement');
emitMeasured(ch, 'pressure', 'upstream', 11);
emitMeasured(ch, 'pressure', 'downstream', 22);
assert.deepEqual(a.sort(), [11, 22]);
assert.deepEqual(b.sort(), [11, 22]);
// Tear down B first — A must continue to fire on subsequent events.
routerB.tearDown();
emitMeasured(ch, 'pressure', 'upstream', 33);
assert.deepEqual(a.sort(), [11, 22, 33]);
assert.deepEqual(b.sort(), [11, 22], 'B receives nothing after its teardown');
// Now tear down A in the reverse order; neither should fire.
routerA.tearDown();
emitMeasured(ch, 'pressure', 'upstream', 44);
assert.deepEqual(a.sort(), [11, 22, 33], 'A receives nothing after its teardown');
assert.deepEqual(b.sort(), [11, 22]);
});
test('position-only filter fans out across every known type for that position', () => {
const router = new ChildRouter(makeDomain());
const hits = [];
router.onMeasurement('measurement', { position: 'upstream' },
(data) => hits.push(data.value));
const ch = makeChild();
router.dispatchRegister(ch, 'measurement');
emitMeasured(ch, 'pressure', 'upstream', 1);
emitMeasured(ch, 'flow', 'upstream', 2);
emitMeasured(ch, 'temperature', 'upstream', 3);
emitMeasured(ch, 'pressure', 'downstream', 99); // wrong position
emitPredicted(ch, 'pressure', 'upstream', 99); // wrong variant
assert.deepEqual(hits.sort(), [1, 2, 3]);
});
test('empty filter ({}) fires for every type/position combination', () => {
const router = new ChildRouter(makeDomain());
const hits = [];
router.onMeasurement('measurement', {}, (data) => hits.push(data.value));
const ch = makeChild();
router.dispatchRegister(ch, 'measurement');
emitMeasured(ch, 'pressure', 'upstream', 1);
emitMeasured(ch, 'flow', 'downstream', 2);
emitMeasured(ch, 'level', 'atequipment', 3);
emitPredicted(ch, 'flow', 'upstream', 99); // wrong variant
assert.deepEqual(hits.sort(), [1, 2, 3]);
});