import { vi } from '../../testing/vitest-compat'; import { calculatePlanningInternal, type OptimizationPlanningRule, type PlanOutcome, type PlanningRequest, } from './leave-planner.core'; import { OPTIMIZATION_PRESETS } from './planning-objectives'; import { createPlanningCalculation } from './leave-planner.service'; const deficit: OptimizationPlanningRule = { id: 'deficit', name: 'Internal work-sequence deficit', type: 'optimization', configuration: OPTIMIZATION_PRESETS.shortSequenceDeficit, }; const longest: OptimizationPlanningRule = { id: 'longest', name: 'Longest work sequence', type: 'optimization', configuration: OPTIMIZATION_PRESETS.longestWorkSequence, }; const gap: PlanningRequest = { horizon: { start: '2024-01-08', end: '2024-01-13' }, balances: [{ id: 'cp', type: 'cp', quantity: 3, expiryDate: '2024-02-01' }], declaredDays: ['2024-01-08', '2024-01-12'].map((date) => ({ date, type: 'other' })), rules: [deficit], }; function plan(request: PlanningRequest): PlanOutcome { const outcome = calculatePlanningInternal(request); if (outcome.kind !== 'Plan') throw new Error(`Expected a plan, got ${outcome.kind}`); return outcome; } describe('composed work-sequence preferences', () => { beforeEach(() => vi.setSystemTime(new Date('2024-01-01T12:00:00Z'))); afterEach(() => vi.useRealTimers()); it('fills a complete internal sequence despite the worse deficit of partial additions', () => { const outcome = plan(gap); expect(outcome.placements.map(({ date }) => date)).toEqual([ '2024-01-09', '2024-01-10', '2024-01-11', ]); expect(outcome.placements.every(({ supporting }) => supporting)).toBe(true); expect(outcome.declaredDays).toEqual(gap.declaredDays); expect(outcome.metrics.shortSequenceDeficit).toBe(0); expect(outcome.remainingBalances).toEqual([]); }); it('splits tied longest sequences together when neither single addition improves the maximum', () => { const outcome = plan({ ...gap, balances: [{ id: 'cp', type: 'cp', quantity: 2, expiryDate: '2024-02-01' }], declaredDays: [{ date: '2024-01-10', type: 'other' }], rules: [longest], }); expect(outcome.placements.map(({ date }) => date)).toEqual(['2024-01-08', '2024-01-11']); expect(outcome.metrics.longestWorkSequence).toBe(1); }); it('leaves a three-day internal sequence intact at threshold three instead of filling it at five', () => { const outcome = plan({ ...gap, rules: [ { ...deficit, configuration: { ...OPTIMIZATION_PRESETS.shortSequenceDeficit, thresholdDays: 3, }, }, ], }); expect(outcome.placements).toEqual([]); expect(outcome.remainingBalances[0].quantity).toBe(3); expect(outcome.metrics.shortSequenceDeficit).toBe(2); }); it('counts selected internal sequences even when their measured deficit is zero', () => { const outcome = plan({ ...gap, rules: [ { ...deficit, configuration: { ...OPTIMIZATION_PRESETS.shortSequenceDeficit, thresholdDays: 3, aggregation: 'count', }, }, ], }); expect(outcome.placements.map(({ date }) => date)).toEqual([ '2024-01-09', '2024-01-10', '2024-01-11', ]); }); it('evaluates two parameterized configurations separately and follows their order', () => { const maximizeThree: OptimizationPlanningRule = { ...deficit, id: 'three', configuration: { ...OPTIMIZATION_PRESETS.shortSequenceDeficit, thresholdDays: 3, direction: 'maximize', }, }; const fiveFirst = plan({ ...gap, rules: [deficit, maximizeThree] }); const threeFirst = plan({ ...gap, rules: [maximizeThree, deficit] }); expect(fiveFirst.placements.map(({ date }) => date)).toEqual([ '2024-01-09', '2024-01-10', '2024-01-11', ]); expect(threeFirst.placements.map(({ date }) => date)).toEqual(['2024-01-10']); expect(threeFirst.metrics.shortSequenceDeficit).toBe(8); }); it('obeys the order between the longest-sequence preference and a personal window', () => { const personal = { id: 'personal', type: 'time_window' as const, startDate: '2024-01-08', endDate: '2024-01-08', placeDays: 1, placeType: 'cp', }; const request: PlanningRequest = { horizon: { start: '2024-01-08', end: '2024-01-20' }, balances: [{ id: 'cp', type: 'cp', quantity: 1, expiryDate: '2024-02-01' }], rules: [], }; const longestFirst = plan({ ...request, rules: [longest, personal] }); const personalFirst = plan({ ...request, rules: [personal, longest] }); expect(longestFirst.placements.map(({ date }) => date)).toEqual(['2024-01-12']); expect(longestFirst.metrics.longestWorkSequence).toBe(5); expect(personalFirst.placements.map(({ date }) => date)).toEqual(['2024-01-08']); expect(personalFirst.metrics.longestWorkSequence).toBe(9); }); it('keeps obligations and earlier preferences when a complete gap would consume their balance', () => { const personal = { id: 'personal', type: 'time_window' as const, startDate: '2024-01-15', endDate: '2024-01-15', placeDays: 1, placeType: 'cp', }; const request = { ...gap, horizon: { start: '2024-01-08', end: '2024-01-16' } }; expect( plan({ ...request, rules: [deficit, personal] }).placements.map(({ date }) => date), ).toEqual(['2024-01-09', '2024-01-10', '2024-01-11']); for (const force of ['preference', 'obligation'] as const) { const outcome = plan({ ...request, rules: [{ ...personal, force }, deficit] }); expect(outcome.placements.map(({ date }) => date)).toEqual(['2024-01-15']); expect(outcome.metrics.shortSequenceDeficit).toBe(2); } }); it('uses only eligible balance dates when completing the entire gap', () => { const outcome = plan({ ...gap, balances: [ { id: 'early', type: 'rtt', quantity: 1, expiryDate: '2024-01-09' }, { id: 'late', type: 'cp', quantity: 2, availableFrom: '2024-01-10', expiryDate: '2024-02-01', }, ], }); expect(outcome.placements.map(({ date, balanceId }) => ({ date, balanceId }))).toEqual([ { date: '2024-01-09', balanceId: 'early' }, { date: '2024-01-10', balanceId: 'late' }, { date: '2024-01-11', balanceId: 'late' }, ]); expect(outcome.placements.map(({ supporting }) => supporting)).toEqual([false, true, true]); const unavailable = plan({ ...gap, balances: [ { id: 'late', type: 'cp', quantity: 3, availableFrom: '2024-01-10', expiryDate: '2024-02-01', }, ], }); expect(unavailable.placements).toEqual([]); }); it('adds no neutral placement when the budget cannot split all tied maxima', () => { const outcome = plan({ ...gap, balances: [{ id: 'cp', type: 'cp', quantity: 1, expiryDate: '2024-02-01' }], declaredDays: [{ date: '2024-01-10', type: 'other' }], rules: [longest], }); expect(outcome.placements).toEqual([]); expect(outcome.metrics.longestWorkSequence).toBe(2); }); it('splits tied sequences at eligible dates when their centers are unavailable', () => { const outcome = plan({ horizon: { start: '2024-01-08', end: '2024-01-19' }, declaredDays: [{ date: '2024-01-12', type: 'other' }], balances: [ { id: 'early', type: 'cp', quantity: 1, expiryDate: '2024-01-08' }, { id: 'late', type: 'cp', quantity: 1, availableFrom: '2024-01-18', expiryDate: '2024-02-01', }, ], rules: [longest], }); expect(outcome.placements.map(({ date }) => date)).toEqual(['2024-01-08', '2024-01-18']); expect(outcome.metrics.longestWorkSequence).toBe(3); }); it('coordinates scarce balances when individually eligible centers cannot be funded together', () => { const outcome = plan({ horizon: { start: '2024-01-08', end: '2024-01-19' }, declaredDays: [{ date: '2024-01-12', type: 'other' }], balances: [ { id: 'early', type: 'cp', quantity: 1, expiryDate: '2024-01-08' }, { id: 'flexible', type: 'cp', quantity: 1, expiryDate: '2024-02-01' }, ], rules: [longest], }); expect(outcome.placements.map(({ date }) => date)).toEqual(['2024-01-08', '2024-01-15']); expect(outcome.metrics.longestWorkSequence).toBe(3); }); it('eliminates tied maximum deficits as one complete gap bundle', () => { const outcome = plan({ horizon: { start: '2024-01-08', end: '2024-01-17' }, declaredDays: ['2024-01-08', '2024-01-11', '2024-01-16'].map((date) => ({ date, type: 'other', })), balances: [{ id: 'cp', type: 'cp', quantity: 4, expiryDate: '2024-02-01' }], rules: [ { ...deficit, configuration: { ...OPTIMIZATION_PRESETS.shortSequenceDeficit, aggregation: 'maximum', }, }, ], }); expect(outcome.placements.map(({ date }) => date)).toEqual([ '2024-01-09', '2024-01-10', '2024-01-12', '2024-01-15', ]); expect(outcome.metrics.shortSequenceDeficit).toBe(0); }); it('includes both horizon edges in the maximum while excluding them from the deficit', () => { const outcome = plan({ horizon: { start: '2024-01-08', end: '2024-01-25' }, balances: [], declaredDays: ['2024-01-11', '2024-01-15', '2024-01-22'].map((date) => ({ date, type: 'other', })), rules: [longest, deficit], }); expect(outcome.metrics).toMatchObject({ longestWorkSequence: 4, shortSequenceDeficit: 5 }); }); it.each([ { horizon: { start: '2024-05-06', end: '2024-05-15' }, dates: [], longest: 5 }, { horizon: { start: '2024-01-08', end: '2024-01-11' }, dates: ['2024-01-08', '2024-01-09', '2024-01-10'], longest: 0, }, { horizon: { start: '2024-01-06', end: '2024-01-08' }, dates: [], longest: 0 }, { horizon: { start: '2024-01-08', end: '2024-01-13' }, dates: ['2024-01-09', '2024-01-10'], longest: 2, }, ])( 'keeps holiday/weekend continuity and zero for empty sequences: $horizon.start', ({ horizon, dates, longest: expected }) => { const outcome = plan({ horizon, balances: [], declaredDays: dates.map((date) => ({ date, type: 'other' })), rules: [longest, deficit], }); expect(outcome.metrics.longestWorkSequence).toBe(expected); expect(outcome.metrics.shortSequenceDeficit).toBe(0); }, ); it('disabling work-sequence rules removes their influence even with deferred balances', () => { const absent = plan({ ...gap, rules: [] }); const disabled = plan({ ...gap, rules: [ { ...deficit, enabled: false }, { ...longest, enabled: false }, ], }); expect(disabled).toEqual(absent); const personal = { id: 'personal', type: 'time_window' as const, startDate: '2024-01-09', endDate: '2024-01-09', placeDays: 1, placeType: 'cp', }; const requested = plan({ ...gap, rules: [{ ...deficit, enabled: false }, personal] }); expect(requested.placements.map(({ date }) => date)).toEqual(['2024-01-09']); expect(requested.metrics.shortSequenceDeficit).toBe(3); }); it('finishes a rejected tied-sequence bundle without retrying equivalent balance allocations', () => { const outcome = plan({ deadlineMs: 1500, horizon: { start: '2024-01-08', end: '2024-02-10' }, declaredDays: [ '2024-01-08', '2024-01-10', '2024-01-12', '2024-01-16', '2024-01-18', '2024-01-22', '2024-01-24', '2024-01-26', '2024-01-30', '2024-02-01', '2024-02-05', '2024-02-07', '2024-02-09', ].map((date) => ({ date, type: 'other' })), balances: ['cp', 'rtt', 'other'].map((type) => ({ id: type, type, quantity: 12, expiryDate: '2024-03-01', })), rules: [ { id: 'support', type: 'optimization', configuration: OPTIMIZATION_PRESETS.supportingPlacements, }, longest, { id: 'typed', type: 'time_window', startDate: '2024-01-09', endDate: '2024-01-09', placeType: 'cp', placeDays: 1, }, ], }); expect(outcome.placements.map(({ date }) => date)).toEqual(['2024-01-09']); expect(outcome.metrics.longestWorkSequence).toBe(1); expect(outcome.metrics.supportingPlacements).toBe(0); }); it.each([0, -1, 1.5, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER + 1])( 'rejects an invalid preference threshold %s at the planning boundary', (thresholdDays) => { const outcome = calculatePlanningInternal({ ...gap, rules: [ { ...deficit, configuration: { ...OPTIMIZATION_PRESETS.shortSequenceDeficit, thresholdDays, }, }, ], }); expect(outcome).toMatchObject({ kind: 'InvalidInput', reasons: [ expect.objectContaining({ code: 'invalid-optimization-configuration', ruleIds: ['deficit'], }), ], }); }, ); it('captures distinct threshold values in explanations and date comparisons', async () => { vi.stubGlobal('window', undefined); try { const five = structuredClone(deficit); const three: OptimizationPlanningRule = { ...deficit, id: 'three', configuration: { ...OPTIMIZATION_PRESETS.shortSequenceDeficit, thresholdDays: 3 }, }; const calculation = createPlanningCalculation({ ...gap, rules: [three, five] }); if (five.configuration.measure !== 'work-sequence-deficit') throw new Error('Expected deficit'); five.configuration.thresholdDays = 1; const outcome = await calculation.outcome; if (outcome.kind !== 'Plan') throw new Error('Expected a plan'); expect(outcome.placements).toHaveLength(3); expect( calculation.snapshot.rules.map((rule) => 'configuration' in rule ? rule.configuration : undefined, ), ).toMatchObject([{ thresholdDays: 3 }, { thresholdDays: 5 }]); const explanation = await calculation.explanation; if (explanation?.kind !== 'PlanExplanation') throw new Error('Expected an explanation'); expect( explanation.placements[0].comparison.before.objectives .filter(({ id }) => id.startsWith('optimization:')) .map(({ value }) => value), ).toEqual([2, 4]); const counterfactual = await calculation.analyzeCounterfactual(outcome.placements[0]); if (counterfactual.kind !== 'Counterfactual') throw new Error('Expected a counterfactual'); expect( counterfactual.baseline.objectives .filter(({ id }) => id.startsWith('optimization:')) .map(({ id, value }) => ({ id, value })), ).toEqual([ { id: 'optimization:three', value: 2 }, { id: 'optimization:deficit', value: 4 }, ]); } finally { vi.unstubAllGlobals(); } }); });