Files
vac-optimizer/src/app/services/optimization-integration.spec.ts
T
nessar cbde23f572
CI / policy (push) Successful in 7s
CI / validate (push) Successful in 1m23s
CI / test (push) Successful in 3m17s
CI / benchmark (push) Failing after 5m52s
CI / build (push) Successful in 1m31s
CI / security (push) Successful in 1m39s
CI / images (push) Skipped
CI / policy (pull_request) Successful in 7s
CI / validate (pull_request) Successful in 1m11s
CI / test (pull_request) Successful in 3m17s
CI / benchmark (pull_request) Failing after 5m57s
CI / build (pull_request) Successful in 1m30s
CI / security (pull_request) Successful in 1m40s
CI / images (pull_request) Skipped
CI / required (push) Failing after 2s
CI / required (pull_request) Failing after 1s
perf(planning): restore the integrated benchmark gate
Reduce objective evaluation and calendar allocation work while preserving planner outcomes and deterministic explanations. Restore required benchmark gating and record the unchanged protocol, differential checks, and browser evidence for #78.
2026-10-16 14:01:32 +02:00

393 lines
11 KiB
TypeScript

import { describe, expect, it } from 'vitest';
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 }),
);
},
);
});