P3 wave 1: extract measurement simulator/calibration/commands + CONTRACT
src/simulation/simulator.js random-walk generator (was simulateInput inline)
src/calibration/calibrator.js calibrate + isStable + evaluateRepeatability,
using generalFunctions/stats. NB: isStable
tautology preserved verbatim — see
OPEN_QUESTIONS.md 2026-05-10 for the bug.
src/commands/ registry + handlers (canonical names from start)
CONTRACT.md inputs/outputs/events surface
77 basic tests pass (62 pre-refactor + 15 new across the three new files).
specificClass.js / nodeClass.js untouched — integration is P3 wave 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
121
test/basic/simulator.basic.test.js
Normal file
121
test/basic/simulator.basic.test.js
Normal file
@@ -0,0 +1,121 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const Simulator = require('../../src/simulation/simulator.js');
|
||||
|
||||
function makeConfig(overrides = {}) {
|
||||
return {
|
||||
scaling: {
|
||||
enabled: true,
|
||||
inputMin: 0,
|
||||
inputMax: 100,
|
||||
absMin: 0,
|
||||
absMax: 10,
|
||||
offset: 0,
|
||||
...(overrides.scaling || {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeFakeLogger() {
|
||||
const log = { warn: [], info: [], debug: [], error: [] };
|
||||
return {
|
||||
log,
|
||||
warn: (m) => log.warn.push(m),
|
||||
info: (m) => log.info.push(m),
|
||||
debug: (m) => log.debug.push(m),
|
||||
error: (m) => log.error.push(m),
|
||||
};
|
||||
}
|
||||
|
||||
// Replace Math.random with a deterministic queue, restore on cleanup.
|
||||
function stubRandom(values) {
|
||||
const orig = Math.random;
|
||||
let i = 0;
|
||||
Math.random = () => (i < values.length ? values[i++] : 0);
|
||||
return () => { Math.random = orig; };
|
||||
}
|
||||
|
||||
test('constructor derives inputRange when scaling.enabled=true', () => {
|
||||
const sim = new Simulator({ config: makeConfig() });
|
||||
assert.equal(sim.inputRange, 100);
|
||||
assert.equal(sim.processRange, 10);
|
||||
assert.equal(sim.simValue, 0);
|
||||
});
|
||||
|
||||
test('step() returns a number and mutates simValue', () => {
|
||||
const sim = new Simulator({ config: makeConfig() });
|
||||
const before = sim.simValue;
|
||||
const out = sim.step();
|
||||
assert.equal(typeof out, 'number');
|
||||
assert.notEqual(out, before);
|
||||
assert.equal(out, sim.simValue);
|
||||
});
|
||||
|
||||
test('step() is deterministic when Math.random is stubbed', () => {
|
||||
// sign-roll then magnitude. With scaling enabled inputRange=100 -> maxStep=5.
|
||||
// 0.4 < 0.5 => sign = -1; 0.2 magnitude => -1 * 0.2 * 5 = -1.
|
||||
const restore = stubRandom([0.4, 0.2]);
|
||||
try {
|
||||
const sim = new Simulator({ config: makeConfig() });
|
||||
const v = sim.step();
|
||||
assert.equal(v, -1);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('step() clamps an out-of-range starting value and warns (scaling enabled)', () => {
|
||||
const restore = stubRandom([0.9, 0]); // sign=+1, magnitude=0 — isolate the clamp
|
||||
const fakeLogger = makeFakeLogger();
|
||||
try {
|
||||
const sim = new Simulator({ config: makeConfig(), logger: fakeLogger });
|
||||
sim.simValue = 500; // outside [0,100]
|
||||
sim.step();
|
||||
assert.equal(sim.simValue, 100, 'clamped to inputMax before stepping');
|
||||
assert.equal(fakeLogger.log.warn.length, 1);
|
||||
assert.match(fakeLogger.log.warn[0], /outside of input range/);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('step() clamps against abs range when scaling.enabled=false', () => {
|
||||
const restore = stubRandom([0.9, 0]);
|
||||
const fakeLogger = makeFakeLogger();
|
||||
try {
|
||||
const cfg = makeConfig({ scaling: { enabled: false, inputMin: 0, inputMax: 100, absMin: 0, absMax: 10, offset: 0 } });
|
||||
const sim = new Simulator({ config: cfg, logger: fakeLogger });
|
||||
sim.simValue = -5;
|
||||
sim.step();
|
||||
assert.equal(sim.simValue, 0, 'clamped to absMin');
|
||||
assert.match(fakeLogger.log.warn[0], /outside of abs range/);
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('reset() zeros simValue', () => {
|
||||
const sim = new Simulator({ config: makeConfig() });
|
||||
sim.simValue = 42;
|
||||
sim.reset();
|
||||
assert.equal(sim.simValue, 0);
|
||||
assert.equal(sim.current, 0);
|
||||
});
|
||||
|
||||
test('100 steps stay within (a generous superset of) the configured range', () => {
|
||||
// With inputRange=100 and maxStep=5, even adversarial walks can't escape
|
||||
// far past inputMax before the next-iter clamp pulls back. Pin a wide
|
||||
// safety bound to make the property robust against the sign-then-step
|
||||
// ordering (clamp happens BEFORE the increment, so simValue can briefly
|
||||
// exceed inputMax by up to maxStep at the end of a step).
|
||||
const sim = new Simulator({ config: makeConfig() });
|
||||
for (let i = 0; i < 100; i++) sim.step();
|
||||
assert.ok(sim.simValue > -10, `walked below -10: ${sim.simValue}`);
|
||||
assert.ok(sim.simValue < 110, `walked above 110: ${sim.simValue}`);
|
||||
});
|
||||
|
||||
test('constructor throws on missing scaling config', () => {
|
||||
assert.throws(() => new Simulator({ config: {} }), /scaling/);
|
||||
assert.throws(() => new Simulator({}), /scaling/);
|
||||
});
|
||||
Reference in New Issue
Block a user