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'], ]); }); });