const test = require('node:test'); const assert = require('node:assert/strict'); const EventEmitter = require('events'); const MovementManager = require('../src/state/movementManager'); const noopLogger = { debug() {}, info() {}, warn() {}, error() {} }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); function makeManager({ mode = 'staticspeed', speed = 50, interval = 1000, initial = 0 } = {}) { // speed%/s on a 0..100 range → velocity = speed %/s. interval defaults to the // production 1000ms so the abort-before-first-tick race is reproduced exactly. return new MovementManager( { position: { min: 0, max: 100, initial }, movement: { mode, speed, maxSpeed: 1000, interval }, }, noopLogger, new EventEmitter(), ); } // Regression: before the time-based fix, currentPosition only advanced inside // setInterval(…, interval). An abort landing before the first tick (the MGC's // ~1s re-command cadence vs the 1000ms tick) left the pump frozen at the start. for (const mode of ['staticspeed', 'dynspeed']) { test(`${mode}: abort before the first tick still advances position (no freeze)`, async () => { const mgr = makeManager({ mode, speed: 50, interval: 1000 }); const ac = new AbortController(); const moving = mgr.moveTo(80, ac.signal); // ~1.6s of travel; first tick at 1000ms await sleep(200); // interrupt well before the first tick ac.abort(); await moving; const pos = mgr.getCurrentPosition(); // The fix: any non-zero progress means the abort re-based instead of // freezing at the start. (dynspeed eases in, so its early travel is small // but must still be > 0; staticspeed travels ~velocity·elapsed.) assert.ok(pos > 0, `expected partial progress, got frozen at ${pos}`); assert.ok(pos < 80, `should not have reached target, got ${pos}`); }); test(`${mode}: a fresh setpoint re-bases from the interrupted position`, async () => { const mgr = makeManager({ mode, speed: 50, interval: 1000 }); const ac1 = new AbortController(); const m1 = mgr.moveTo(80, ac1.signal); await sleep(200); ac1.abort(); await m1; const afterFirst = mgr.getCurrentPosition(); // New command toward 0 must start from afterFirst, not from 80 or a reset. const ac2 = new AbortController(); const m2 = mgr.moveTo(0, ac2.signal); await sleep(100); ac2.abort(); await m2; const afterSecond = mgr.getCurrentPosition(); assert.ok(afterSecond < afterFirst, `expected re-base downward from ${afterFirst}, got ${afterSecond}`); assert.ok(afterSecond >= 0, `position must stay in range, got ${afterSecond}`); }); } test('staticspeed: an uninterrupted move reaches the exact target', async () => { const mgr = makeManager({ mode: 'staticspeed', speed: 500, interval: 10 }); // fast await mgr.moveTo(40, new AbortController().signal); assert.equal(mgr.getCurrentPosition(), 40); }); test('position is clamped to [min,max] on a re-based abort', async () => { const mgr = makeManager({ mode: 'staticspeed', speed: 5000, interval: 1000, initial: 0 }); const ac = new AbortController(); const moving = mgr.moveTo(100, ac.signal); await sleep(150); ac.abort(); await moving; const pos = mgr.getCurrentPosition(); assert.ok(pos >= 0 && pos <= 100, `clamped, got ${pos}`); });