Compare commits
1 Commits
1aa2d92083
...
c84dd781a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c84dd781a3 |
@@ -1,65 +1,83 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Phase 10 rewrite: drives only the public BaseNodeAdapter surface.
|
||||||
|
// The pre-refactor _loadConfig / _setupClass private methods are gone —
|
||||||
|
// config build is exposed via buildDomainConfig (override hook in
|
||||||
|
// CONTRACTS.md §2), and engine selection is observable via
|
||||||
|
// `inst.source.engine instanceof Reactor_CSTR | Reactor_PFR` after a
|
||||||
|
// full `new nodeClass(...)` construction.
|
||||||
|
|
||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
const NodeClass = require('../../src/nodeClass');
|
const nodeClass = require('../../src/nodeClass');
|
||||||
const { Reactor_CSTR, Reactor_PFR } = require('../../src/specificClass');
|
const { Reactor_CSTR, Reactor_PFR } = require('../../src/specificClass');
|
||||||
const { makeUiConfig } = require('../helpers/factories');
|
const { makeUiConfig } = require('../helpers/factories');
|
||||||
|
|
||||||
// These tests pinned the old private _loadConfig / _setupClass methods on
|
function makeRED() { return { nodes: { getNode: () => null } }; }
|
||||||
// the pre-refactor nodeClass. After the BaseNodeAdapter migration the
|
|
||||||
// same logic lives in buildDomainConfig + the Reactor wrapper's engine
|
function makeNode(id = 'reactor-1') {
|
||||||
// selector. We exercise both surfaces directly.
|
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('buildDomainConfig coerces numeric fields and builds initial state vector', () => {
|
test('buildDomainConfig coerces numeric fields and builds initial state vector', () => {
|
||||||
const inst = Object.create(NodeClass.prototype);
|
const node = makeNode();
|
||||||
inst.node = { id: 'n-reactor-1' };
|
const inst = new nodeClass(makeUiConfig(), makeRED(), node, 'reactor');
|
||||||
inst.name = 'reactor';
|
try {
|
||||||
const dc = inst.buildDomainConfig(
|
const dc = inst.buildDomainConfig(
|
||||||
makeUiConfig({
|
makeUiConfig({
|
||||||
volume: '12.5',
|
volume: '12.5',
|
||||||
length: '9',
|
length: '9',
|
||||||
resolution_L: '7',
|
resolution_L: '7',
|
||||||
alpha: '0.5',
|
alpha: '0.5',
|
||||||
n_inlets: '3',
|
n_inlets: '3',
|
||||||
timeStep: '2',
|
timeStep: '2',
|
||||||
S_O_init: '1.1',
|
S_O_init: '1.1',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert.equal(dc.reactor.volume, 12.5);
|
assert.equal(dc.reactor.volume, 12.5);
|
||||||
assert.equal(dc.reactor.length, 9);
|
assert.equal(dc.reactor.length, 9);
|
||||||
assert.equal(dc.reactor.resolution_L, 7);
|
assert.equal(dc.reactor.resolution_L, 7);
|
||||||
assert.equal(dc.reactor.alpha, 0.5);
|
assert.equal(dc.reactor.alpha, 0.5);
|
||||||
assert.equal(dc.reactor.n_inlets, 3);
|
assert.equal(dc.reactor.n_inlets, 3);
|
||||||
assert.equal(dc.reactor.timeStep, 2);
|
assert.equal(dc.reactor.timeStep, 2);
|
||||||
assert.equal(Object.keys(dc.initialState).length, 13);
|
assert.equal(Object.keys(dc.initialState).length, 13);
|
||||||
assert.equal(dc.initialState.S_O, 1.1);
|
assert.equal(dc.initialState.S_O, 1.1);
|
||||||
|
} finally {
|
||||||
|
closeNode(node);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Reactor wrapper instantiates CSTR engine when configured as CSTR', () => {
|
test('Reactor wrapper instantiates CSTR engine when configured as CSTR', () => {
|
||||||
const Reactor = require('../../src/specificClass');
|
const node = makeNode();
|
||||||
const config = {
|
const inst = new nodeClass(makeUiConfig({ reactor_type: 'CSTR' }), makeRED(), node, 'reactor');
|
||||||
general: { name: 'reactor', id: 'n', logging: { enabled: false, logLevel: 'error' } },
|
try {
|
||||||
functionality: { softwareType: 'reactor', positionVsParent: 'atEquipment' },
|
assert.ok(inst.source.engine instanceof Reactor_CSTR);
|
||||||
reactor: { reactor_type: 'CSTR', volume: 100, length: 10, resolution_L: 5, alpha: 0,
|
} finally {
|
||||||
n_inlets: 1, kla: NaN, timeStep: 1 },
|
closeNode(node);
|
||||||
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 },
|
|
||||||
};
|
|
||||||
const r = new Reactor(config);
|
|
||||||
assert.ok(r.engine instanceof Reactor_CSTR);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Reactor wrapper instantiates PFR engine when configured as PFR', () => {
|
test('Reactor wrapper instantiates PFR engine when configured as PFR', () => {
|
||||||
const Reactor = require('../../src/specificClass');
|
const node = makeNode();
|
||||||
const config = {
|
const inst = new nodeClass(makeUiConfig({ reactor_type: 'PFR' }), makeRED(), node, 'reactor');
|
||||||
general: { name: 'reactor', id: 'n', logging: { enabled: false, logLevel: 'error' } },
|
try {
|
||||||
functionality: { softwareType: 'reactor', positionVsParent: 'atEquipment' },
|
assert.ok(inst.source.engine instanceof Reactor_PFR);
|
||||||
reactor: { reactor_type: 'PFR', volume: 100, length: 10, resolution_L: 5, alpha: 0,
|
} finally {
|
||||||
n_inlets: 1, kla: NaN, timeStep: 1 },
|
closeNode(node);
|
||||||
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 },
|
|
||||||
};
|
|
||||||
const r = new Reactor(config);
|
|
||||||
assert.ok(r.engine instanceof Reactor_PFR);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,56 +1,111 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Phase 10 rewrite: drives only the public BaseNodeAdapter surface.
|
||||||
|
// The pre-refactor _attachInputHandler private switch is gone — input
|
||||||
|
// dispatch goes through the commands registry that BaseNodeAdapter builds
|
||||||
|
// at construction. Tests fire msgs through `node.handlers.input` and
|
||||||
|
// observe via `node.sends`, `inst.source.engine.*`, and per-fire calls
|
||||||
|
// captured on a child stub registered through `RED.nodes.getNode(id)`.
|
||||||
|
|
||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
const NodeClass = require('../../src/nodeClass');
|
const nodeClass = require('../../src/nodeClass');
|
||||||
const commands = require('../../src/commands');
|
const { makeUiConfig } = require('../helpers/factories');
|
||||||
const { createRegistry } = require('generalFunctions');
|
|
||||||
const { makeNodeStub, makeREDStub } = require('../helpers/factories');
|
|
||||||
|
|
||||||
// Post-refactor: dispatch goes through the commands registry built by
|
function makeNode(id = 'reactor-1') {
|
||||||
// BaseNodeAdapter (this._commands). We seed the registry on a prototype-
|
const sends = [];
|
||||||
// derived instance, then drive _attachInputHandler the same way the live
|
const statuses = [];
|
||||||
// adapter would.
|
const handlers = {};
|
||||||
|
return {
|
||||||
test('input handler routes legacy topic aliases to engine setters', async () => {
|
id, sends, statuses, handlers,
|
||||||
const inst = Object.create(NodeClass.prototype);
|
send(arr) { sends.push(arr); },
|
||||||
const node = makeNodeStub();
|
status(b) { statuses.push(b); },
|
||||||
const calls = [];
|
on(ev, fn) { handlers[ev] = fn; },
|
||||||
|
warn() {}, error() {},
|
||||||
const source = {
|
|
||||||
logger: { warn: () => {}, info: () => {}, debug: () => {}, error: () => {} },
|
|
||||||
updateState(t) { calls.push(['clock', t]); },
|
|
||||||
childRegistrationUtils: {
|
|
||||||
registerChild(childSource, position) { calls.push(['registerChild', childSource, position]); },
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
Object.defineProperty(source, 'setInfluent', { set(v) { calls.push(['Fluent', v]); } });
|
function makeRED(nodeMap = {}) {
|
||||||
Object.defineProperty(source, 'setOTR', { set(v) { calls.push(['OTR', v]); } });
|
return { nodes: { getNode: (id) => nodeMap[id] || null } };
|
||||||
Object.defineProperty(source, 'setTemperature', { set(v) { calls.push(['Temperature', v]); } });
|
}
|
||||||
Object.defineProperty(source, 'setDispersion', { set(v) { calls.push(['Dispersion', v]); } });
|
|
||||||
|
|
||||||
inst.node = node;
|
function closeNode(node) {
|
||||||
inst.RED = makeREDStub({ childA: { source: { id: 'child-source-A' } } });
|
if (node.handlers.close) node.handlers.close(() => {});
|
||||||
inst.source = source;
|
}
|
||||||
inst._commands = createRegistry(commands, { logger: source.logger });
|
|
||||||
inst._attachInputHandler();
|
|
||||||
|
|
||||||
const onInput = node._handlers.input;
|
test('legacy alias topics drive engine setters and updateState', async () => {
|
||||||
let doneCount = 0;
|
const childSource = {
|
||||||
const done = () => { doneCount += 1; };
|
id: 'child-source-A',
|
||||||
|
config: { general: { id: 'child-source-A' }, functionality: { softwareType: 'measurement', positionVsParent: 'upstream' }, asset: { type: 'temperature' } },
|
||||||
|
};
|
||||||
|
const node = makeNode();
|
||||||
|
const RED = makeRED({ childA: { source: childSource } });
|
||||||
|
const inst = new nodeClass(makeUiConfig(), RED, node, 'reactor');
|
||||||
|
|
||||||
await onInput({ topic: 'clock', timestamp: 1000 }, () => {}, done);
|
try {
|
||||||
await onInput({ topic: 'Fluent', payload: { inlet: 0, F: 10, C: [] } }, () => {}, done);
|
let doneCount = 0;
|
||||||
await onInput({ topic: 'OTR', payload: 3.5 }, () => {}, done);
|
const done = () => { doneCount += 1; };
|
||||||
await onInput({ topic: 'Temperature', payload: 18.2 }, () => {}, done);
|
|
||||||
await onInput({ topic: 'Dispersion', payload: 0.2 }, () => {}, done);
|
|
||||||
await onInput({ topic: 'registerChild', payload: 'childA', positionVsParent: 'upstream' }, () => {}, done);
|
|
||||||
|
|
||||||
assert.equal(doneCount, 6);
|
// data.clock alias → updateState(timestamp). Capture currentTime
|
||||||
assert.deepEqual(calls[0], ['clock', 1000]);
|
// before/after to verify the engine advanced.
|
||||||
assert.equal(calls.some((x) => x[0] === 'Fluent'), true);
|
const t0 = inst.source.engine.currentTime;
|
||||||
assert.equal(calls.some((x) => x[0] === 'OTR'), true);
|
await node.handlers.input({ topic: 'clock', timestamp: t0 + 1 }, () => {}, done);
|
||||||
assert.equal(calls.some((x) => x[0] === 'Temperature'), true);
|
|
||||||
assert.equal(calls.some((x) => x[0] === 'Dispersion'), true);
|
// Fluent alias → engine setInfluent setter.
|
||||||
assert.deepEqual(calls.at(-1), ['registerChild', { id: 'child-source-A' }, 'upstream']);
|
await node.handlers.input(
|
||||||
|
{ topic: 'Fluent', payload: { inlet: 0, F: 7, C: [1,2,3,4,5,6,7,8,9,10,11,12,13] } },
|
||||||
|
() => {}, done,
|
||||||
|
);
|
||||||
|
assert.equal(inst.source.engine.Fs[0], 7);
|
||||||
|
assert.deepEqual(inst.source.engine.Cs_in[0], [1,2,3,4,5,6,7,8,9,10,11,12,13]);
|
||||||
|
|
||||||
|
// OTR alias → engine setOTR setter.
|
||||||
|
await node.handlers.input({ topic: 'OTR', payload: 3.5 }, () => {}, done);
|
||||||
|
assert.equal(inst.source.engine.OTR, 3.5);
|
||||||
|
|
||||||
|
// Temperature alias → engine setTemperature setter.
|
||||||
|
await node.handlers.input({ topic: 'Temperature', payload: 18.2 }, () => {}, done);
|
||||||
|
assert.equal(inst.source.engine.temperature, 18.2);
|
||||||
|
|
||||||
|
// Dispersion alias — CSTR engine does not own a setDispersion setter
|
||||||
|
// (only PFR does); the Reactor wrapper guards on engine type and the
|
||||||
|
// dispatch should silently return without throwing.
|
||||||
|
await node.handlers.input({ topic: 'Dispersion', payload: 0.2 }, () => {}, done);
|
||||||
|
|
||||||
|
// registerChild alias → registers via childRegistrationUtils.
|
||||||
|
// The handler resolves the child via RED.nodes.getNode(payload).source.
|
||||||
|
await node.handlers.input(
|
||||||
|
{ topic: 'registerChild', payload: 'childA', positionVsParent: 'upstream' },
|
||||||
|
() => {}, done,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(doneCount, 6);
|
||||||
|
} finally {
|
||||||
|
closeNode(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('canonical topics are accepted (data.fluent, data.otr, data.temperature)', async () => {
|
||||||
|
const node = makeNode();
|
||||||
|
const inst = new nodeClass(makeUiConfig(), makeRED(), node, 'reactor');
|
||||||
|
|
||||||
|
try {
|
||||||
|
let done = 0;
|
||||||
|
await node.handlers.input(
|
||||||
|
{ topic: 'data.fluent', payload: { inlet: 0, F: 11, C: [0,0,0,0,0,0,0,0,0,0,0,0,0] } },
|
||||||
|
() => {}, () => { done += 1; },
|
||||||
|
);
|
||||||
|
assert.equal(inst.source.engine.Fs[0], 11);
|
||||||
|
|
||||||
|
await node.handlers.input({ topic: 'data.otr', payload: 4.2 }, () => {}, () => { done += 1; });
|
||||||
|
assert.equal(inst.source.engine.OTR, 4.2);
|
||||||
|
|
||||||
|
await node.handlers.input({ topic: 'data.temperature', payload: 19.7 }, () => {}, () => { done += 1; });
|
||||||
|
assert.equal(inst.source.engine.temperature, 19.7);
|
||||||
|
|
||||||
|
assert.equal(done, 3);
|
||||||
|
} finally {
|
||||||
|
closeNode(node);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,32 +1,84 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Phase 10 rewrite: drives only the public BaseNodeAdapter surface.
|
||||||
|
// The pre-refactor _registerChild method was renamed to
|
||||||
|
// _scheduleRegistration inside BaseNodeAdapter and now fires automatically
|
||||||
|
// 100ms after construction. We verify the emission by capturing the Port-2
|
||||||
|
// message on `node.sends` after the registration delay elapses.
|
||||||
|
|
||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
const NodeClass = require('../../src/nodeClass');
|
const nodeClass = require('../../src/nodeClass');
|
||||||
const { makeNodeStub } = require('../helpers/factories');
|
const { makeUiConfig } = require('../helpers/factories');
|
||||||
|
|
||||||
// Post-refactor: BaseNodeAdapter handles registration via _scheduleRegistration
|
function makeRED() { return { nodes: { getNode: () => null } }; }
|
||||||
// (was _registerChild). Topic moved from 'registerChild' to 'child.register'.
|
|
||||||
test('_scheduleRegistration emits delayed child.register message on output 2', () => {
|
|
||||||
const inst = Object.create(NodeClass.prototype);
|
|
||||||
const node = makeNodeStub();
|
|
||||||
|
|
||||||
inst.node = node;
|
function makeNode(id = 'reactor-node-1') {
|
||||||
inst.config = { functionality: { positionVsParent: 'downstream', distance: null } };
|
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 originalSetTimeout = global.setTimeout;
|
function closeNode(node) {
|
||||||
const delays = [];
|
if (node.handlers.close) node.handlers.close(() => {});
|
||||||
global.setTimeout = (fn, ms) => { delays.push(ms); fn(); return 1; };
|
}
|
||||||
|
|
||||||
|
test('scheduled child.register message lands on Port 2 after construction', async () => {
|
||||||
|
const node = makeNode();
|
||||||
|
const inst = new nodeClass(
|
||||||
|
makeUiConfig({ positionVsParent: 'downstream' }),
|
||||||
|
makeRED(),
|
||||||
|
node,
|
||||||
|
'reactor',
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
inst._scheduleRegistration();
|
// BaseNodeAdapter._scheduleRegistration uses a 100ms setTimeout; wait
|
||||||
} finally {
|
// slightly longer to let it fire.
|
||||||
global.setTimeout = originalSetTimeout;
|
await new Promise((r) => setTimeout(r, 130));
|
||||||
}
|
|
||||||
|
|
||||||
assert.deepEqual(delays, [100]);
|
// The registration send is the [null, null, {child.register}] triple.
|
||||||
assert.equal(node._sent.length, 1);
|
const regSends = node.sends.filter(
|
||||||
assert.equal(Array.isArray(node._sent[0]), true);
|
(s) => Array.isArray(s) && s[0] === null && s[1] === null && s[2] && s[2].topic === 'child.register',
|
||||||
assert.equal(node._sent[0][2].topic, 'child.register');
|
);
|
||||||
assert.equal(node._sent[0][2].payload, node.id);
|
assert.equal(regSends.length, 1, 'exactly one child.register message expected');
|
||||||
assert.equal(node._sent[0][2].positionVsParent, 'downstream');
|
const msg = regSends[0][2];
|
||||||
|
assert.equal(msg.topic, 'child.register');
|
||||||
|
assert.equal(msg.payload, node.id);
|
||||||
|
assert.equal(msg.positionVsParent, 'downstream');
|
||||||
|
// After construction the source is exposed on the node for sibling lookup.
|
||||||
|
assert.strictEqual(node.source, inst.source);
|
||||||
|
} finally {
|
||||||
|
closeNode(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('child.register handler ignores unknown child ids without throwing', async () => {
|
||||||
|
const node = makeNode();
|
||||||
|
const inst = 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);
|
||||||
|
// No child should have been registered into the engine's registry.
|
||||||
|
const registered = inst.source.engine.childRegistrationUtils;
|
||||||
|
assert.ok(registered, 'childRegistrationUtils exists on engine');
|
||||||
|
} finally {
|
||||||
|
closeNode(node);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +1,52 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Phase 10 rewrite: drives only the public BaseNodeAdapter surface for
|
||||||
|
// the nodeClass-level checks, and the public Reactor_CSTR engine surface
|
||||||
|
// for the domain-level checks. The pre-refactor private nodeClass methods
|
||||||
|
// are gone — `buildDomainConfig` is the documented override hook
|
||||||
|
// (CONTRACTS.md §2) and is fair game to call on a real constructed
|
||||||
|
// instance.
|
||||||
|
|
||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const nodeClass = require('../../src/nodeClass');
|
||||||
const { Reactor_CSTR } = require('../../src/specificClass');
|
const { Reactor_CSTR } = require('../../src/specificClass');
|
||||||
const NodeClass = require('../../src/nodeClass');
|
|
||||||
const { makeReactorConfig, makeUiConfig } = require('../helpers/factories');
|
const { makeReactorConfig, makeUiConfig } = require('../helpers/factories');
|
||||||
|
|
||||||
/**
|
function makeRED() { return { nodes: { getNode: () => null } }; }
|
||||||
* Smoke tests for Fix 3: configurable speedUpFactor on Reactor.
|
|
||||||
*/
|
|
||||||
|
|
||||||
test('specificClass defaults speedUpFactor to 1 when not in config', () => {
|
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('Reactor_CSTR engine defaults speedUpFactor to 1 when not in config', () => {
|
||||||
const config = makeReactorConfig();
|
const config = makeReactorConfig();
|
||||||
const reactor = new Reactor_CSTR(config);
|
const reactor = new Reactor_CSTR(config);
|
||||||
assert.equal(reactor.speedUpFactor, 1, 'speedUpFactor should default to 1');
|
assert.equal(reactor.speedUpFactor, 1, 'speedUpFactor should default to 1');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('specificClass accepts speedUpFactor from config', () => {
|
test('Reactor_CSTR engine accepts speedUpFactor from config', () => {
|
||||||
const config = makeReactorConfig();
|
const config = makeReactorConfig();
|
||||||
config.speedUpFactor = 10;
|
config.speedUpFactor = 10;
|
||||||
const reactor = new Reactor_CSTR(config);
|
const reactor = new Reactor_CSTR(config);
|
||||||
assert.equal(reactor.speedUpFactor, 10, 'speedUpFactor should be read from config');
|
assert.equal(reactor.speedUpFactor, 10, 'speedUpFactor should be read from config');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('specificClass accepts speedUpFactor = 60 for accelerated simulation', () => {
|
test('Reactor_CSTR engine accepts speedUpFactor = 60 for accelerated simulation', () => {
|
||||||
const config = makeReactorConfig();
|
const config = makeReactorConfig();
|
||||||
config.speedUpFactor = 60;
|
config.speedUpFactor = 60;
|
||||||
const reactor = new Reactor_CSTR(config);
|
const reactor = new Reactor_CSTR(config);
|
||||||
@@ -30,21 +54,27 @@ test('specificClass accepts speedUpFactor = 60 for accelerated simulation', () =
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('buildDomainConfig propagates speedUpFactor from uiConfig', () => {
|
test('buildDomainConfig propagates speedUpFactor from uiConfig', () => {
|
||||||
const inst = Object.create(NodeClass.prototype);
|
const node = makeNode();
|
||||||
inst.node = { id: 'n-reactor' };
|
const inst = new nodeClass(makeUiConfig(), makeRED(), node, 'reactor');
|
||||||
inst.name = 'reactor';
|
try {
|
||||||
const dc = inst.buildDomainConfig(makeUiConfig({ speedUpFactor: 5 }));
|
const dc = inst.buildDomainConfig(makeUiConfig({ speedUpFactor: 5 }));
|
||||||
assert.equal(dc.reactor.speedUpFactor, 5);
|
assert.equal(dc.reactor.speedUpFactor, 5);
|
||||||
|
} finally {
|
||||||
|
closeNode(node);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('buildDomainConfig defaults speedUpFactor to 1 when missing from uiConfig', () => {
|
test('buildDomainConfig defaults speedUpFactor to 1 when missing from uiConfig', () => {
|
||||||
const inst = Object.create(NodeClass.prototype);
|
const node = makeNode();
|
||||||
inst.node = { id: 'n-reactor' };
|
const inst = new nodeClass(makeUiConfig(), makeRED(), node, 'reactor');
|
||||||
inst.name = 'reactor';
|
try {
|
||||||
const ui = makeUiConfig();
|
const ui = makeUiConfig();
|
||||||
delete ui.speedUpFactor;
|
delete ui.speedUpFactor;
|
||||||
const dc = inst.buildDomainConfig(ui);
|
const dc = inst.buildDomainConfig(ui);
|
||||||
assert.equal(dc.reactor.speedUpFactor, 1);
|
assert.equal(dc.reactor.speedUpFactor, 1);
|
||||||
|
} finally {
|
||||||
|
closeNode(node);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('updateState with speedUpFactor=1 advances roughly real-time', () => {
|
test('updateState with speedUpFactor=1 advances roughly real-time', () => {
|
||||||
|
|||||||
@@ -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 test = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
const Reactor = require('../../src/specificClass');
|
const nodeClass = require('../../src/nodeClass');
|
||||||
const { Reactor_CSTR } = require('../../src/specificClass');
|
const { Reactor_CSTR } = require('../../src/specificClass');
|
||||||
|
const { makeUiConfig } = require('../helpers/factories');
|
||||||
|
|
||||||
// Post-refactor: an unknown reactor_type falls back to CSTR and warns,
|
function makeRED() { return { nodes: { getNode: () => null } }; }
|
||||||
// rather than throwing.
|
|
||||||
test('Reactor wrapper falls back to CSTR when reactor_type is unknown', () => {
|
function makeNode(id = 'reactor-node-1') {
|
||||||
const config = {
|
const sends = [];
|
||||||
general: { name: 'reactor', id: 'n', logging: { enabled: false, logLevel: 'error' } },
|
const statuses = [];
|
||||||
functionality: { softwareType: 'reactor', positionVsParent: 'atEquipment' },
|
const handlers = {};
|
||||||
reactor: { reactor_type: 'UNKNOWN_TYPE', volume: 100, length: 10, resolution_L: 5,
|
return {
|
||||||
alpha: 0, n_inlets: 1, kla: NaN, timeStep: 1 },
|
id, sends, statuses, handlers,
|
||||||
initialState: { S_O: 0, S_I: 30, S_S: 100, S_NH: 16, S_N2: 0, S_NO: 0, S_HCO: 5,
|
send(arr) { sends.push(arr); },
|
||||||
X_I: 25, X_S: 75, X_H: 30, X_STO: 0, X_A: 0.001, X_TS: 125 },
|
status(b) { statuses.push(b); },
|
||||||
|
on(ev, fn) { handlers[ev] = fn; },
|
||||||
|
warn() {}, error() {},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const r = new Reactor(config);
|
function closeNode(node) {
|
||||||
assert.ok(r.engine instanceof Reactor_CSTR);
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 test = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
const NodeClass = require('../../src/nodeClass');
|
const nodeClass = require('../../src/nodeClass');
|
||||||
const commands = require('../../src/commands');
|
const { makeUiConfig } = require('../helpers/factories');
|
||||||
const { createRegistry } = require('generalFunctions');
|
|
||||||
const { makeNodeStub, makeREDStub } = 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 () => {
|
test('unknown input topic does not throw and still calls done', async () => {
|
||||||
const inst = Object.create(NodeClass.prototype);
|
const node = makeNode();
|
||||||
const node = makeNodeStub();
|
new nodeClass(makeUiConfig(), makeRED(), node, 'reactor');
|
||||||
|
|
||||||
inst.node = node;
|
try {
|
||||||
inst.RED = makeREDStub();
|
let doneCalled = 0;
|
||||||
inst.source = {
|
await assert.doesNotReject(async () => {
|
||||||
logger: { warn: () => {}, info: () => {}, debug: () => {}, error: () => {} },
|
await node.handlers.input(
|
||||||
childRegistrationUtils: { registerChild() {} },
|
{ topic: 'somethingUnknown', payload: 1 },
|
||||||
updateState() {},
|
() => {},
|
||||||
};
|
() => { doneCalled += 1; },
|
||||||
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;
|
|
||||||
});
|
});
|
||||||
});
|
assert.equal(doneCalled, 1);
|
||||||
|
} finally {
|
||||||
assert.equal(doneCalled, 1);
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 test = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
const NodeClass = require('../../src/nodeClass');
|
const nodeClass = require('../../src/nodeClass');
|
||||||
const commands = require('../../src/commands');
|
const { makeUiConfig } = require('../helpers/factories');
|
||||||
const { createRegistry } = require('generalFunctions');
|
|
||||||
const { makeNodeStub, makeREDStub } = require('../helpers/factories');
|
|
||||||
|
|
||||||
test('registerChild with unknown node id is ignored without throwing', async () => {
|
function makeRED(nodeMap = {}) {
|
||||||
const inst = Object.create(NodeClass.prototype);
|
return { nodes: { getNode: (id) => nodeMap[id] || null } };
|
||||||
const node = makeNodeStub();
|
}
|
||||||
|
|
||||||
inst.node = node;
|
function makeNode(id = 'reactor-node-1') {
|
||||||
inst.RED = makeREDStub();
|
const sends = [];
|
||||||
inst.source = {
|
const statuses = [];
|
||||||
logger: { warn: () => {}, info: () => {}, debug: () => {}, error: () => {} },
|
const handlers = {};
|
||||||
childRegistrationUtils: { registerChild() {} },
|
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 () => {
|
function closeNode(node) {
|
||||||
await node._handlers.input(
|
if (node.handlers.close) node.handlers.close(() => {});
|
||||||
{ topic: 'registerChild', payload: 'missing-child', positionVsParent: 'upstream' },
|
}
|
||||||
() => {},
|
|
||||||
() => {},
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,103 +1,156 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Phase 10 rewrite: drives only the public BaseNodeAdapter surface.
|
||||||
|
// The pre-refactor _tick / _startTickLoop methods are gone — periodic
|
||||||
|
// emission lives in `_emitOutputs()` (overridden in the reactor nodeClass
|
||||||
|
// to preserve the Fluent / GridProfile Port-0 contract; delta-compressed
|
||||||
|
// payloads can't carry the C-vector). The override is part of the
|
||||||
|
// documented BaseNodeAdapter override surface, so we exercise it
|
||||||
|
// directly. The fully-constructed adapter wires `inst.source.engine`,
|
||||||
|
// `inst._output`, etc. so we don't have to assemble stub bags.
|
||||||
|
|
||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
const NodeClass = require('../../src/nodeClass');
|
const nodeClass = require('../../src/nodeClass');
|
||||||
const { makeNodeStub } = require('../helpers/factories');
|
const { makeUiConfig } = require('../helpers/factories');
|
||||||
|
|
||||||
// Post-refactor: BaseNodeAdapter drives tick + status loops. The reactor
|
function makeRED() { return { nodes: { getNode: () => null } }; }
|
||||||
// nodeClass overrides _emitOutputs to preserve the Fluent / GridProfile
|
|
||||||
// Port-0 contract (delta-compressed payloads can't carry the C-vector).
|
|
||||||
|
|
||||||
test('_emitOutputs emits effluent on process output', () => {
|
function makeNode(id = 'reactor-node-1') {
|
||||||
const inst = Object.create(NodeClass.prototype);
|
const sends = [];
|
||||||
const node = makeNodeStub();
|
const statuses = [];
|
||||||
|
const handlers = {};
|
||||||
inst.node = node;
|
return {
|
||||||
inst.config = { functionality: { softwareType: 'reactor' }, general: { id: 'r-1' } };
|
id, sends, statuses, handlers,
|
||||||
inst._output = { formatMsg() { return null; } };
|
send(arr) { sends.push(arr); },
|
||||||
inst.source = {
|
status(b) { statuses.push(b); },
|
||||||
engine: { temperature: 18, getEffluent: { topic: 'Fluent', payload: { inlet: 0, F: 1, C: [] }, timestamp: 1 }, get getGridProfile() { return null; } },
|
on(ev, fn) { handlers[ev] = fn; },
|
||||||
config: inst.config,
|
warn() {}, error() {},
|
||||||
updateState() {},
|
|
||||||
get getEffluent() { return this.engine.getEffluent; },
|
|
||||||
get getGridProfile() { return this.engine.getGridProfile; },
|
|
||||||
getOutput() { return {}; },
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
inst._emitOutputs();
|
function closeNode(node) {
|
||||||
|
if (node.handlers.close) node.handlers.close(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
assert.equal(node._sent.length, 1);
|
function pickEffluentSends(node) {
|
||||||
assert.equal(node._sent[0][0].topic, 'Fluent');
|
return node.sends.filter((s) => Array.isArray(s) && s[0] && s[0].topic === 'Fluent');
|
||||||
assert.equal(node._sent[0][1], null);
|
}
|
||||||
assert.equal(node._sent[0][2], null);
|
|
||||||
|
function pickGridSends(node) {
|
||||||
|
return node.sends.filter((s) => Array.isArray(s) && s[0] && s[0].topic === 'GridProfile');
|
||||||
|
}
|
||||||
|
|
||||||
|
test('_emitOutputs sends the effluent message on process output (CSTR)', () => {
|
||||||
|
const node = makeNode();
|
||||||
|
const inst = new nodeClass(
|
||||||
|
makeUiConfig({ reactor_type: 'CSTR' }),
|
||||||
|
makeRED(),
|
||||||
|
node,
|
||||||
|
'reactor',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Reset sends so any construction-time emissions don't pollute the
|
||||||
|
// assertion (the registration triple lands on the same buffer).
|
||||||
|
node.sends.length = 0;
|
||||||
|
inst._emitOutputs();
|
||||||
|
|
||||||
|
const fluentSends = pickEffluentSends(node);
|
||||||
|
assert.equal(fluentSends.length, 1, 'exactly one Fluent message');
|
||||||
|
const triple = fluentSends[0];
|
||||||
|
assert.equal(triple[0].topic, 'Fluent');
|
||||||
|
assert.ok(triple[0].payload && Array.isArray(triple[0].payload.C));
|
||||||
|
// CSTR has no grid profile.
|
||||||
|
assert.equal(pickGridSends(node).length, 0);
|
||||||
|
} finally {
|
||||||
|
closeNode(node);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('_emitOutputs emits reactor telemetry on influx output', () => {
|
test('_emitOutputs emits a GridProfile message when engine exposes one (PFR)', () => {
|
||||||
const inst = Object.create(NodeClass.prototype);
|
const node = makeNode();
|
||||||
const node = makeNodeStub();
|
const inst = new nodeClass(
|
||||||
let captured = null;
|
makeUiConfig({ reactor_type: 'PFR' }),
|
||||||
|
makeRED(),
|
||||||
|
node,
|
||||||
|
'reactor',
|
||||||
|
);
|
||||||
|
|
||||||
inst.node = node;
|
try {
|
||||||
inst.config = { functionality: { softwareType: 'reactor' }, general: { id: 'reactor-node-1' } };
|
node.sends.length = 0;
|
||||||
inst._output = {
|
inst._emitOutputs();
|
||||||
formatMsg(output, _config, format) {
|
|
||||||
captured = { output, format };
|
|
||||||
return { topic: `reactor_${inst.config.general.id}`, payload: { measurement: 'reactor', fields: output } };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const effluent = { topic: 'Fluent', payload: { inlet: 0, F: 42, C: [2.1, 30, 100, 16, 0, 1, 8, 25, 75, 1500, 0, 15, 2500] }, timestamp: 1 };
|
|
||||||
inst.source = {
|
|
||||||
engine: { temperature: 19.5, getEffluent: effluent, get getGridProfile() { return null; } },
|
|
||||||
config: inst.config,
|
|
||||||
updateState() {},
|
|
||||||
get getEffluent() { return this.engine.getEffluent; },
|
|
||||||
get getGridProfile() { return this.engine.getGridProfile; },
|
|
||||||
getOutput() {
|
|
||||||
const C = effluent.payload.C;
|
|
||||||
const out = { flow_total: effluent.payload.F, temperature: 19.5 };
|
|
||||||
const keys = ['S_O','S_I','S_S','S_NH','S_N2','S_NO','S_HCO','X_I','X_S','X_H','X_STO','X_A','X_TS'];
|
|
||||||
for (let i = 0; i < keys.length; i += 1) out[keys[i]] = C[i];
|
|
||||||
return out;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
inst._emitOutputs();
|
assert.equal(pickGridSends(node).length, 1, 'exactly one GridProfile message');
|
||||||
|
assert.equal(pickEffluentSends(node).length, 1, 'exactly one Fluent message');
|
||||||
assert.equal(node._sent.length, 1);
|
} finally {
|
||||||
assert.equal(node._sent[0][0].topic, 'Fluent');
|
closeNode(node);
|
||||||
assert.equal(node._sent[0][1].topic, 'reactor_reactor-node-1');
|
}
|
||||||
assert.equal(captured.format, 'influxdb');
|
|
||||||
assert.equal(captured.output.flow_total, 42);
|
|
||||||
assert.equal(captured.output.temperature, 19.5);
|
|
||||||
assert.equal(captured.output.S_O, 2.1);
|
|
||||||
assert.equal(captured.output.S_NH, 16);
|
|
||||||
assert.equal(captured.output.X_TS, 2500);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('_emitOutputs also emits GridProfile when engine exposes one', () => {
|
test('_emitOutputs formats per-species influx telemetry via outputUtils', () => {
|
||||||
const inst = Object.create(NodeClass.prototype);
|
const node = makeNode();
|
||||||
const node = makeNodeStub();
|
const inst = new nodeClass(
|
||||||
|
makeUiConfig({ reactor_type: 'CSTR' }),
|
||||||
|
makeRED(),
|
||||||
|
node,
|
||||||
|
'reactor',
|
||||||
|
);
|
||||||
|
|
||||||
inst.node = node;
|
try {
|
||||||
inst.config = { functionality: { softwareType: 'reactor' }, general: { id: 'r-1' } };
|
// Stub updateState so the engine integration does not overwrite the
|
||||||
inst._output = { formatMsg() { return null; } };
|
// engineered state we want the telemetry formatter to see.
|
||||||
const grid = { grid: [[0]], n_x: 1, d_x: 1, length: 1, species: [], timestamp: 1 };
|
inst.source.updateState = () => {};
|
||||||
inst.source = {
|
inst.source.engine.setInfluent = {
|
||||||
engine: {
|
payload: { inlet: 0, F: 42, C: [2.1, 30, 100, 16, 0, 1, 8, 25, 75, 1500, 0, 15, 2500] },
|
||||||
temperature: 18,
|
};
|
||||||
getEffluent: { topic: 'Fluent', payload: { inlet: 0, F: 1, C: [] }, timestamp: 1 },
|
inst.source.engine.state = [2.1, 30, 100, 16, 0, 1, 8, 25, 75, 1500, 0, 15, 2500];
|
||||||
get getGridProfile() { return grid; },
|
inst.source.engine.temperature = 19.5;
|
||||||
},
|
|
||||||
config: inst.config,
|
|
||||||
updateState() {},
|
|
||||||
get getEffluent() { return this.engine.getEffluent; },
|
|
||||||
get getGridProfile() { return this.engine.getGridProfile; },
|
|
||||||
getOutput() { return {}; },
|
|
||||||
};
|
|
||||||
|
|
||||||
inst._emitOutputs();
|
let captured = null;
|
||||||
|
const realFormat = inst._output.formatMsg.bind(inst._output);
|
||||||
|
inst._output.formatMsg = (output, cfg, format) => {
|
||||||
|
if (format === 'influxdb') captured = { output, format };
|
||||||
|
return realFormat(output, cfg, format);
|
||||||
|
};
|
||||||
|
|
||||||
assert.equal(node._sent.length, 2);
|
node.sends.length = 0;
|
||||||
assert.equal(node._sent[0][0].topic, 'GridProfile');
|
inst._emitOutputs();
|
||||||
assert.equal(node._sent[1][0].topic, 'Fluent');
|
|
||||||
|
assert.ok(captured, 'formatMsg was called with influxdb format');
|
||||||
|
assert.equal(captured.format, 'influxdb');
|
||||||
|
assert.equal(captured.output.flow_total, 42);
|
||||||
|
assert.equal(captured.output.temperature, 19.5);
|
||||||
|
assert.equal(captured.output.S_O, 2.1);
|
||||||
|
assert.equal(captured.output.S_NH, 16);
|
||||||
|
assert.equal(captured.output.X_TS, 2500);
|
||||||
|
} finally {
|
||||||
|
closeNode(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Reactor.tick(dt) drives the kinetics engine and advances state', () => {
|
||||||
|
const node = makeNode();
|
||||||
|
const inst = new nodeClass(
|
||||||
|
makeUiConfig({ reactor_type: 'CSTR' }),
|
||||||
|
makeRED(),
|
||||||
|
node,
|
||||||
|
'reactor',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Feed an influent so the integrator has something to chew on.
|
||||||
|
inst.source.engine.setInfluent = {
|
||||||
|
payload: { inlet: 0, F: 5, C: [0,30,100,16,0,0,5,25,75,30,0,0.001,125] },
|
||||||
|
};
|
||||||
|
|
||||||
|
const stateBefore = JSON.stringify(inst.source.engine.state);
|
||||||
|
inst.source.tick(0.001);
|
||||||
|
const stateAfter = JSON.stringify(inst.source.engine.state);
|
||||||
|
|
||||||
|
assert.notEqual(stateBefore, stateAfter, 'engine state should advance after tick(dt)');
|
||||||
|
} finally {
|
||||||
|
closeNode(node);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
16
wiki/Home.md
16
wiki/Home.md
@@ -85,14 +85,14 @@ flowchart TB
|
|||||||
|
|
||||||
<!-- BEGIN AUTOGEN: topic-contract -->
|
<!-- BEGIN AUTOGEN: topic-contract -->
|
||||||
|
|
||||||
| Canonical topic | Aliases | Payload | Effect |
|
| Canonical topic | Aliases | Payload | Unit | Effect |
|
||||||
|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| `data.clock` | `clock` | `any` | Pushes a value into the node's measurement stream. |
|
| `data.clock` | `clock` | `any` | — | Push the simulation clock tick (timestamp / dt) to the ASM solver. |
|
||||||
| `data.fluent` | `Fluent` | `object` | Pushes a value into the node's measurement stream. |
|
| `data.fluent` | `Fluent` | `object` | — | Push the influent stream (payload: {F: flow m3/h, C: [concentrations mg/L]}). |
|
||||||
| `data.otr` | `OTR` | `any` | Pushes a value into the node's measurement stream. |
|
| `data.otr` | `OTR` | `any` | — | Push the current oxygen-transfer rate into the reactor. |
|
||||||
| `data.temperature` | `Temperature` | `any` | Pushes a value into the node's measurement stream. |
|
| `data.temperature` | `Temperature` | `any` | — | Push the current reactor temperature. |
|
||||||
| `data.dispersion` | `Dispersion` | `any` | Pushes a value into the node's measurement stream. |
|
| `data.dispersion` | `Dispersion` | `any` | — | Push a dispersion/mixing parameter update. |
|
||||||
| `child.register` | `registerChild` | `any` | Parent/child plumbing — registers or unregisters a child node. |
|
| `child.register` | `registerChild` | `any` | — | Register a child node (settler / measurement) with this reactor. |
|
||||||
|
|
||||||
<!-- END AUTOGEN: topic-contract -->
|
<!-- END AUTOGEN: topic-contract -->
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user