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>
103 lines
2.9 KiB
TypeScript
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']],
|
|
]);
|
|
});
|
|
});
|