Files
vac-optimizer/src/app/services/leave-planner-obligation-solutions.spec.ts
T
nessarandClaude Opus 5.5 8168137cd5 perf(planning): plan from the best-ranked obligation solution
With several obligation solutions, the planner ran the whole preference
chain on candidate solutions as previews: every solution after a
complete enumeration, or the eight best-ranked after a truncated window
search. It now orders all solutions by the existing pre-preference
ranking (active objectives, then tie-breaks, of each obligation-only
plan; equal ranks keep enumeration order) and runs the preference
stages once, on the first. If that run ends with earned leave available
only after its final merged time-off period, the stages restart on the
next solution, so a request that planned before never becomes a
conflict.

The loaded year drops from 43 to 5.5 seconds and typed obligations from
3.3 to 0.1 seconds, with byte-identical benchmark plans. Over the 5,000
seeded requests, 23 plans become worse (9 on a real objective) and 1
better; over 50 loaded-year-sized requests, 4 become worse on a real
objective. ADR 0010 records the decision; docs/benchmark/issue-302.md
records the comparison and the opt-in runner that reproduces it.

The frozen-engine check fails until the frozen engine is rebuilt from
this commit in the next one.

Part of #302.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:18:06 +02:00

103 lines
2.9 KiB
TypeScript

import { describe, expect, it } from 'bun:test';
import { calculatePlanningInternal, type PlanningRequest } from './leave-planner.core';
import { createOptimizationPresetRules } from './rule-service';
const presets = createOptimizationPresetRules();
const preset = (id: string) => presets.find((rule) => rule.id === id)!;
describe('choosing among obligation solutions', () => {
it('plans from the solution ranked best before the preferences', () => {
// The obligation day can take either balance. Before the preferences, the
// at-risk RTT ranks first because it leaves no at-risk leave unplaced. Funding
// it with the CP instead would let the RTT gain three days around Easter
// later; ADR 0010 accepts that loss to run the preferences only once.
const request: PlanningRequest & { calculationDate: string } = {
calculationDate: '2026-04-03',
horizon: { start: '2026-04-03', end: '2026-05-24' },
balances: [
{ id: 'cp', type: 'cp', quantity: 1, expiryDate: '2026-06-30' },
{ id: 'rtt', type: 'rtt', quantity: 1, expiryDate: '2026-05-23' },
],
rules: [
preset('default-unplaced-at-risk'),
preset('default-supporting-placements'),
preset('default-non-working-day-gain'),
{
id: 'thursday',
type: 'time_window',
force: 'obligation',
startDate: '2026-04-09',
endDate: '2026-04-09',
placeDays: 1,
placeType: 'any',
},
],
};
const outcome = calculatePlanningInternal(request);
expect(outcome.kind).toBe('Plan');
if (outcome.kind !== 'Plan') return;
expect(outcome.placements).toEqual([
{
date: '2026-04-09',
balanceId: 'rtt',
type: 'rtt',
ruleIds: ['thursday'],
supporting: false,
},
]);
expect(outcome.metrics.unplacedAtRisk).toBe(0);
});
it('plans from the next solution when the best-ranked one earns its leave too late', () => {
const request: PlanningRequest & { calculationDate: string } = {
calculationDate: '2026-07-09',
horizon: { start: '2026-07-09', end: '2026-07-25' },
balances: [
{
id: 'cp',
type: 'cp',
quantity: 3,
availableFrom: '2026-07-13',
expiryDate: '2026-08-10',
},
],
rules: [
{
id: 'tuesday',
type: 'if_then',
force: 'preference',
dayOfWeek: 2,
placeDays: 1,
placeType: 'any',
placement: 'auto',
rewardDays: 2,
rewardType: 'rtt',
},
{
id: 'late-july',
type: 'time_window',
force: 'obligation',
startDate: '2026-07-23',
endDate: '2026-08-02',
placeDays: 2,
placeType: 'any',
},
],
};
const outcome = calculatePlanningInternal(request);
expect(outcome.kind).toBe('Plan');
if (outcome.kind !== 'Plan') return;
expect(
outcome.placements.map(({ date, balanceId, ruleIds }) => [date, balanceId, ruleIds]),
).toEqual([
['2026-07-13', 'cp', ['tuesday']],
['2026-07-23', 'cp', ['late-july']],
['2026-07-24', 'cp', ['late-july']],
]);
});
});