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>
393 lines
11 KiB
TypeScript
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 }),
|
|
);
|
|
},
|
|
);
|
|
});
|