Evaluate explicit, ordered optimization configurations through one shared planner model. Add six editable presets, durable one-time legacy migration, and consistent completed and cancelled plan provenance. Include functional regressions and the agreed trace-removal and benchmark follow-up specs. Performance acceptance remains in #78 under unchanged gates. Refs: #70, #71, #72, #73, #74, #75, #76, #77, #78
187 lines
5.6 KiB
TypeScript
187 lines
5.6 KiB
TypeScript
import { createOptimizationPresetRules } from './rule-service';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { calculatePlanningInternal, type PlanningArtifacts } from './leave-planner.core';
|
|
import {
|
|
evaluateOptimization,
|
|
OPTIMIZATION_PRESETS,
|
|
type OptimizationObservations,
|
|
} from './planning-objectives';
|
|
|
|
const presets = createOptimizationPresetRules();
|
|
|
|
describe('planning objective observations', () => {
|
|
it.each([
|
|
{ start: '2024-05-06', end: '2024-05-13' },
|
|
{ start: '2024-05-10', end: '2024-05-11' },
|
|
])(
|
|
'evaluates the same declared holiday week and supporting placement at horizon boundaries',
|
|
(horizon) => {
|
|
const result = calculatePlanningInternal({
|
|
horizon,
|
|
balances: [
|
|
{
|
|
id: 'cp',
|
|
type: 'cp',
|
|
quantity: 1,
|
|
availableFrom: '2024-05-06',
|
|
expiryDate: '2025-05-06',
|
|
},
|
|
],
|
|
rules: presets,
|
|
declaredDays: ['2024-05-06', '2024-05-07'].map((date) => ({ date, type: 'other' })),
|
|
});
|
|
expect(result.kind).toBe('Plan');
|
|
if (result.kind !== 'Plan') return;
|
|
expect(result.placements).toEqual([
|
|
{
|
|
date: '2024-05-10',
|
|
balanceId: 'cp',
|
|
type: 'cp',
|
|
ruleIds: ['default-holiday-week-value'],
|
|
supporting: true,
|
|
},
|
|
]);
|
|
const observations: OptimizationObservations = {
|
|
leaveBalances: [{ unplacedQuantity: 0, atRisk: false }],
|
|
publicHolidayWeeks: [{ value: 5, complete: true }],
|
|
timeOffPeriods: result.timeOffPeriods,
|
|
workSequences: [
|
|
{ length: 0, internal: false },
|
|
{ length: 0, internal: false },
|
|
],
|
|
leavePlacements: result.placements,
|
|
};
|
|
expect(
|
|
Object.values(OPTIMIZATION_PRESETS).map((configuration) =>
|
|
evaluateOptimization(configuration, observations),
|
|
),
|
|
).toEqual([0, 5, 6, 0, 0, 1]);
|
|
expect(result.metrics).toMatchObject({
|
|
unplacedAtRisk: 0,
|
|
holidayWeekValue: 5,
|
|
nonWorkingDayGain: 6,
|
|
longestWorkSequence: 0,
|
|
shortSequenceDeficit: 0,
|
|
supportingPlacements: 1,
|
|
});
|
|
},
|
|
);
|
|
|
|
it('uses internal working-day deficits while retaining both horizon edges in the maximum', () => {
|
|
const result = calculatePlanningInternal({
|
|
horizon: { start: '2024-01-02', end: '2024-01-18' },
|
|
balances: [],
|
|
rules: [],
|
|
declaredDays: ['2024-01-05', '2024-01-09'].map((date) => ({ date, type: 'other' })),
|
|
});
|
|
expect(result.kind).toBe('Plan');
|
|
if (result.kind !== 'Plan') return;
|
|
expect(result.metrics).toMatchObject({
|
|
longestWorkSequence: 6,
|
|
shortSequenceDeficit: 4,
|
|
nonWorkingDayGain: 2,
|
|
});
|
|
const observations: OptimizationObservations = {
|
|
leaveBalances: [],
|
|
publicHolidayWeeks: [],
|
|
timeOffPeriods: result.timeOffPeriods,
|
|
workSequences: [
|
|
{ length: 3, internal: false },
|
|
{ length: 1, internal: true },
|
|
{ length: 6, internal: false },
|
|
],
|
|
leavePlacements: result.placements,
|
|
};
|
|
expect(evaluateOptimization(OPTIMIZATION_PRESETS.shortSequenceDeficit, observations)).toBe(
|
|
4,
|
|
);
|
|
expect(
|
|
evaluateOptimization(
|
|
{ ...OPTIMIZATION_PRESETS.shortSequenceDeficit, thresholdDays: 3 },
|
|
observations,
|
|
),
|
|
).toBe(2);
|
|
});
|
|
|
|
it.each([
|
|
['2024-01-01', '2024-01-06', ['2024-01-02', '2024-01-03', '2024-01-04', '2024-01-05'], 1],
|
|
['2018-04-30', '2018-05-05', ['2018-04-30', '2018-05-02', '2018-05-03', '2018-05-04'], 2],
|
|
['2024-04-29', '2024-05-04', ['2024-04-29', '2024-04-30', '2024-05-02', '2024-05-03'], 3],
|
|
['2024-08-12', '2024-08-17', ['2024-08-12', '2024-08-13', '2024-08-14', '2024-08-16'], 2],
|
|
['2024-10-28', '2024-11-02', ['2024-10-28', '2024-10-29', '2024-10-30', '2024-10-31'], 1],
|
|
] as const)(
|
|
'retains the weekday holiday weight in the completed week starting %s',
|
|
(start, end, dates, value) => {
|
|
const result = calculatePlanningInternal({
|
|
horizon: { start, end },
|
|
balances: [],
|
|
rules: [],
|
|
declaredDays: dates.map((date) => ({ date, type: 'other' })),
|
|
});
|
|
expect(result.kind).toBe('Plan');
|
|
if (result.kind === 'Plan') expect(result.metrics.holidayWeekValue).toBe(value);
|
|
},
|
|
);
|
|
|
|
it('keeps the historical objective order in the explanation of a jointly valid plan', () => {
|
|
let artifacts: PlanningArtifacts | undefined;
|
|
const result = calculatePlanningInternal(
|
|
{
|
|
horizon: { start: '2024-01-02', end: '2024-01-18' },
|
|
balances: [
|
|
{
|
|
id: 'cp',
|
|
type: 'cp',
|
|
quantity: 1,
|
|
availableFrom: '2024-01-02',
|
|
expiryDate: '2024-01-17',
|
|
},
|
|
],
|
|
rules: [
|
|
presets[0],
|
|
...[
|
|
{
|
|
id: 'fixed',
|
|
type: 'time_window',
|
|
force: 'obligation',
|
|
startDate: '2024-01-05',
|
|
endDate: '2024-01-05',
|
|
placeDays: 1,
|
|
placeType: 'cp',
|
|
},
|
|
{
|
|
id: 'personal',
|
|
type: 'time_window',
|
|
force: 'preference',
|
|
startDate: '2024-01-09',
|
|
endDate: '2024-01-09',
|
|
placeDays: 1,
|
|
placeType: 'cp',
|
|
},
|
|
],
|
|
...presets.slice(1),
|
|
],
|
|
},
|
|
undefined,
|
|
(value) => (artifacts = value),
|
|
);
|
|
expect(result.kind).toBe('Plan');
|
|
expect(artifacts?.explanation?.kind).toBe('PlanExplanation');
|
|
if (artifacts?.explanation?.kind !== 'PlanExplanation') return;
|
|
const placement = artifacts.explanation.placements[0];
|
|
expect(placement.objectiveVector.objectives.map((objective) => objective.id)).toEqual([
|
|
'obligation-validity',
|
|
'optimization:default-unplaced-at-risk',
|
|
'preference:personal',
|
|
'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',
|
|
]);
|
|
expect(placement.firstDecisiveObjective).toBe('obligation-validity');
|
|
});
|
|
});
|