feat(rotatingMachine): resolve supplier+type from asset registry, drop denormalized fields

specificClass._setupCurves now calls assetResolver.resolveAssetMetadata
to derive supplier/type/units from the model id, instead of trusting
denormalized fields on the node config. If the model isn't in the
registry, installs a null-predictor stub and logs a clear "pick a model
from the asset menu" error rather than crashing.

rotatingMachine.html: defaults block trimmed (supplier/category/assetType
were stale copies of registry data).

Tests:
- New test/basic/assetMetadata.basic.test.js covers the registry-resolve
  path and the missing-model fallback.
- nodeClass-config / error-paths / nodeClass-routing / factories /
  abort-deadlock fixtures updated to the trimmed asset shape.
- 209/209 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-12 17:12:33 +02:00
parent b373727338
commit 28344c6810
9 changed files with 148 additions and 18 deletions

View File

@@ -0,0 +1,61 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const Machine = require('../../src/specificClass');
// Phase 4 regression: after the AssetResolver cutover the node must
// (a) derive supplier/type/units from the registry, not from saved config,
// (b) hard-fail with a clear log if asset.model is missing,
// (c) hard-fail if asset.unit is missing or not in registry's allowed set,
// (d) succeed with a known good model + unit.
function makeConfig({ model = 'hidrostal-H05K-S03R', unit = 'm3/h' } = {}) {
return {
general: { id: 'test-node', name: 'Pump-T', logging: { enabled: false } },
asset: { model, unit, curveUnits: { pressure: 'mbar', flow: unit, power: 'kW', control: '%' } },
functionality: { softwareType: 'rotatingmachine' },
};
}
test('asset metadata is derived from the registry, not from config', () => {
const m = new Machine(makeConfig());
assert.ok(m.assetMetadata, 'expected assetMetadata to be populated');
assert.equal(m.assetMetadata.supplier, 'Hidrostal');
assert.equal(m.assetMetadata.type, 'Centrifugal');
assert.ok(Array.isArray(m.assetMetadata.units));
assert.ok(m.assetMetadata.units.length > 0);
});
test('valid model + unit yields working curve predictors', () => {
const m = new Machine(makeConfig());
assert.equal(m.hasCurve, true);
assert.equal(typeof m.predictFlow, 'object');
assert.equal(typeof m.predictPower, 'object');
});
test('missing model installs null predictors (degraded mode)', () => {
const m = new Machine(makeConfig({ model: null }));
assert.equal(m.hasCurve, false);
assert.equal(m.predictFlow, null);
assert.equal(m.predictPower, null);
});
test('unknown model installs null predictors and logs', () => {
const m = new Machine(makeConfig({ model: 'no-such-model-xyz' }));
assert.equal(m.hasCurve, false);
assert.equal(m.assetMetadata, null);
});
test('unit not in registry allowed-set installs null predictors', () => {
const m = new Machine(makeConfig({ unit: 'furlongs-per-fortnight' }));
assert.equal(m.hasCurve, false);
});
test('two machines with the same model get independent assetMetadata instances', () => {
const a = new Machine(makeConfig());
const b = new Machine(makeConfig());
assert.notStrictEqual(a, b);
assert.equal(a.assetMetadata.supplier, b.assetMetadata.supplier);
});

View File

@@ -9,13 +9,13 @@ const { makeNodeStub, makeREDStub } = require('../helpers/factories');
// validated merged shape) and the source's runtime mode. No private hooks.
function makeUiConfig(overrides = {}) {
// After the AssetResolver cutover, the editor no longer saves
// supplier/category/assetType — those are derived from the model id via
// assetResolver.resolveAssetMetadata at runtime.
return {
unit: 'm3/h',
enableLog: false,
logLevel: 'error',
supplier: 'hidrostal',
category: 'machine',
assetType: 'pump',
model: 'hidrostal-H05K-S03R',
curvePressureUnit: 'mbar',
curveFlowUnit: 'm3/h',

View File

@@ -6,9 +6,9 @@ const NodeClass = require('../../src/nodeClass');
const { makeMachineConfig, makeStateConfig, makeNodeStub, makeREDStub } = require('../helpers/factories');
function makeUiConfig(overrides = {}) {
// Post-AssetResolver: editor saves only model + unit + uuid/tagCode.
return {
unit: 'm3/h', enableLog: false, logLevel: 'error',
supplier: 'hidrostal', category: 'machine', assetType: 'pump',
model: 'hidrostal-H05K-S03R',
curvePressureUnit: 'mbar', curveFlowUnit: 'm3/h',
curvePowerUnit: 'kW', curveControlUnit: '%',

View File

@@ -11,13 +11,12 @@ const { makeNodeStub, makeREDStub } = require('../helpers/factories');
// source, and instrumented domain methods.
function makeUiConfig(overrides = {}) {
// Post-AssetResolver: editor saves only model + unit + uuid/tagCode.
// supplier/category/assetType are derived at runtime.
return {
unit: 'm3/h',
enableLog: false,
logLevel: 'error',
supplier: 'hidrostal',
category: 'machine',
assetType: 'pump',
model: 'hidrostal-H05K-S03R',
curvePressureUnit: 'mbar',
curveFlowUnit: 'm3/h',

View File

@@ -11,10 +11,11 @@ function makeMachineConfig(overrides = {}) {
functionality: {
positionVsParent: 'atEquipment',
},
// Post-AssetResolver: only model + unit + tagCode/uuid are saved on the
// node. supplier/category/type are derived from the registry. Keeping
// legacy fields in the factory would trip the strict-cutover guard in
// nodeClass.buildDomainConfig.
asset: {
supplier: 'hidrostal',
category: 'machine',
type: 'pump',
model: 'hidrostal-H05K-S03R',
unit: 'm3/h',
curveUnits: {

View File

@@ -36,8 +36,7 @@ function machineConfig() {
general: { id: 'p1', name: 'p1', unit: 'm3/h',
logging: { enabled: false, logLevel: 'error' } },
functionality: { softwareType: 'machine', role: 'rotationaldevicecontroller' },
asset: { category: 'pump', type: 'centrifugal',
model: 'hidrostal-H05K-S03R', supplier: 'hidrostal' },
asset: { model: 'hidrostal-H05K-S03R', unit: 'm3/h' },
mode: {
current: 'auto',
allowedActions: { auto: ['execsequence', 'execmovement', 'flowmovement', 'statuscheck'] },