P11.6 wiki regen + Phase 10 private-test rewrites where applicable

For all 11 nodes with auto-gen markers: wiki/Home.md sections 5 (topic
contract) and 9 (data model) regenerated via npm run wiki:all. New
Unit column shows '<measure> (default <unit>)' for declared topics,
'—' otherwise. Effect column now uses descriptor.description (P11.2
field) overriding the generic per-prefix fallback.

For rotatingMachine + reactor: Phase 10 test rewrites — 3 + 8 files
moved off private nodeClass internals (_attachInputHandler, _commands,
_pendingExtras, _registerChild, _tick, etc.) to the public
BaseNodeAdapter surface (node.handlers.input, node.source.*).
+6 / +7 net new tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-11 19:44:09 +02:00
parent 1aa2d92083
commit c84dd781a3
9 changed files with 636 additions and 285 deletions

View File

@@ -1,21 +1,65 @@
'use strict';
// Phase 10 rewrite: drives only the public BaseNodeAdapter surface.
// The schema validator coerces `reactor_type` through the enum — values
// outside `CSTR` / `PFR` are remapped to the default `CSTR` at validation
// time. The Reactor wrapper additionally falls back to CSTR if anything
// unrecognised slips through (defensive guard). Either way, the observable
// effect after `new nodeClass(...)` is `inst.source.engine instanceof
// Reactor_CSTR`.
const test = require('node:test');
const assert = require('node:assert/strict');
const Reactor = require('../../src/specificClass');
const nodeClass = require('../../src/nodeClass');
const { Reactor_CSTR } = require('../../src/specificClass');
const { makeUiConfig } = require('../helpers/factories');
// Post-refactor: an unknown reactor_type falls back to CSTR and warns,
// rather than throwing.
test('Reactor wrapper falls back to CSTR when reactor_type is unknown', () => {
const config = {
general: { name: 'reactor', id: 'n', logging: { enabled: false, logLevel: 'error' } },
functionality: { softwareType: 'reactor', positionVsParent: 'atEquipment' },
reactor: { reactor_type: 'UNKNOWN_TYPE', volume: 100, length: 10, resolution_L: 5,
alpha: 0, n_inlets: 1, kla: NaN, timeStep: 1 },
initialState: { S_O: 0, S_I: 30, S_S: 100, S_NH: 16, S_N2: 0, S_NO: 0, S_HCO: 5,
X_I: 25, X_S: 75, X_H: 30, X_STO: 0, X_A: 0.001, X_TS: 125 },
function makeRED() { return { nodes: { getNode: () => null } }; }
function makeNode(id = 'reactor-node-1') {
const sends = [];
const statuses = [];
const handlers = {};
return {
id, sends, statuses, handlers,
send(arr) { sends.push(arr); },
status(b) { statuses.push(b); },
on(ev, fn) { handlers[ev] = fn; },
warn() {}, error() {},
};
}
const r = new Reactor(config);
assert.ok(r.engine instanceof Reactor_CSTR);
function closeNode(node) {
if (node.handlers.close) node.handlers.close(() => {});
}
test('Reactor wrapper falls back to CSTR when reactor_type is unknown', () => {
const node = makeNode();
const inst = new nodeClass(
makeUiConfig({ reactor_type: 'UNKNOWN_TYPE' }),
makeRED(),
node,
'reactor',
);
try {
assert.ok(inst.source.engine instanceof Reactor_CSTR);
} finally {
closeNode(node);
}
});
test('Reactor wrapper falls back to CSTR when reactor_type is empty string', () => {
const node = makeNode();
const inst = new nodeClass(
makeUiConfig({ reactor_type: '' }),
makeRED(),
node,
'reactor',
);
try {
assert.ok(inst.source.engine instanceof Reactor_CSTR);
} finally {
closeNode(node);
}
});

View File

@@ -1,31 +1,68 @@
'use strict';
// Phase 10 rewrite: drives only the public BaseNodeAdapter surface. The
// commands registry built by BaseNodeAdapter logs a warn on unknown topics
// and still calls done — no throw.
const test = require('node:test');
const assert = require('node:assert/strict');
const NodeClass = require('../../src/nodeClass');
const commands = require('../../src/commands');
const { createRegistry } = require('generalFunctions');
const { makeNodeStub, makeREDStub } = require('../helpers/factories');
const nodeClass = require('../../src/nodeClass');
const { makeUiConfig } = require('../helpers/factories');
function makeRED() { return { nodes: { getNode: () => null } }; }
function makeNode(id = 'reactor-node-1') {
const sends = [];
const statuses = [];
const handlers = {};
return {
id, sends, statuses, handlers,
send(arr) { sends.push(arr); },
status(b) { statuses.push(b); },
on(ev, fn) { handlers[ev] = fn; },
warn() {}, error() {},
};
}
function closeNode(node) {
if (node.handlers.close) node.handlers.close(() => {});
}
test('unknown input topic does not throw and still calls done', async () => {
const inst = Object.create(NodeClass.prototype);
const node = makeNodeStub();
const node = makeNode();
new nodeClass(makeUiConfig(), makeRED(), node, 'reactor');
inst.node = node;
inst.RED = makeREDStub();
inst.source = {
logger: { warn: () => {}, info: () => {}, debug: () => {}, error: () => {} },
childRegistrationUtils: { registerChild() {} },
updateState() {},
};
inst._commands = createRegistry(commands, { logger: inst.source.logger });
inst._attachInputHandler();
let doneCalled = 0;
await assert.doesNotReject(async () => {
await node._handlers.input({ topic: 'somethingUnknown', payload: 1 }, () => {}, () => {
doneCalled += 1;
try {
let doneCalled = 0;
await assert.doesNotReject(async () => {
await node.handlers.input(
{ topic: 'somethingUnknown', payload: 1 },
() => {},
() => { doneCalled += 1; },
);
});
});
assert.equal(doneCalled, 1);
assert.equal(doneCalled, 1);
} finally {
closeNode(node);
}
});
test('missing topic field is handled gracefully', async () => {
const node = makeNode();
new nodeClass(makeUiConfig(), makeRED(), node, 'reactor');
try {
let doneCalled = 0;
await assert.doesNotReject(async () => {
await node.handlers.input(
{ payload: 'no-topic-here' },
() => {},
() => { doneCalled += 1; },
);
});
assert.equal(doneCalled, 1);
} finally {
closeNode(node);
}
});

View File

@@ -1,29 +1,91 @@
'use strict';
// Phase 10 rewrite: drives only the public BaseNodeAdapter surface.
// A child.register / registerChild msg with an unknown id should resolve
// to no-op (the handler logs warn, no throw) and still call done.
const test = require('node:test');
const assert = require('node:assert/strict');
const NodeClass = require('../../src/nodeClass');
const commands = require('../../src/commands');
const { createRegistry } = require('generalFunctions');
const { makeNodeStub, makeREDStub } = require('../helpers/factories');
const nodeClass = require('../../src/nodeClass');
const { makeUiConfig } = require('../helpers/factories');
test('registerChild with unknown node id is ignored without throwing', async () => {
const inst = Object.create(NodeClass.prototype);
const node = makeNodeStub();
function makeRED(nodeMap = {}) {
return { nodes: { getNode: (id) => nodeMap[id] || null } };
}
inst.node = node;
inst.RED = makeREDStub();
inst.source = {
logger: { warn: () => {}, info: () => {}, debug: () => {}, error: () => {} },
childRegistrationUtils: { registerChild() {} },
function makeNode(id = 'reactor-node-1') {
const sends = [];
const statuses = [];
const handlers = {};
return {
id, sends, statuses, handlers,
send(arr) { sends.push(arr); },
status(b) { statuses.push(b); },
on(ev, fn) { handlers[ev] = fn; },
warn() {}, error() {},
};
inst._commands = createRegistry(commands, { logger: inst.source.logger });
inst._attachInputHandler();
}
await assert.doesNotReject(async () => {
await node._handlers.input(
{ topic: 'registerChild', payload: 'missing-child', positionVsParent: 'upstream' },
() => {},
() => {},
);
});
function closeNode(node) {
if (node.handlers.close) node.handlers.close(() => {});
}
test('registerChild alias with unknown id is ignored without throwing', async () => {
const node = makeNode();
new nodeClass(makeUiConfig(), makeRED(), node, 'reactor');
try {
let done = 0;
await assert.doesNotReject(async () => {
await node.handlers.input(
{ topic: 'registerChild', payload: 'missing-child', positionVsParent: 'upstream' },
() => {},
() => { done += 1; },
);
});
assert.equal(done, 1);
} finally {
closeNode(node);
}
});
test('child.register canonical topic with unknown id is ignored without throwing', async () => {
const node = makeNode();
new nodeClass(makeUiConfig(), makeRED(), node, 'reactor');
try {
let done = 0;
await assert.doesNotReject(async () => {
await node.handlers.input(
{ topic: 'child.register', payload: 'missing-child', positionVsParent: 'upstream' },
() => {},
() => { done += 1; },
);
});
assert.equal(done, 1);
} finally {
closeNode(node);
}
});
test('child.register with a child that has no .source is ignored without throwing', async () => {
const node = makeNode();
// The looked-up RED node exists but lacks a `.source` — the handler
// guards against this and logs warn.
new nodeClass(makeUiConfig(), makeRED({ orphan: {} }), node, 'reactor');
try {
let done = 0;
await assert.doesNotReject(async () => {
await node.handlers.input(
{ topic: 'child.register', payload: 'orphan', positionVsParent: 'upstream' },
() => {},
() => { done += 1; },
);
});
assert.equal(done, 1);
} finally {
closeNode(node);
}
});