Files
vac-optimizer/src/app/services/leave-planner-work-sequences.spec.ts
T
nessarandClaude Opus 5.5 94d57add2c test: run the suite and benchmark with Bun's native test runner
Replace Vitest with bun test across the whole suite. A preload compiles
components through Angular's JIT transform with inlined templates, sets
up jsdom, TestBed and zoneless change detection, and a small shim covers
the Vitest APIs bun:test lacks. Files run isolated, real-worker specs
spawn the running Bun, and line coverage from lcov is converted to the
existing HTML and Cobertura reports. The CI benchmark replaces its heap
cap with a container memory limit. Vitest, Jest, Nx and their Vite
configuration are removed.

Refs #291

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:40:42 +02:00

454 lines
14 KiB
TypeScript

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();
}
});
});