| 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 stop leaves only a terminal job for the next solve to replace', async () => { |
| await withBrowserEnv({}, async ({ importModule, window }) => { |
| const { createSolverController } = await importModule('static/app/shell/solver-controller.mjs'); |
| const calls = []; |
| let onMessage; |
| let createCount = 0; |
| let latestPlan = null; |
| let restartProviderCalls = 0; |
|
|
| 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: 'CANCELLED', |
| currentScore: '0hard/-2soft', |
| bestScore: '0hard/-2soft', |
| solution: { id, revision, source: 'stopped-snapshot' }, |
| }), |
| analyzeSnapshot: async () => null, |
| pauseJob: async () => {}, |
| resumeJob: async (id) => calls.push(['resumeJob', id]), |
| cancelJob: async (id) => calls.push(['cancelJob', id]), |
| deleteJob: async (id) => calls.push(['deleteJob', id]), |
| }, |
| statusBar: null, |
| onPlan: (plan) => { |
| latestPlan = plan; |
| }, |
| onAnalysis: () => {}, |
| onMeta: () => {}, |
| onLifecycle: () => {}, |
| onError: () => {}, |
| }); |
|
|
| await controller.start(async () => ({ source: 'initial-plan' })); |
|
|
| const stop = controller.cancel(); |
| await flush(); |
| onMessage({ |
| eventType: 'cancelled', |
| jobId: 'job-1', |
| lifecycleState: 'CANCELLED', |
| snapshotRevision: 5, |
| currentScore: '0hard/-2soft', |
| bestScore: '0hard/-2soft', |
| }); |
| await stop; |
|
|
| assert.equal(controller.getLifecycleState(), 'CANCELLED'); |
| assert.deepEqual(latestPlan, { id: 'job-1', revision: 5, source: 'stopped-snapshot' }); |
|
|
| await controller.start(async () => { |
| restartProviderCalls += 1; |
| return latestPlan; |
| }); |
|
|
| assert.equal(createCount, 2); |
| assert.equal(restartProviderCalls, 1); |
| assert.equal(controller.getJobId(), 'job-2'); |
| assert.deepEqual(calls, [ |
| ['streamJobEvents', 'job-1'], |
| ['cancelJob', 'job-1'], |
| ['closeStream', 'job-1'], |
| ['deleteJob', 'job-1'], |
| ['streamJobEvents', 'job-2'], |
| ]); |
| }); |
| }); |
|
|
| test('solver controller aborts a retry when terminal cleanup fails', 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; |
|
|
| 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: 'COMPLETED', |
| currentScore: '0hard/0soft', |
| bestScore: '0hard/0soft', |
| solution: { id, revision }, |
| }), |
| analyzeSnapshot: async () => null, |
| pauseJob: async () => {}, |
| resumeJob: async () => {}, |
| cancelJob: async () => {}, |
| deleteJob: async (id) => { |
| calls.push(['deleteJob', id]); |
| throw new Error('delete failed'); |
| }, |
| }, |
| statusBar: null, |
| onPlan: () => {}, |
| onAnalysis: () => {}, |
| onMeta: () => {}, |
| onLifecycle: () => {}, |
| onError: () => {}, |
| }); |
|
|
| await controller.start(async () => ({})); |
| onMessage({ |
| eventType: 'completed', |
| jobId: 'job-1', |
| lifecycleState: 'COMPLETED', |
| snapshotRevision: 1, |
| currentScore: '0hard/0soft', |
| bestScore: '0hard/0soft', |
| }); |
| await flush(); |
|
|
| await assert.rejects( |
| () => controller.start(async () => { |
| planProviderCalls += 1; |
| return {}; |
| }), |
| /delete failed/, |
| ); |
|
|
| assert.equal(createCount, 1); |
| assert.equal(planProviderCalls, 0); |
| assert.deepEqual(calls, [ |
| ['streamJobEvents', 'job-1'], |
| ['closeStream', 'job-1'], |
| ['deleteJob', 'job-1'], |
| ]); |
| }); |
| }); |
|
|