2026-03-11 11:13:26 +01:00
|
|
|
const test = require('node:test');
|
|
|
|
|
const assert = require('node:assert/strict');
|
|
|
|
|
|
|
|
|
|
const NodeClass = require('../../src/nodeClass');
|
2026-05-11 19:44:11 +02:00
|
|
|
const { makeNodeStub, makeREDStub } = require('../helpers/factories');
|
2026-03-11 11:13:26 +01:00
|
|
|
|
2026-05-11 19:44:11 +02:00
|
|
|
// These tests drive the BaseNodeAdapter public surface. We construct the
|
|
|
|
|
// full nodeClass and observe the resulting `inst.source.config` (the
|
|
|
|
|
// validated merged shape) and the source's runtime mode. No private hooks.
|
2026-05-10 22:00:34 +02:00
|
|
|
|
2026-03-11 11:13:26 +01:00
|
|
|
function makeUiConfig(overrides = {}) {
|
2026-05-12 17:12:33 +02:00
|
|
|
// After the AssetResolver cutover, the editor no longer saves
|
|
|
|
|
// supplier/category/assetType — those are derived from the model id via
|
|
|
|
|
// assetResolver.resolveAssetMetadata at runtime.
|
2026-03-11 11:13:26 +01:00
|
|
|
return {
|
|
|
|
|
unit: 'm3/h',
|
2026-05-10 22:00:34 +02:00
|
|
|
enableLog: false,
|
|
|
|
|
logLevel: 'error',
|
2026-03-11 11:13:26 +01:00
|
|
|
model: 'hidrostal-H05K-S03R',
|
|
|
|
|
curvePressureUnit: 'mbar',
|
|
|
|
|
curveFlowUnit: 'm3/h',
|
|
|
|
|
curvePowerUnit: 'kW',
|
|
|
|
|
curveControlUnit: '%',
|
|
|
|
|
positionVsParent: 'atEquipment',
|
|
|
|
|
speed: 1,
|
|
|
|
|
movementMode: 'staticspeed',
|
|
|
|
|
startup: 0,
|
|
|
|
|
warmup: 0,
|
|
|
|
|
shutdown: 0,
|
|
|
|
|
cooldown: 0,
|
|
|
|
|
...overrides,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 19:44:11 +02:00
|
|
|
// Adapters built by these tests park a periodic status-poll timer. We
|
|
|
|
|
// drive the BaseNodeAdapter close handler after each test to stop it so
|
|
|
|
|
// node:test exits cleanly — this is the public teardown path Node-RED
|
|
|
|
|
// itself uses on flow shutdown.
|
|
|
|
|
const _adapters = [];
|
|
|
|
|
function buildAdapter(ui) {
|
|
|
|
|
const node = makeNodeStub();
|
|
|
|
|
const RED = makeREDStub();
|
|
|
|
|
const inst = new NodeClass(ui, RED, node, 'rotatingMachine');
|
|
|
|
|
_adapters.push(node);
|
|
|
|
|
return { inst, node };
|
2026-05-10 22:00:34 +02:00
|
|
|
}
|
2026-05-11 19:44:11 +02:00
|
|
|
test.afterEach(() => {
|
|
|
|
|
while (_adapters.length) {
|
|
|
|
|
const node = _adapters.pop();
|
|
|
|
|
try { node._handlers.close?.(() => {}); } catch (_) { /* best effort */ }
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-03-11 11:13:26 +01:00
|
|
|
|
2026-05-11 19:44:11 +02:00
|
|
|
test('asset identity flows from legacy editor fields through buildDomainConfig', () => {
|
|
|
|
|
const { inst } = buildAdapter(makeUiConfig({ uuid: 'uuid-from-editor', assetTagNumber: 'TAG-123' }));
|
|
|
|
|
assert.equal(inst.source.config.asset.uuid, 'uuid-from-editor');
|
|
|
|
|
assert.equal(inst.source.config.asset.tagCode, 'tag-123');
|
|
|
|
|
assert.equal(inst.source.config.asset.tagNumber, 'tag-123');
|
2026-03-11 11:13:26 +01:00
|
|
|
});
|
|
|
|
|
|
2026-05-11 19:44:11 +02:00
|
|
|
test('explicit assetUuid/assetTagCode override legacy editor fields', () => {
|
|
|
|
|
const { inst } = buildAdapter(makeUiConfig({
|
2026-05-10 22:00:34 +02:00
|
|
|
uuid: 'legacy-uuid', assetUuid: 'explicit-uuid',
|
|
|
|
|
assetTagNumber: 'legacy-tag', assetTagCode: 'explicit-tag',
|
|
|
|
|
}));
|
2026-05-11 19:44:11 +02:00
|
|
|
assert.equal(inst.source.config.asset.uuid, 'explicit-uuid');
|
|
|
|
|
assert.equal(inst.source.config.asset.tagCode, 'explicit-tag');
|
2026-03-11 11:13:26 +01:00
|
|
|
});
|
|
|
|
|
|
2026-05-11 19:44:11 +02:00
|
|
|
test('curveUnits propagate through buildDomainConfig, invalid flow unit falls back', () => {
|
|
|
|
|
const { inst } = buildAdapter(makeUiConfig({
|
2026-05-10 22:00:34 +02:00
|
|
|
unit: 'not-a-unit',
|
|
|
|
|
curvePressureUnit: 'mbar', curveFlowUnit: 'm3/h',
|
|
|
|
|
curvePowerUnit: 'kW', curveControlUnit: '%',
|
|
|
|
|
}));
|
2026-05-11 19:44:11 +02:00
|
|
|
assert.equal(inst.source.config.general.unit, 'm3/h');
|
|
|
|
|
assert.equal(inst.source.config.asset.unit, 'm3/h');
|
|
|
|
|
assert.equal(inst.source.config.asset.curveUnits.pressure, 'mbar');
|
|
|
|
|
assert.equal(inst.source.config.asset.curveUnits.flow, 'm3/h');
|
|
|
|
|
assert.equal(inst.source.config.asset.curveUnits.power, 'kW');
|
|
|
|
|
assert.equal(inst.source.config.asset.curveUnits.control, '%');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('logging.enabled flag reaches the domain via configManager.buildConfig', () => {
|
|
|
|
|
const { inst } = buildAdapter(makeUiConfig({ enableLog: true }));
|
|
|
|
|
// uiConfig.enableLog flows through configManager.buildConfig and lands
|
|
|
|
|
// on the validated source config. (logLevel currently doesn't propagate
|
|
|
|
|
// — known platform behaviour; not exercised here.)
|
|
|
|
|
assert.equal(inst.source.config.general.logging.enabled, true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('state machine is wired and exposes its public surface', () => {
|
|
|
|
|
const { inst } = buildAdapter(makeUiConfig());
|
|
|
|
|
// The state machine is constructed during configure() and exposes
|
|
|
|
|
// observable methods used by the rest of the domain + the status badge.
|
|
|
|
|
assert.equal(typeof inst.source.state.getCurrentState, 'function');
|
|
|
|
|
assert.equal(typeof inst.source.state.getCurrentPosition, 'function');
|
|
|
|
|
assert.equal(inst.source.state.getCurrentState(), 'idle');
|
2026-03-11 11:13:26 +01:00
|
|
|
});
|
|
|
|
|
|
2026-05-11 19:44:11 +02:00
|
|
|
test('default mode is honoured on the constructed source', () => {
|
|
|
|
|
const { inst } = buildAdapter(makeUiConfig());
|
|
|
|
|
assert.equal(typeof inst.source.currentMode, 'string');
|
|
|
|
|
assert.ok(inst.source.currentMode.length > 0);
|
2026-03-11 11:13:26 +01:00
|
|
|
});
|