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>
122 lines
3.9 KiB
JavaScript
122 lines
3.9 KiB
JavaScript
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/);
|
|
});
|