before functional changes by codex
This commit is contained in:
21
test/README.md
Normal file
21
test/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Measurement Test Suite Layout
|
||||
|
||||
This folder follows EVOLV standard node test structure.
|
||||
|
||||
## Required folders
|
||||
- `basic/`
|
||||
- `integration/`
|
||||
- `edge/`
|
||||
- `helpers/`
|
||||
|
||||
## Baseline files
|
||||
- `basic/specific-constructor.basic.test.js`
|
||||
- `basic/scaling-and-output.basic.test.js`
|
||||
- `basic/nodeclass-routing.basic.test.js`
|
||||
- `integration/examples-flows.integration.test.js`
|
||||
- `integration/measurement-event.integration.test.js`
|
||||
- `edge/invalid-payload.edge.test.js`
|
||||
- `edge/outlier-toggle.edge.test.js`
|
||||
|
||||
Authoritative mapping for coverage intent lives in:
|
||||
- `.agents/function-anchors/measurement/EVIDENCE-measurement-tests.md`
|
||||
0
test/basic/.gitkeep
Normal file
0
test/basic/.gitkeep
Normal file
54
test/basic/nodeclass-routing.basic.test.js
Normal file
54
test/basic/nodeclass-routing.basic.test.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const NodeClass = require('../../src/nodeClass');
|
||||
const { makeNodeStub, makeREDStub } = require('../helpers/factories');
|
||||
|
||||
test('_attachInputHandler routes known topics to source methods', () => {
|
||||
const inst = Object.create(NodeClass.prototype);
|
||||
const node = makeNodeStub();
|
||||
const calls = [];
|
||||
|
||||
inst.node = node;
|
||||
inst.RED = makeREDStub();
|
||||
inst.source = {
|
||||
toggleSimulation() { calls.push('simulator'); },
|
||||
toggleOutlierDetection() { calls.push('outlierDetection'); },
|
||||
calibrate() { calls.push('calibrate'); },
|
||||
set inputValue(v) { calls.push(['measurement', v]); },
|
||||
};
|
||||
|
||||
inst._attachInputHandler();
|
||||
const onInput = node._handlers.input;
|
||||
|
||||
onInput({ topic: 'simulator' }, () => {}, () => {});
|
||||
onInput({ topic: 'outlierDetection' }, () => {}, () => {});
|
||||
onInput({ topic: 'calibrate' }, () => {}, () => {});
|
||||
onInput({ topic: 'measurement', payload: 12.3 }, () => {}, () => {});
|
||||
|
||||
assert.deepEqual(calls[0], 'simulator');
|
||||
assert.deepEqual(calls[1], 'outlierDetection');
|
||||
assert.deepEqual(calls[2], 'calibrate');
|
||||
assert.deepEqual(calls[3], ['measurement', 12.3]);
|
||||
});
|
||||
|
||||
test('_registerChild emits delayed registerChild message on output 2', () => {
|
||||
const inst = Object.create(NodeClass.prototype);
|
||||
const node = makeNodeStub();
|
||||
|
||||
inst.node = node;
|
||||
inst.config = { functionality: { positionVsParent: 'upstream', distance: 5 } };
|
||||
|
||||
const originalSetTimeout = global.setTimeout;
|
||||
global.setTimeout = (fn) => { fn(); return 1; };
|
||||
try {
|
||||
inst._registerChild();
|
||||
} finally {
|
||||
global.setTimeout = originalSetTimeout;
|
||||
}
|
||||
|
||||
assert.equal(node._sent.length, 1);
|
||||
assert.equal(node._sent[0][2].topic, 'registerChild');
|
||||
assert.equal(node._sent[0][2].positionVsParent, 'upstream');
|
||||
assert.equal(node._sent[0][2].distance, 5);
|
||||
});
|
||||
25
test/basic/scaling-and-output.basic.test.js
Normal file
25
test/basic/scaling-and-output.basic.test.js
Normal file
@@ -0,0 +1,25 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { makeMeasurementInstance } = require('../helpers/factories');
|
||||
|
||||
test('calculateInput applies scaling and updates bounded output', () => {
|
||||
const m = makeMeasurementInstance();
|
||||
|
||||
m.calculateInput(50);
|
||||
const out = m.getOutput();
|
||||
|
||||
assert.equal(out.mAbs >= 0 && out.mAbs <= 10, true);
|
||||
assert.equal(out.mPercent >= 0 && out.mPercent <= 100, true);
|
||||
});
|
||||
|
||||
test('out-of-range input is constrained to abs range', () => {
|
||||
const m = makeMeasurementInstance({
|
||||
smoothing: { smoothWindow: 1, smoothMethod: 'none' },
|
||||
});
|
||||
|
||||
m.calculateInput(10000);
|
||||
const out = m.getOutput();
|
||||
|
||||
assert.equal(out.mAbs, 10);
|
||||
});
|
||||
16
test/basic/specific-constructor.basic.test.js
Normal file
16
test/basic/specific-constructor.basic.test.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { makeMeasurementInstance } = require('../helpers/factories');
|
||||
|
||||
test('Measurement constructor initializes key defaults and ranges', () => {
|
||||
const m = makeMeasurementInstance();
|
||||
|
||||
assert.equal(m.inputValue, 0);
|
||||
assert.equal(m.outputAbs, 0);
|
||||
assert.equal(m.outputPercent, 0);
|
||||
assert.equal(Array.isArray(m.storedValues), true);
|
||||
assert.equal(typeof m.measurements, 'object');
|
||||
assert.equal(m.inputRange, 100);
|
||||
assert.equal(m.processRange, 10);
|
||||
});
|
||||
0
test/edge/.gitkeep
Normal file
0
test/edge/.gitkeep
Normal file
28
test/edge/invalid-payload.edge.test.js
Normal file
28
test/edge/invalid-payload.edge.test.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const NodeClass = require('../../src/nodeClass');
|
||||
const { makeNodeStub, makeREDStub } = require('../helpers/factories');
|
||||
|
||||
test('measurement topic ignores non-number payloads (current behavior)', () => {
|
||||
const inst = Object.create(NodeClass.prototype);
|
||||
const node = makeNodeStub();
|
||||
const calls = [];
|
||||
|
||||
inst.node = node;
|
||||
inst.RED = makeREDStub();
|
||||
inst.source = {
|
||||
set inputValue(v) { calls.push(v); },
|
||||
toggleSimulation() {},
|
||||
toggleOutlierDetection() {},
|
||||
calibrate() {},
|
||||
};
|
||||
|
||||
inst._attachInputHandler();
|
||||
const onInput = node._handlers.input;
|
||||
|
||||
onInput({ topic: 'measurement', payload: '42' }, () => {}, () => {});
|
||||
onInput({ topic: 'measurement', payload: { value: 42 } }, () => {}, () => {});
|
||||
|
||||
assert.equal(calls.length, 0);
|
||||
});
|
||||
12
test/edge/outlier-toggle.edge.test.js
Normal file
12
test/edge/outlier-toggle.edge.test.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { makeMeasurementInstance } = require('../helpers/factories');
|
||||
|
||||
test('toggleOutlierDetection currently converts config object to boolean (known gap)', () => {
|
||||
const m = makeMeasurementInstance();
|
||||
|
||||
assert.equal(typeof m.config.outlierDetection, 'object');
|
||||
m.toggleOutlierDetection();
|
||||
assert.equal(typeof m.config.outlierDetection, 'boolean');
|
||||
});
|
||||
0
test/helpers/.gitkeep
Normal file
0
test/helpers/.gitkeep
Normal file
111
test/helpers/factories.js
Normal file
111
test/helpers/factories.js
Normal file
@@ -0,0 +1,111 @@
|
||||
const Measurement = require('../../src/specificClass');
|
||||
|
||||
function makeUiConfig(overrides = {}) {
|
||||
return {
|
||||
unit: 'bar',
|
||||
enableLog: false,
|
||||
logLevel: 'error',
|
||||
supplier: 'vendor',
|
||||
category: 'sensor',
|
||||
assetType: 'pressure',
|
||||
model: 'PT-1',
|
||||
scaling: true,
|
||||
i_min: 0,
|
||||
i_max: 100,
|
||||
o_min: 0,
|
||||
o_max: 10,
|
||||
i_offset: 0,
|
||||
count: 5,
|
||||
smooth_method: 'mean',
|
||||
simulator: false,
|
||||
positionVsParent: 'atEquipment',
|
||||
hasDistance: false,
|
||||
distance: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeMeasurementConfig(overrides = {}) {
|
||||
return {
|
||||
general: {
|
||||
id: 'm-test-1',
|
||||
name: 'measurement-test',
|
||||
unit: 'bar',
|
||||
logging: { enabled: false, logLevel: 'error' },
|
||||
},
|
||||
asset: {
|
||||
uuid: '',
|
||||
tagCode: '',
|
||||
tagNumber: 'PT-001',
|
||||
supplier: 'vendor',
|
||||
category: 'sensor',
|
||||
type: 'pressure',
|
||||
model: 'PT-1',
|
||||
unit: 'bar',
|
||||
},
|
||||
scaling: {
|
||||
enabled: true,
|
||||
inputMin: 0,
|
||||
inputMax: 100,
|
||||
absMin: 0,
|
||||
absMax: 10,
|
||||
offset: 0,
|
||||
},
|
||||
smoothing: {
|
||||
smoothWindow: 5,
|
||||
smoothMethod: 'mean',
|
||||
},
|
||||
simulation: {
|
||||
enabled: false,
|
||||
},
|
||||
functionality: {
|
||||
positionVsParent: 'atEquipment',
|
||||
distance: undefined,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeNodeStub() {
|
||||
const handlers = {};
|
||||
const sent = [];
|
||||
const status = [];
|
||||
const warns = [];
|
||||
return {
|
||||
id: 'm-node-1',
|
||||
source: null,
|
||||
on(event, cb) { handlers[event] = cb; },
|
||||
send(msg) { sent.push(msg); },
|
||||
status(s) { status.push(s); },
|
||||
warn(w) { warns.push(w); },
|
||||
_handlers: handlers,
|
||||
_sent: sent,
|
||||
_status: status,
|
||||
_warns: warns,
|
||||
};
|
||||
}
|
||||
|
||||
function makeREDStub(nodeMap = {}) {
|
||||
return {
|
||||
nodes: {
|
||||
getNode(id) {
|
||||
return nodeMap[id] || null;
|
||||
},
|
||||
createNode() {},
|
||||
registerType() {},
|
||||
},
|
||||
httpAdmin: { get() {}, post() {} },
|
||||
};
|
||||
}
|
||||
|
||||
function makeMeasurementInstance(overrides = {}) {
|
||||
return new Measurement(makeMeasurementConfig(overrides));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
makeUiConfig,
|
||||
makeMeasurementConfig,
|
||||
makeNodeStub,
|
||||
makeREDStub,
|
||||
makeMeasurementInstance,
|
||||
};
|
||||
0
test/integration/.gitkeep
Normal file
0
test/integration/.gitkeep
Normal file
48
test/integration/examples-flows.integration.test.js
Normal file
48
test/integration/examples-flows.integration.test.js
Normal file
@@ -0,0 +1,48 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const EXAMPLES_DIR = path.resolve(__dirname, '../../examples');
|
||||
|
||||
function readFlow(file) {
|
||||
const full = path.join(EXAMPLES_DIR, file);
|
||||
const parsed = JSON.parse(fs.readFileSync(full, 'utf8'));
|
||||
assert.equal(Array.isArray(parsed), true);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function nodesByType(flow, type) {
|
||||
return flow.filter((n) => n && n.type === type);
|
||||
}
|
||||
|
||||
function injectByTopic(flow, topic) {
|
||||
return flow.filter((n) => n && n.type === 'inject' && n.topic === topic);
|
||||
}
|
||||
|
||||
test('examples package contains required files', () => {
|
||||
for (const name of ['README.md', 'basic.flow.json', 'integration.flow.json', 'edge.flow.json']) {
|
||||
assert.equal(fs.existsSync(path.join(EXAMPLES_DIR, name)), true, `${name} missing`);
|
||||
}
|
||||
});
|
||||
|
||||
test('basic flow has measurement node and baseline injects', () => {
|
||||
const flow = readFlow('basic.flow.json');
|
||||
assert.equal(nodesByType(flow, 'measurement').length >= 1, true);
|
||||
assert.equal(injectByTopic(flow, 'measurement').length >= 1, true);
|
||||
assert.equal(injectByTopic(flow, 'calibrate').length >= 1, true);
|
||||
});
|
||||
|
||||
test('integration flow has two measurement nodes and registerChild example', () => {
|
||||
const flow = readFlow('integration.flow.json');
|
||||
assert.equal(nodesByType(flow, 'measurement').length >= 2, true);
|
||||
assert.equal(injectByTopic(flow, 'registerChild').length >= 1, true);
|
||||
assert.equal(injectByTopic(flow, 'measurement').length >= 1, true);
|
||||
});
|
||||
|
||||
test('edge flow contains edge-driving injects', () => {
|
||||
const flow = readFlow('edge.flow.json');
|
||||
assert.equal(injectByTopic(flow, 'measurement').length >= 1, true);
|
||||
assert.equal(injectByTopic(flow, 'outlierDetection').length >= 1, true);
|
||||
assert.equal(injectByTopic(flow, 'doesNotExist').length >= 1, true);
|
||||
});
|
||||
37
test/integration/measurement-event.integration.test.js
Normal file
37
test/integration/measurement-event.integration.test.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { makeMeasurementInstance } = require('../helpers/factories');
|
||||
|
||||
test('updateOutputAbs emits measurement event with configured type/position', async () => {
|
||||
const m = makeMeasurementInstance({
|
||||
asset: {
|
||||
uuid: '',
|
||||
tagCode: '',
|
||||
tagNumber: 'PT-001',
|
||||
supplier: 'vendor',
|
||||
category: 'sensor',
|
||||
type: 'pressure',
|
||||
model: 'PT-1',
|
||||
unit: 'bar',
|
||||
},
|
||||
functionality: {
|
||||
positionVsParent: 'upstream',
|
||||
distance: undefined,
|
||||
},
|
||||
smoothing: {
|
||||
smoothWindow: 1,
|
||||
smoothMethod: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
const event = await new Promise((resolve) => {
|
||||
m.measurements.emitter.once('pressure.measured.upstream', resolve);
|
||||
m.calculateInput(30);
|
||||
});
|
||||
|
||||
assert.equal(event.type, 'pressure');
|
||||
assert.equal(event.variant, 'measured');
|
||||
assert.equal(event.position, 'upstream');
|
||||
assert.equal(typeof event.value, 'number');
|
||||
});
|
||||
Reference in New Issue
Block a user