solverforge-hospital / tests /frontend /solver-controller.test.js
blackopsrepl's picture
refactor(app): split oversized modules
cbea823
const assert = require('node:assert/strict');
const test = require('node:test');
const { withBrowserEnv } = require('./support/load-browser-modules');
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
test('solver controller keeps the retained job after transport interruption', async () => {
await withBrowserEnv({}, async ({ importModule, window }) => {
const { createSolverController } = await importModule('static/app/shell/solver-controller.mjs');
const calls = [];
let onStreamError;
let createCount = 0;
let planProviderCalls = 0;
const controller = createSolverController({
sf: window.SF,
backend: {
createJob: async () => {
createCount += 1;
return 'job-retained';
},
streamJobEvents(id, onMessage, onError) {
calls.push(['streamJobEvents', id]);
onStreamError = onError;
onMessage({
eventType: 'progress',
jobId: id,
lifecycleState: 'SOLVING',
currentScore: '0hard/-1soft',
bestScore: '0hard/-1soft',
});
return () => calls.push(['closeStream']);
},
getSnapshot: async () => null,
analyzeSnapshot: async () => null,
pauseJob: async () => {},
resumeJob: async () => {},
cancelJob: async (id) => calls.push(['cancelJob', id]),
deleteJob: async () => {},
},
statusBar: null,
onPlan: () => {},
onAnalysis: () => {},
onMeta: () => {},
onLifecycle: () => {},
onError: () => {},
});
await controller.start(async () => ({}));
onStreamError(new Error('temporary disconnect'));
await flush();
assert.equal(controller.getJobId(), 'job-retained');
assert.equal(controller.getLifecycleState(), 'SOLVING');
await controller.start(async () => {
planProviderCalls += 1;
return {};
});
void controller.cancel();
await flush();
assert.equal(createCount, 1);
assert.equal(planProviderCalls, 0);
assert.deepEqual(calls, [
['streamJobEvents', 'job-retained'],
['closeStream'],
['streamJobEvents', 'job-retained'],
['cancelJob', 'job-retained'],
]);
});
});
test('solver controller pauses and resumes the retained runtime without starting over', async () => {
await withBrowserEnv({}, async ({ importModule, window }) => {
const { createSolverController } = await importModule('static/app/shell/solver-controller.mjs');
const calls = [];
let onMessage;
let createCount = 0;
let planProviderCalls = 0;
let latestPlan = null;
const controller = createSolverController({
sf: window.SF,
backend: {
createJob: async () => {
createCount += 1;
return `job-${createCount}`;
},
streamJobEvents(id, callback) {
calls.push(['streamJobEvents', id]);
onMessage = callback;
return () => calls.push(['closeStream', id]);
},
getSnapshot: async (id, revision) => ({
id,
jobId: id,
snapshotRevision: revision,
lifecycleState: 'PAUSED',
currentScore: '0hard/-4soft',
bestScore: '0hard/-4soft',
solution: { id, revision, source: 'paused-snapshot' },
}),
analyzeSnapshot: async () => null,
pauseJob: async (id) => calls.push(['pauseJob', id]),
resumeJob: async (id) => calls.push(['resumeJob', id]),
cancelJob: async () => {},
deleteJob: async () => {},
},
statusBar: null,
onPlan: (plan) => {
latestPlan = plan;
},
onAnalysis: () => {},
onMeta: () => {},
onLifecycle: () => {},
onError: () => {},
});
await controller.start(async () => ({ source: 'initial-plan' }));
const pause = controller.pause();
await flush();
onMessage({
eventType: 'paused',
jobId: 'job-1',
lifecycleState: 'PAUSED',
snapshotRevision: 7,
currentScore: '0hard/-4soft',
bestScore: '0hard/-4soft',
});
await pause;
assert.equal(controller.getLifecycleState(), 'PAUSED');
assert.deepEqual(latestPlan, { id: 'job-1', revision: 7, source: 'paused-snapshot' });
await controller.start(async () => {
planProviderCalls += 1;
return {};
});
const resume = controller.resume();
await flush();
onMessage({
eventType: 'resumed',
jobId: 'job-1',
lifecycleState: 'SOLVING',
snapshotRevision: 7,
});
await resume;
assert.equal(createCount, 1);
assert.equal(planProviderCalls, 0);
assert.equal(controller.getJobId(), 'job-1');
assert.equal(controller.getLifecycleState(), 'SOLVING');
assert.deepEqual(calls, [
['streamJobEvents', 'job-1'],
['pauseJob', 'job-1'],
['resumeJob', 'job-1'],
]);
});
});