feat(mgc-config + state): planner.useRendezvous schema + remaining-transition reads
Three coherent additions that the MGC rendezvous planner depends on: - machineGroupControl.json: new `planner.useRendezvous` boolean (default true). Used by both `_optimalControl` and `equalFlowControl` (via the shared `_dispatchFlowDistribution` helper) to gate same-time-landing. - state.js: external aborts (returnToOperational=false) bump a monotonic `sequenceAbortToken`. executeSequence captures it at entry and bails out of its for-loop if it advances mid-sequence, so a shutdown that's past its ramp-down step doesn't barge through stopping → coolingdown when a fresher demand re-engages the pump. - stateManager.js: new `getRemainingTransitionS()` returns the seconds remaining in a timed state by reading the wall-clock entry timestamp. buildProfile() reads it so the planner can compute exact eta for a child that's currently mid-ladder (warmingup / starting / cooling). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
77
test/basic/stateManagerRemaining.basic.test.js
Normal file
77
test/basic/stateManagerRemaining.basic.test.js
Normal file
@@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const StateManager = require('../../src/state/stateManager');
|
||||
|
||||
// Minimal config that satisfies the stateManager constructor's expectations.
|
||||
// Real configs come from configs/<node>.json; we hand-roll one here so the
|
||||
// test doesn't drag the whole node-config plumbing in for a 30-line getter.
|
||||
function makeConfig(initial = 'idle', times = { idle: 0, warmingup: 5 }) {
|
||||
return {
|
||||
state: {
|
||||
current: initial,
|
||||
available: ['idle', 'warmingup', 'operational'],
|
||||
descriptions: { idle: 'off', warmingup: 'warming', operational: 'running' },
|
||||
allowedTransitions: {
|
||||
idle: new Set(['warmingup']),
|
||||
warmingup: new Set(['operational']),
|
||||
operational: new Set(['idle']),
|
||||
},
|
||||
activeStates: new Set(['operational']),
|
||||
},
|
||||
time: times,
|
||||
};
|
||||
}
|
||||
|
||||
const noopLogger = { debug() {}, info() {}, warn() {}, error() {} };
|
||||
|
||||
test('getRemainingTransitionS returns 0 for untimed initial state', () => {
|
||||
const sm = new StateManager(makeConfig('idle'), noopLogger);
|
||||
assert.equal(sm.getRemainingTransitionS(), 0);
|
||||
});
|
||||
|
||||
test('getRemainingTransitionS returns ≈full duration just after entering a timed state', async () => {
|
||||
const sm = new StateManager(makeConfig('idle', { idle: 0, warmingup: 5 }), noopLogger);
|
||||
// Force-enter "warmingup" via the constructor's state machinery: simulate
|
||||
// by manually setting fields the way transitionTo would.
|
||||
sm.currentState = 'warmingup';
|
||||
sm.stateEnteredAt = Date.now();
|
||||
const remaining = sm.getRemainingTransitionS();
|
||||
assert.ok(remaining > 4.9 && remaining <= 5.0, `expected ~5s, got ${remaining}`);
|
||||
});
|
||||
|
||||
test('getRemainingTransitionS decays with elapsed time', async () => {
|
||||
const sm = new StateManager(makeConfig('idle', { idle: 0, warmingup: 5 }), noopLogger);
|
||||
sm.currentState = 'warmingup';
|
||||
sm.stateEnteredAt = Date.now() - 2000; // pretend we entered 2s ago
|
||||
const remaining = sm.getRemainingTransitionS();
|
||||
assert.ok(remaining > 2.9 && remaining <= 3.0, `expected ~3s, got ${remaining}`);
|
||||
});
|
||||
|
||||
test('getRemainingTransitionS clamps to 0 once duration has elapsed', () => {
|
||||
const sm = new StateManager(makeConfig('idle', { idle: 0, warmingup: 5 }), noopLogger);
|
||||
sm.currentState = 'warmingup';
|
||||
sm.stateEnteredAt = Date.now() - 60_000; // a minute ago, way past 5s
|
||||
assert.equal(sm.getRemainingTransitionS(), 0);
|
||||
});
|
||||
|
||||
test('transitionTo refreshes stateEnteredAt on the immediate branch', async () => {
|
||||
const sm = new StateManager(makeConfig('idle', { idle: 0 }), noopLogger);
|
||||
const before = sm.stateEnteredAt;
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
await sm.transitionTo('warmingup');
|
||||
assert.ok(sm.stateEnteredAt > before, 'stateEnteredAt should advance on transition');
|
||||
});
|
||||
|
||||
test('transitionTo refreshes stateEnteredAt on the timed branch', async () => {
|
||||
// Tiny duration so the test stays fast.
|
||||
const sm = new StateManager(makeConfig('idle', { idle: 0.05, warmingup: 0 }), noopLogger);
|
||||
const before = sm.stateEnteredAt;
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
await sm.transitionTo('warmingup');
|
||||
assert.ok(sm.stateEnteredAt > before, 'stateEnteredAt should advance after timed transition');
|
||||
// And remaining should now be 0 (we're in warmingup, but warmingup duration is 0).
|
||||
assert.equal(sm.getRemainingTransitionS(), 0);
|
||||
});
|
||||
Reference in New Issue
Block a user