const { test } = require('node:test'); const assert = require('node:assert/strict'); const { EventEmitter } = require('events'); const BaseDomain = require('../../src/domain/BaseDomain'); const UnitPolicy = require('../../src/domain/UnitPolicy'); // ── Subclasses ──────────────────────────────────────────────────────── // Minimal subclass — relies on every base default. Uses 'measurement' so the // configManager finds a real config schema in src/configs/measurement.json. class PlainMeasurement extends BaseDomain { static name = 'measurement'; } // Subclass that records call ordering and exposes hooks. class TrackingMeasurement extends BaseDomain { static name = 'measurement'; configure() { this.calls = this.calls || []; // Pin the moment at which `configure` runs — these MUST be populated // before the hook fires. this.calls.push({ hook: 'configure', hasConfig: !!this.config, hasMeasurements: !!this.measurements, }); } _init() { this.calls = this.calls || []; this.calls.push({ hook: '_init' }); } } // Subclass with a UnitPolicy — verify containerOptions reach MeasurementContainer. class PolicyMeasurement extends BaseDomain { static name = 'measurement'; static unitPolicy = UnitPolicy.declare({ canonical: { flow: 'm3/s', pressure: 'Pa' }, output: { flow: 'L/s', pressure: 'kPa' }, }); } // Subclass that declares a child getter in `configure`. class ParentDomain extends BaseDomain { static name = 'measurement'; configure() { this.declareChildGetter('machines', 'machine'); } } // ── Helpers ────────────────────────────────────────────────────────── function makeChild({ id = 'c1', name = id, softwareType = 'machine', category = 'centrifugal' } = {}) { return { config: { general: { id, name }, functionality: { softwareType }, asset: { category, type: 'pump' }, }, measurements: { emitter: new EventEmitter(), setChildId() {}, setChildName() {}, setParentRef() {}, }, }; } // ── Tests ──────────────────────────────────────────────────────────── test('constructs successfully against a real config schema', () => { const m = new PlainMeasurement({}); assert.ok(m.config?.general?.name); assert.ok(m.measurements); assert.ok(m.logger); assert.ok(m.emitter); assert.ok(m.childRegistrationUtils); assert.ok(m.router); }); test('configure() runs after config + measurements are populated, exactly once', () => { const m = new TrackingMeasurement({}); const configureCalls = m.calls.filter(c => c.hook === 'configure'); assert.equal(configureCalls.length, 1); assert.equal(configureCalls[0].hasConfig, true); assert.equal(configureCalls[0].hasMeasurements, true); }); test('_init() runs after configure()', () => { const m = new TrackingMeasurement({}); const order = m.calls.map(c => c.hook); assert.deepEqual(order, ['configure', '_init']); }); test('static unitPolicy is honored — defaultUnits reflect output map', () => { const m = new PolicyMeasurement({}); // PolicyMeasurement declares output.flow='L/s', output.pressure='kPa' assert.equal(m.measurements.defaultUnits.flow, 'L/s'); assert.equal(m.measurements.defaultUnits.pressure, 'kPa'); // Canonical flow was declared as 'm3/s' assert.equal(m.measurements.canonicalUnits.flow, 'm3/s'); }); test('without unitPolicy, MeasurementContainer keeps its built-in defaults', () => { const m = new PlainMeasurement({}); assert.equal(m.unitPolicy, null); // Built-in defaults from MeasurementContainer. assert.equal(m.measurements.defaultUnits.flow, 'm3/h'); assert.equal(m.measurements.defaultUnits.pressure, 'mbar'); assert.equal(m.measurements.autoConvert, true); }); test('declareChildGetter flattens registry slice across categories', () => { const p = new ParentDomain({}); // Empty before any registration. assert.deepEqual(p.machines, {}); // Mirror what childRegistrationUtils._storeChild does: child.machine.=[...] const a = makeChild({ id: 'pumpA', category: 'centrifugal' }); const b = makeChild({ id: 'pumpB', category: 'positivedisplacement' }); p.child = { machine: { centrifugal: [a], positivedisplacement: [b] } }; const flat = p.machines; assert.deepEqual(Object.keys(flat).sort(), ['pumpA', 'pumpB']); assert.equal(flat.pumpA, a); assert.equal(flat.pumpB, b); }); test('notifyOutputChanged fires "output-changed" on emitter', () => { const m = new PlainMeasurement({}); let count = 0; m.emitter.on('output-changed', () => count++); m.notifyOutputChanged(); m.notifyOutputChanged(); assert.equal(count, 2); }); test('context() returns a frozen object with the documented keys', () => { const m = new PlainMeasurement({}); const ctx = m.context(); assert.ok(Object.isFrozen(ctx)); for (const k of ['config', 'logger', 'measurements', 'emitter', 'child', 'unitPolicy', 'router']) { assert.ok(k in ctx, `context() missing key '${k}'`); } assert.equal(ctx.config, m.config); assert.equal(ctx.measurements, m.measurements); }); test('close() removes emitter listeners and tears down router', () => { const m = new PlainMeasurement({}); let teardownCount = 0; const origTeardown = m.router.tearDown.bind(m.router); m.router.tearDown = () => { teardownCount++; origTeardown(); }; m.emitter.on('output-changed', () => {}); assert.equal(m.emitter.listenerCount('output-changed'), 1); m.close(); assert.equal(teardownCount, 1); assert.equal(m.emitter.listenerCount('output-changed'), 0); }); test('registerChild delegates to router.dispatchRegister', () => { const m = new PlainMeasurement({}); const seen = []; const origDispatch = m.router.dispatchRegister.bind(m.router); m.router.dispatchRegister = (child, st) => { seen.push({ id: child.config.general.id, st }); return origDispatch(child, st); }; const child = makeChild({ id: 'kid1', softwareType: 'measurement' }); const result = m.registerChild(child, 'measurement'); assert.equal(result, true); assert.deepEqual(seen, [{ id: 'kid1', st: 'measurement' }]); }); test('childRegistrationUtils.registerChild flows through router (end-to-end handshake)', async () => { const m = new PlainMeasurement({}); let routed = null; m.router.onRegister('measurement', (child, st) => { routed = { id: child.config.general.id, st }; }); const child = makeChild({ id: 'kid2', softwareType: 'measurement' }); await m.childRegistrationUtils.registerChild(child, 'upstream', 0); assert.deepEqual(routed, { id: 'kid2', st: 'measurement' }); }); test('direct BaseDomain instantiation throws (abstract)', () => { assert.throws(() => new BaseDomain({}), /abstract/); });