import { execFile } from 'node:child_process'; import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { promisify } from 'node:util'; import { Worker as NodeWorker } from 'node:worker_threads'; import { vi } from '../../testing/vitest-compat'; import { createPlanningCalculation } from './leave-planner.service'; import type { OptimizationPlanningRule } from './leave-planner.core'; import { OPTIMIZATION_PRESETS } from './planning-objectives'; describe('calendar objectives in the real worker', () => { let directory: string; let bundle: string; const threads = new Set(); const messages: string[] = []; let stopped = 0; beforeAll(async () => { directory = mkdtempSync(join(tmpdir(), 'vac-calendar-worker-')); bundle = join(directory, 'worker.mjs'); const entry = join(directory, 'entry.ts'); writeFileSync( entry, `import { parentPort } from 'node:worker_threads'; globalThis.self = { postMessage: (message) => parentPort.postMessage(message) }; await import(${JSON.stringify(resolve('src/app/services/leave-planner.worker.ts'))}); parentPort.on('message', (data) => globalThis.self.onmessage({ data })); `, ); await promisify(execFile)(process.execPath, [ 'build', entry, '--target=node', '--outfile', bundle, ]); }); beforeEach(() => { messages.length = 0; stopped = 0; vi.setSystemTime(new Date('2024-04-01T12:00:00Z')); class BrowserWorker { onmessage: ((event: MessageEvent) => void) | null = null; onerror: (() => void) | null = null; private readonly thread = new NodeWorker(bundle); constructor() { threads.add(this.thread); this.thread.on('message', (data) => { messages.push(data.type); this.onmessage?.({ data } as MessageEvent); }); this.thread.on('error', () => this.onerror?.()); } postMessage(message: unknown) { this.thread.postMessage(message); } terminate() { stopped++; void this.thread.terminate(); } } vi.stubGlobal('Worker', BrowserWorker); }); afterEach(async () => { await Promise.all([...threads].map((thread) => thread.terminate())); threads.clear(); vi.useRealTimers(); vi.unstubAllGlobals(); }); afterAll(() => rmSync(directory, { recursive: true, force: true })); it.each([false, true])( 'retains the submitted configuration, plan and artifact order (gain first: %s)', async (gainFirst) => { const holiday: OptimizationPlanningRule = { id: 'holiday', type: 'optimization' as const, name: 'Captured holiday rule', configuration: structuredClone(OPTIMIZATION_PRESETS.holidayWeekValue), }; const gain: OptimizationPlanningRule = { id: 'gain', type: 'optimization' as const, name: 'Captured gain rule', configuration: structuredClone(OPTIMIZATION_PRESETS.nonWorkingDayGain), }; const rules = gainFirst ? [gain, holiday] : [holiday, gain]; const calculation = createPlanningCalculation({ horizon: { start: '2024-04-29', end: '2024-05-18' }, balances: [ { id: 'cp', type: 'cp', quantity: 3, availableFrom: '2024-04-29', expiryDate: '2025-01-01', }, ], rules, }); holiday.configuration = { ...holiday.configuration, conditions: [], direction: 'minimize', }; const plan = await calculation.outcome; if (plan.kind !== 'Plan') throw new Error(`Expected a worker plan, received ${plan.kind}`); expect(plan.metrics).toMatchObject( gainFirst ? { holidayWeekValue: 0, nonWorkingDayGain: 9 } : { holidayWeekValue: 5, nonWorkingDayGain: 6 }, ); expect(calculation.snapshot.rules.find((rule) => rule.id === 'holiday')).toMatchObject({ name: 'Captured holiday rule', configuration: OPTIMIZATION_PRESETS.holidayWeekValue, }); const first = `optimization:${rules[0].id}`; const explanation = await calculation.explanation; if (explanation?.kind !== 'PlanExplanation') throw new Error('Expected a worker explanation'); expect(explanation.placements[0].firstDecisiveObjective).toBe(first); await calculation.timings; expect(new Set(messages)).toEqual( new Set([ 'heartbeat', 'progress', 'checkpoint', 'result', 'explanation', 'artifacts', ]), ); expect(messages.indexOf('result')).toBeLessThan(messages.indexOf('explanation')); expect(stopped).toBe(1); const comparison = await calculation.analyzeCounterfactual(plan.placements[0]); if (comparison.kind !== 'Counterfactual') throw new Error('Expected a worker comparison'); expect( comparison.results.find((result) => result.date === plan.placements[0].date) ?.comparison?.firstDecisiveObjective, ).toBe(first); }, ); it.each([false, true])( 'lets economy block or permit the complete holiday bundle in a real worker (economy first: %s)', async (economyFirst) => { const economy: OptimizationPlanningRule = { id: 'economy', name: 'Captured supporting economy', type: 'optimization', configuration: structuredClone(OPTIMIZATION_PRESETS.supportingPlacements), }; const holiday: OptimizationPlanningRule = { id: 'holiday', type: 'optimization', configuration: OPTIMIZATION_PRESETS.holidayWeekValue, }; const rules = economyFirst ? [economy, holiday] : [holiday, economy]; const calculation = createPlanningCalculation({ horizon: { start: '2026-11-09', end: '2026-11-14' }, balances: [ { id: 'cp', type: 'cp', quantity: 2, availableFrom: '2026-11-09', expiryDate: '2027-01-01', }, ], declaredDays: ['2026-11-09', '2026-11-10'].map((date) => ({ date, type: 'other' })), rules, }); economy.name = 'Edited later'; economy.configuration.conditions = []; const outcome = await calculation.outcome; if (outcome.kind !== 'Plan') throw new Error('Expected a worker plan'); expect(outcome.metrics).toMatchObject( economyFirst ? { holidayWeekValue: 0, supportingPlacements: 0 } : { holidayWeekValue: 3, supportingPlacements: 2 }, ); expect(calculation.snapshot.rules.find(({ id }) => id === 'economy')).toMatchObject({ name: 'Captured supporting economy', configuration: OPTIMIZATION_PRESETS.supportingPlacements, }); const explanation = await calculation.explanation; if (explanation?.kind !== 'PlanExplanation') throw new Error('Expected an explanation'); if (economyFirst) { expect(outcome.placements).toEqual([]); expect(outcome.remainingBalances[0]).toMatchObject({ id: 'cp', quantity: 2, classification: 'deferred', }); expect(explanation.placements).toEqual([]); return; } expect(outcome.remainingBalances).toEqual([]); expect(explanation.placements.every(({ supporting }) => supporting)).toBe(true); expect(explanation.placements[0].firstDecisiveObjective).toBe('optimization:holiday'); expect(explanation.placements[0].objectiveVector.objectives).toContainEqual( expect.objectContaining({ id: 'optimization:economy', value: 2, label: 'Captured supporting economy', }), ); const analysis = await calculation.analyzeCounterfactual(outcome.placements[0]); if (analysis.kind !== 'Counterfactual') throw new Error('Expected a counterfactual'); expect( analysis.results.find(({ date }) => date === outcome.placements[0].date)?.comparison ?.after.objectives, ).toContainEqual(expect.objectContaining({ id: 'optimization:economy', value: 2 })); }, ); });