'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 assert = require('node:assert/strict'); 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(() => {}); } function pickEffluentSends(node) { return node.sends.filter((s) => Array.isArray(s) && s[0] && s[0].topic === 'Fluent'); } 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 a GridProfile message when engine exposes one (PFR)', () => { const node = makeNode(); const inst = new nodeClass( makeUiConfig({ reactor_type: 'PFR' }), makeRED(), node, 'reactor', ); try { node.sends.length = 0; inst._emitOutputs(); assert.equal(pickGridSends(node).length, 1, 'exactly one GridProfile message'); assert.equal(pickEffluentSends(node).length, 1, 'exactly one Fluent message'); } finally { closeNode(node); } }); test('_emitOutputs formats per-species influx telemetry via outputUtils', () => { const node = makeNode(); const inst = new nodeClass( makeUiConfig({ reactor_type: 'CSTR' }), makeRED(), node, 'reactor', ); try { // Stub updateState so the engine integration does not overwrite the // engineered state we want the telemetry formatter to see. inst.source.updateState = () => {}; inst.source.engine.setInfluent = { payload: { inlet: 0, F: 42, C: [2.1, 30, 100, 16, 0, 1, 8, 25, 75, 1500, 0, 15, 2500] }, }; inst.source.engine.state = [2.1, 30, 100, 16, 0, 1, 8, 25, 75, 1500, 0, 15, 2500]; inst.source.engine.temperature = 19.5; 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); }; node.sends.length = 0; inst._emitOutputs(); 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); } });