Files
vac-optimizer/src/app/services/optimization-integration.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

393 lines
11 KiB
TypeScript

import { describe, expect, it } from 'bun:test';
import {
calculatePlanningInternal,
type PlanningArtifacts,
type PlanningRequest,
} from './leave-planner.core';
import { OPTIMIZATION_PRESETS } from './planning-objectives';
import { createOptimizationPresetRules } from './rule-service';
const presets = createOptimizationPresetRules();
const composedRequest: PlanningRequest & { calculationDate: string } = {
calculationDate: '2024-04-01',
horizon: { start: '2024-05-06', end: '2024-05-18' },
balances: [
{
id: 'cp',
type: 'cp',
quantity: 3,
availableFrom: '2024-05-06',
expiryDate: '2024-05-17',
},
],
rules: presets,
};
describe('integrated optimization and earned leave', () => {
it.each(presets)(
'makes disabled $name identical to its absence in the complete configuration',
(disabled) => {
const absent = calculatePlanningInternal({
...composedRequest,
rules: presets.filter(({ id }) => id !== disabled.id),
});
const inactive = calculatePlanningInternal({
...composedRequest,
rules: presets.map((rule) =>
rule.id === disabled.id ? { ...rule, enabled: false } : rule,
),
});
expect(absent.kind).toBe('Plan');
expect(inactive).toEqual(absent);
},
);
it('uses the complete configuration after the sole obligation state', () => {
let artifacts: PlanningArtifacts = {};
const outcome = calculatePlanningInternal(
composedRequest,
undefined,
(value) => (artifacts = value),
);
expect(outcome).toMatchObject({
kind: 'Plan',
placements: [{ date: '2024-05-06' }, { date: '2024-05-07' }, { date: '2024-05-10' }],
metrics: { unplacedAtRisk: 0, holidayWeekValue: 5, nonWorkingDayGain: 6 },
});
if (artifacts.explanation?.kind !== 'PlanExplanation')
throw new Error('Expected an explanation.');
expect(
artifacts.explanation.placements[0].objectiveVector.objectives.map(({ id }) => id),
).toEqual([
'obligation-validity',
'optimization:default-unplaced-at-risk',
'optimization:default-holiday-week-value',
'optimization:default-non-working-day-gain',
'optimization:default-longest-work-sequence',
'optimization:default-short-sequence-deficit',
'optimization:default-supporting-placements',
'balance-expiry',
'placement-date',
]);
});
it.each([
{
name: 'holiday value first',
rules: [presets[1], presets[0], ...presets.slice(2)],
dates: ['2024-05-06', '2024-05-07', '2024-05-10'],
values: [0, 5, 0, 6, 5, 0, 0],
decisive: 'optimization:default-holiday-week-value',
},
{
name: 'non-working-day gain first',
rules: [presets[2], presets[0], presets[1], ...presets.slice(3)],
dates: ['2024-05-03', '2024-05-10', '2024-05-17'],
values: [0, 9, 0, 0, 4, 4, 0],
decisive: 'optimization:default-non-working-day-gain',
},
{
name: 'maximum holiday value after loss prevention',
rules: [
presets[0],
{
...presets[1],
configuration: {
...presets[1].configuration,
aggregation: 'maximum' as const,
},
},
...presets.slice(2),
],
dates: ['2024-05-06', '2024-05-07', '2024-05-10'],
values: [0, 0, 5, 6, 5, 0, 0],
decisive: 'optimization:default-unplaced-at-risk',
},
])('preserves complete vectors and placement explanations with $name', (fixture) => {
let artifacts: PlanningArtifacts = {};
const outcome = calculatePlanningInternal(
{
...composedRequest,
horizon: { start: '2024-04-29', end: '2024-05-18' },
balances: [{ id: 'cp', type: 'cp', quantity: 3, expiryDate: '2024-05-17' }],
rules: fixture.rules,
},
undefined,
(value) => (artifacts = value),
);
if (outcome.kind !== 'Plan' || artifacts.explanation?.kind !== 'PlanExplanation')
throw new Error('Expected a complete plan and explanation.');
expect(outcome.placements.map(({ date }) => date)).toEqual(fixture.dates);
for (const placement of artifacts.explanation.placements) {
expect(placement.objectiveVector.objectives.map(({ value }) => value)).toEqual([
...fixture.values,
'2024-05-17|2024-05-17|2024-05-17',
fixture.dates.join('|'),
]);
expect(placement.firstDecisiveObjective).toBe(fixture.decisive);
expect(placement.ruleIds).toEqual([fixture.decisive.slice('optimization:'.length)]);
}
});
it('preserves the first equivalent funder while evaluating typed window alternatives', () => {
const outcome = calculatePlanningInternal({
...composedRequest,
balances: ['first-cp', 'second-cp', 'rtt'].map((id) => ({
id,
type: id === 'rtt' ? 'rtt' : 'cp',
quantity: 1,
availableFrom: '2024-05-06',
expiryDate: '2024-05-17',
})),
rules: [
presets[0],
{
id: 'typed-window',
type: 'time_window',
startDate: '2024-05-07',
endDate: '2024-05-07',
placeDays: 1,
placeType: 'rtt',
},
...presets.slice(1),
],
});
expect(outcome).toMatchObject({
kind: 'Plan',
placements: [
{ date: '2024-05-06', balanceId: 'second-cp' },
{ date: '2024-05-07', balanceId: 'rtt' },
{ date: '2024-05-10', balanceId: 'first-cp' },
],
preferences: [{ ruleId: 'typed-window', satisfied: 1 }],
metrics: { unplacedAtRisk: 0, holidayWeekValue: 5, nonWorkingDayGain: 6 },
});
});
it.each(['maximum', 'unsafe-total'] as const)(
'keeps distinct funding evaluations when equivalence cannot be proved (%s)',
(control) => {
const outcome = calculatePlanningInternal({
calculationDate: '2024-01-01',
horizon: { start: '2024-01-02', end: '2024-01-03' },
balances: [
{
id: 'first',
type: 'cp',
quantity: control === 'maximum' ? 1 : Number.MAX_SAFE_INTEGER,
expiryDate: '2024-01-02',
},
{
id: 'second',
type: 'rtt',
quantity: control === 'maximum' ? 2 : 1,
expiryDate: '2024-01-02',
},
],
rules: [
presets[0],
...(control === 'maximum'
? [
{
id: 'largest-balance',
type: 'optimization',
configuration: {
...OPTIMIZATION_PRESETS.unplacedAtRisk,
aggregation: 'maximum',
},
},
]
: []),
],
});
expect(outcome).toMatchObject({
kind: 'Plan',
placements: [{ balanceId: control === 'maximum' ? 'second' : 'first' }],
});
},
);
it('finishes a renamed first quantity rule at zero and still evaluates later priorities and final ties', () => {
const loss = { ...presets[0], id: 'personal-quantity', name: 'Personal quantity' };
const run = (rules: PlanningRequest['rules']) => {
const outcome = calculatePlanningInternal({ ...composedRequest, rules });
return { outcome };
};
const window = {
id: 'already-satisfied',
type: 'time_window',
startDate: '2024-05-06',
endDate: '2024-05-06',
placeDays: 1,
placeType: 'cp',
};
const first = run([loss, window, ...presets.slice(1)]);
expect(first.outcome).toMatchObject({
kind: 'Plan',
placements: [{ date: '2024-05-06' }, { date: '2024-05-07' }, { date: '2024-05-10' }],
metrics: { unplacedAtRisk: 0, holidayWeekValue: 5, nonWorkingDayGain: 6 },
});
const reordered = run([presets[1], loss, ...presets.slice(2)]);
expect(reordered.outcome.kind).toBe('Plan');
});
it.each(['sum', 'count', 'maximum'] as const)(
'finishes a configured maximize holiday prefix at its evaluator bound (%s)',
(aggregation) => {
const holiday = {
...presets[1],
id: 'personal-holiday',
configuration: { ...OPTIMIZATION_PRESETS.holidayWeekValue, aggregation },
};
const outcome = calculatePlanningInternal({
...composedRequest,
rules: [presets[0], holiday, ...presets.slice(2)],
});
expect(outcome).toMatchObject({
kind: 'Plan',
placements: [
{ date: '2024-05-06' },
{ date: '2024-05-07' },
{ date: '2024-05-10' },
],
metrics: { unplacedAtRisk: 0, holidayWeekValue: 5, nonWorkingDayGain: 6 },
});
},
);
it('commits a strict holiday bundle before deferring old generic alternatives and preserves higher rules', () => {
const outcome = calculatePlanningInternal({
calculationDate: '2026-11-01',
horizon: { start: '2026-11-02', end: '2026-11-14' },
balances: [{ id: 'cp', type: 'cp', quantity: 3, expiryDate: '2026-11-30' }],
declaredDays: ['2026-11-09', '2026-11-10'].map((date) => ({ date, type: 'other' })),
rules: [
{
id: 'mandatory',
type: 'time_window',
force: 'obligation',
startDate: '2026-11-02',
endDate: '2026-11-02',
placeDays: 1,
placeType: 'cp',
},
presets[0],
{
id: 'earlier-window',
type: 'time_window',
startDate: '2026-11-02',
endDate: '2026-11-02',
placeDays: 1,
placeType: 'cp',
},
...presets.slice(1),
],
});
expect(outcome).toMatchObject({
kind: 'Plan',
placements: [{ date: '2026-11-02' }, { date: '2026-11-12' }, { date: '2026-11-13' }],
preferences: [{ ruleId: 'earlier-window', satisfied: 1 }],
metrics: { unplacedAtRisk: 0, holidayWeekValue: 3 },
});
});
it.each([false, true])(
'earns an already-complete preference occurrence before comparisons (minimize every balance: %s)',
(minimizeEveryBalance) => {
const request: PlanningRequest & { calculationDate: string } = {
calculationDate: '2026-11-01',
horizon: { start: '2026-11-09', end: '2026-11-14' },
balances: [{ id: 'cp', type: 'cp', quantity: 1, expiryDate: '2027-01-01' }],
rules: [
{
id: 'mandatory',
type: 'time_window',
force: 'obligation',
startDate: '2026-11-12',
endDate: '2026-11-12',
placeDays: 1,
placeType: 'cp',
},
...(minimizeEveryBalance
? [
{
id: 'all-balances',
type: 'optimization' as const,
configuration: {
...OPTIMIZATION_PRESETS.unplacedAtRisk,
conditions: [],
},
},
]
: []),
{
id: 'producer',
type: 'if_then',
dayOfWeek: 3,
placeDays: 1,
placeType: 'cp',
placement: 'after',
rewardDays: 1,
rewardType: 'rtt',
},
],
};
let artifacts: PlanningArtifacts = {};
const outcome = calculatePlanningInternal(
request,
undefined,
(value) => (artifacts = value),
);
expect(outcome).toMatchObject({
kind: 'Plan',
placements: [
{ date: '2026-11-12', type: 'cp', ruleIds: ['mandatory', 'producer'] },
],
preferences: [{ ruleId: 'producer', satisfied: 1 }],
remainingBalances: [
{
id: 'earned:producer:2026-11-11',
type: 'rtt',
quantity: 1,
availableFrom: '2026-11-13',
expiryDate: '2027-11-13',
sourceRuleId: 'producer',
},
],
});
expect(artifacts.finalState?.earnedActivations).toEqual([
{
id: 'earned:producer:2026-11-11',
ruleId: 'producer',
occurrence: '2026-11-11',
dates: ['2026-11-12'],
},
]);
if (artifacts.explanation?.kind !== 'PlanExplanation')
throw new Error('Expected an explanation.');
expect(artifacts.explanation.placements[0].contributions).toContainEqual(
expect.objectContaining({ kind: 'rule', ruleId: 'producer' }),
);
expect(artifacts.explanation.ifThenContributions).toContainEqual(
expect.objectContaining({
ruleId: 'producer',
earnedEntitlement: {
id: 'earned:producer:2026-11-11',
type: 'rtt',
quantity: 1,
availableFrom: '2026-11-13',
expiryDate: '2027-11-13',
added: true,
},
}),
);
if (minimizeEveryBalance)
expect(
artifacts.explanation.placements[0].objectiveVector.objectives,
).toContainEqual(
expect.objectContaining({ id: 'optimization:all-balances', value: 1 }),
);
},
);
});