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>
454 lines
14 KiB
TypeScript
454 lines
14 KiB
TypeScript
import { vi } from '../../testing/vitest-compat';
|
|
import {
|
|
calculatePlanningInternal,
|
|
type OptimizationPlanningRule,
|
|
type PlanOutcome,
|
|
type PlanningRequest,
|
|
} from './leave-planner.core';
|
|
import { OPTIMIZATION_PRESETS } from './planning-objectives';
|
|
import { createPlanningCalculation } from './leave-planner.service';
|
|
|
|
const deficit: OptimizationPlanningRule = {
|
|
id: 'deficit',
|
|
name: 'Internal work-sequence deficit',
|
|
type: 'optimization',
|
|
configuration: OPTIMIZATION_PRESETS.shortSequenceDeficit,
|
|
};
|
|
const longest: OptimizationPlanningRule = {
|
|
id: 'longest',
|
|
name: 'Longest work sequence',
|
|
type: 'optimization',
|
|
configuration: OPTIMIZATION_PRESETS.longestWorkSequence,
|
|
};
|
|
const gap: PlanningRequest = {
|
|
horizon: { start: '2024-01-08', end: '2024-01-13' },
|
|
balances: [{ id: 'cp', type: 'cp', quantity: 3, expiryDate: '2024-02-01' }],
|
|
declaredDays: ['2024-01-08', '2024-01-12'].map((date) => ({ date, type: 'other' })),
|
|
rules: [deficit],
|
|
};
|
|
|
|
function plan(request: PlanningRequest): PlanOutcome {
|
|
const outcome = calculatePlanningInternal(request);
|
|
if (outcome.kind !== 'Plan') throw new Error(`Expected a plan, got ${outcome.kind}`);
|
|
return outcome;
|
|
}
|
|
|
|
describe('composed work-sequence preferences', () => {
|
|
beforeEach(() => vi.setSystemTime(new Date('2024-01-01T12:00:00Z')));
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
it('fills a complete internal sequence despite the worse deficit of partial additions', () => {
|
|
const outcome = plan(gap);
|
|
expect(outcome.placements.map(({ date }) => date)).toEqual([
|
|
'2024-01-09',
|
|
'2024-01-10',
|
|
'2024-01-11',
|
|
]);
|
|
expect(outcome.placements.every(({ supporting }) => supporting)).toBe(true);
|
|
expect(outcome.declaredDays).toEqual(gap.declaredDays);
|
|
expect(outcome.metrics.shortSequenceDeficit).toBe(0);
|
|
expect(outcome.remainingBalances).toEqual([]);
|
|
});
|
|
|
|
it('splits tied longest sequences together when neither single addition improves the maximum', () => {
|
|
const outcome = plan({
|
|
...gap,
|
|
balances: [{ id: 'cp', type: 'cp', quantity: 2, expiryDate: '2024-02-01' }],
|
|
declaredDays: [{ date: '2024-01-10', type: 'other' }],
|
|
rules: [longest],
|
|
});
|
|
expect(outcome.placements.map(({ date }) => date)).toEqual(['2024-01-08', '2024-01-11']);
|
|
expect(outcome.metrics.longestWorkSequence).toBe(1);
|
|
});
|
|
|
|
it('leaves a three-day internal sequence intact at threshold three instead of filling it at five', () => {
|
|
const outcome = plan({
|
|
...gap,
|
|
rules: [
|
|
{
|
|
...deficit,
|
|
configuration: {
|
|
...OPTIMIZATION_PRESETS.shortSequenceDeficit,
|
|
thresholdDays: 3,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
expect(outcome.placements).toEqual([]);
|
|
expect(outcome.remainingBalances[0].quantity).toBe(3);
|
|
expect(outcome.metrics.shortSequenceDeficit).toBe(2);
|
|
});
|
|
|
|
it('counts selected internal sequences even when their measured deficit is zero', () => {
|
|
const outcome = plan({
|
|
...gap,
|
|
rules: [
|
|
{
|
|
...deficit,
|
|
configuration: {
|
|
...OPTIMIZATION_PRESETS.shortSequenceDeficit,
|
|
thresholdDays: 3,
|
|
aggregation: 'count',
|
|
},
|
|
},
|
|
],
|
|
});
|
|
expect(outcome.placements.map(({ date }) => date)).toEqual([
|
|
'2024-01-09',
|
|
'2024-01-10',
|
|
'2024-01-11',
|
|
]);
|
|
});
|
|
|
|
it('evaluates two parameterized configurations separately and follows their order', () => {
|
|
const maximizeThree: OptimizationPlanningRule = {
|
|
...deficit,
|
|
id: 'three',
|
|
configuration: {
|
|
...OPTIMIZATION_PRESETS.shortSequenceDeficit,
|
|
thresholdDays: 3,
|
|
direction: 'maximize',
|
|
},
|
|
};
|
|
const fiveFirst = plan({ ...gap, rules: [deficit, maximizeThree] });
|
|
const threeFirst = plan({ ...gap, rules: [maximizeThree, deficit] });
|
|
expect(fiveFirst.placements.map(({ date }) => date)).toEqual([
|
|
'2024-01-09',
|
|
'2024-01-10',
|
|
'2024-01-11',
|
|
]);
|
|
expect(threeFirst.placements.map(({ date }) => date)).toEqual(['2024-01-10']);
|
|
expect(threeFirst.metrics.shortSequenceDeficit).toBe(8);
|
|
});
|
|
|
|
it('obeys the order between the longest-sequence preference and a personal window', () => {
|
|
const personal = {
|
|
id: 'personal',
|
|
type: 'time_window' as const,
|
|
startDate: '2024-01-08',
|
|
endDate: '2024-01-08',
|
|
placeDays: 1,
|
|
placeType: 'cp',
|
|
};
|
|
const request: PlanningRequest = {
|
|
horizon: { start: '2024-01-08', end: '2024-01-20' },
|
|
balances: [{ id: 'cp', type: 'cp', quantity: 1, expiryDate: '2024-02-01' }],
|
|
rules: [],
|
|
};
|
|
const longestFirst = plan({ ...request, rules: [longest, personal] });
|
|
const personalFirst = plan({ ...request, rules: [personal, longest] });
|
|
expect(longestFirst.placements.map(({ date }) => date)).toEqual(['2024-01-12']);
|
|
expect(longestFirst.metrics.longestWorkSequence).toBe(5);
|
|
expect(personalFirst.placements.map(({ date }) => date)).toEqual(['2024-01-08']);
|
|
expect(personalFirst.metrics.longestWorkSequence).toBe(9);
|
|
});
|
|
|
|
it('keeps obligations and earlier preferences when a complete gap would consume their balance', () => {
|
|
const personal = {
|
|
id: 'personal',
|
|
type: 'time_window' as const,
|
|
startDate: '2024-01-15',
|
|
endDate: '2024-01-15',
|
|
placeDays: 1,
|
|
placeType: 'cp',
|
|
};
|
|
const request = { ...gap, horizon: { start: '2024-01-08', end: '2024-01-16' } };
|
|
expect(
|
|
plan({ ...request, rules: [deficit, personal] }).placements.map(({ date }) => date),
|
|
).toEqual(['2024-01-09', '2024-01-10', '2024-01-11']);
|
|
for (const force of ['preference', 'obligation'] as const) {
|
|
const outcome = plan({ ...request, rules: [{ ...personal, force }, deficit] });
|
|
expect(outcome.placements.map(({ date }) => date)).toEqual(['2024-01-15']);
|
|
expect(outcome.metrics.shortSequenceDeficit).toBe(2);
|
|
}
|
|
});
|
|
|
|
it('uses only eligible balance dates when completing the entire gap', () => {
|
|
const outcome = plan({
|
|
...gap,
|
|
balances: [
|
|
{ id: 'early', type: 'rtt', quantity: 1, expiryDate: '2024-01-09' },
|
|
{
|
|
id: 'late',
|
|
type: 'cp',
|
|
quantity: 2,
|
|
availableFrom: '2024-01-10',
|
|
expiryDate: '2024-02-01',
|
|
},
|
|
],
|
|
});
|
|
expect(outcome.placements.map(({ date, balanceId }) => ({ date, balanceId }))).toEqual([
|
|
{ date: '2024-01-09', balanceId: 'early' },
|
|
{ date: '2024-01-10', balanceId: 'late' },
|
|
{ date: '2024-01-11', balanceId: 'late' },
|
|
]);
|
|
expect(outcome.placements.map(({ supporting }) => supporting)).toEqual([false, true, true]);
|
|
const unavailable = plan({
|
|
...gap,
|
|
balances: [
|
|
{
|
|
id: 'late',
|
|
type: 'cp',
|
|
quantity: 3,
|
|
availableFrom: '2024-01-10',
|
|
expiryDate: '2024-02-01',
|
|
},
|
|
],
|
|
});
|
|
expect(unavailable.placements).toEqual([]);
|
|
});
|
|
|
|
it('adds no neutral placement when the budget cannot split all tied maxima', () => {
|
|
const outcome = plan({
|
|
...gap,
|
|
balances: [{ id: 'cp', type: 'cp', quantity: 1, expiryDate: '2024-02-01' }],
|
|
declaredDays: [{ date: '2024-01-10', type: 'other' }],
|
|
rules: [longest],
|
|
});
|
|
expect(outcome.placements).toEqual([]);
|
|
expect(outcome.metrics.longestWorkSequence).toBe(2);
|
|
});
|
|
|
|
it('splits tied sequences at eligible dates when their centers are unavailable', () => {
|
|
const outcome = plan({
|
|
horizon: { start: '2024-01-08', end: '2024-01-19' },
|
|
declaredDays: [{ date: '2024-01-12', type: 'other' }],
|
|
balances: [
|
|
{ id: 'early', type: 'cp', quantity: 1, expiryDate: '2024-01-08' },
|
|
{
|
|
id: 'late',
|
|
type: 'cp',
|
|
quantity: 1,
|
|
availableFrom: '2024-01-18',
|
|
expiryDate: '2024-02-01',
|
|
},
|
|
],
|
|
rules: [longest],
|
|
});
|
|
expect(outcome.placements.map(({ date }) => date)).toEqual(['2024-01-08', '2024-01-18']);
|
|
expect(outcome.metrics.longestWorkSequence).toBe(3);
|
|
});
|
|
|
|
it('coordinates scarce balances when individually eligible centers cannot be funded together', () => {
|
|
const outcome = plan({
|
|
horizon: { start: '2024-01-08', end: '2024-01-19' },
|
|
declaredDays: [{ date: '2024-01-12', type: 'other' }],
|
|
balances: [
|
|
{ id: 'early', type: 'cp', quantity: 1, expiryDate: '2024-01-08' },
|
|
{ id: 'flexible', type: 'cp', quantity: 1, expiryDate: '2024-02-01' },
|
|
],
|
|
rules: [longest],
|
|
});
|
|
expect(outcome.placements.map(({ date }) => date)).toEqual(['2024-01-08', '2024-01-15']);
|
|
expect(outcome.metrics.longestWorkSequence).toBe(3);
|
|
});
|
|
|
|
it('eliminates tied maximum deficits as one complete gap bundle', () => {
|
|
const outcome = plan({
|
|
horizon: { start: '2024-01-08', end: '2024-01-17' },
|
|
declaredDays: ['2024-01-08', '2024-01-11', '2024-01-16'].map((date) => ({
|
|
date,
|
|
type: 'other',
|
|
})),
|
|
balances: [{ id: 'cp', type: 'cp', quantity: 4, expiryDate: '2024-02-01' }],
|
|
rules: [
|
|
{
|
|
...deficit,
|
|
configuration: {
|
|
...OPTIMIZATION_PRESETS.shortSequenceDeficit,
|
|
aggregation: 'maximum',
|
|
},
|
|
},
|
|
],
|
|
});
|
|
expect(outcome.placements.map(({ date }) => date)).toEqual([
|
|
'2024-01-09',
|
|
'2024-01-10',
|
|
'2024-01-12',
|
|
'2024-01-15',
|
|
]);
|
|
expect(outcome.metrics.shortSequenceDeficit).toBe(0);
|
|
});
|
|
|
|
it('includes both horizon edges in the maximum while excluding them from the deficit', () => {
|
|
const outcome = plan({
|
|
horizon: { start: '2024-01-08', end: '2024-01-25' },
|
|
balances: [],
|
|
declaredDays: ['2024-01-11', '2024-01-15', '2024-01-22'].map((date) => ({
|
|
date,
|
|
type: 'other',
|
|
})),
|
|
rules: [longest, deficit],
|
|
});
|
|
expect(outcome.metrics).toMatchObject({ longestWorkSequence: 4, shortSequenceDeficit: 5 });
|
|
});
|
|
|
|
it.each([
|
|
{ horizon: { start: '2024-05-06', end: '2024-05-15' }, dates: [], longest: 5 },
|
|
{
|
|
horizon: { start: '2024-01-08', end: '2024-01-11' },
|
|
dates: ['2024-01-08', '2024-01-09', '2024-01-10'],
|
|
longest: 0,
|
|
},
|
|
{ horizon: { start: '2024-01-06', end: '2024-01-08' }, dates: [], longest: 0 },
|
|
{
|
|
horizon: { start: '2024-01-08', end: '2024-01-13' },
|
|
dates: ['2024-01-09', '2024-01-10'],
|
|
longest: 2,
|
|
},
|
|
])(
|
|
'keeps holiday/weekend continuity and zero for empty sequences: $horizon.start',
|
|
({ horizon, dates, longest: expected }) => {
|
|
const outcome = plan({
|
|
horizon,
|
|
balances: [],
|
|
declaredDays: dates.map((date) => ({ date, type: 'other' })),
|
|
rules: [longest, deficit],
|
|
});
|
|
expect(outcome.metrics.longestWorkSequence).toBe(expected);
|
|
expect(outcome.metrics.shortSequenceDeficit).toBe(0);
|
|
},
|
|
);
|
|
|
|
it('disabling work-sequence rules removes their influence even with deferred balances', () => {
|
|
const absent = plan({ ...gap, rules: [] });
|
|
const disabled = plan({
|
|
...gap,
|
|
rules: [
|
|
{ ...deficit, enabled: false },
|
|
{ ...longest, enabled: false },
|
|
],
|
|
});
|
|
expect(disabled).toEqual(absent);
|
|
const personal = {
|
|
id: 'personal',
|
|
type: 'time_window' as const,
|
|
startDate: '2024-01-09',
|
|
endDate: '2024-01-09',
|
|
placeDays: 1,
|
|
placeType: 'cp',
|
|
};
|
|
const requested = plan({ ...gap, rules: [{ ...deficit, enabled: false }, personal] });
|
|
expect(requested.placements.map(({ date }) => date)).toEqual(['2024-01-09']);
|
|
expect(requested.metrics.shortSequenceDeficit).toBe(3);
|
|
});
|
|
|
|
it('finishes a rejected tied-sequence bundle without retrying equivalent balance allocations', () => {
|
|
const outcome = plan({
|
|
deadlineMs: 1500,
|
|
horizon: { start: '2024-01-08', end: '2024-02-10' },
|
|
declaredDays: [
|
|
'2024-01-08',
|
|
'2024-01-10',
|
|
'2024-01-12',
|
|
'2024-01-16',
|
|
'2024-01-18',
|
|
'2024-01-22',
|
|
'2024-01-24',
|
|
'2024-01-26',
|
|
'2024-01-30',
|
|
'2024-02-01',
|
|
'2024-02-05',
|
|
'2024-02-07',
|
|
'2024-02-09',
|
|
].map((date) => ({ date, type: 'other' })),
|
|
balances: ['cp', 'rtt', 'other'].map((type) => ({
|
|
id: type,
|
|
type,
|
|
quantity: 12,
|
|
expiryDate: '2024-03-01',
|
|
})),
|
|
rules: [
|
|
{
|
|
id: 'support',
|
|
type: 'optimization',
|
|
configuration: OPTIMIZATION_PRESETS.supportingPlacements,
|
|
},
|
|
longest,
|
|
{
|
|
id: 'typed',
|
|
type: 'time_window',
|
|
startDate: '2024-01-09',
|
|
endDate: '2024-01-09',
|
|
placeType: 'cp',
|
|
placeDays: 1,
|
|
},
|
|
],
|
|
});
|
|
expect(outcome.placements.map(({ date }) => date)).toEqual(['2024-01-09']);
|
|
expect(outcome.metrics.longestWorkSequence).toBe(1);
|
|
expect(outcome.metrics.supportingPlacements).toBe(0);
|
|
});
|
|
|
|
it.each([0, -1, 1.5, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER + 1])(
|
|
'rejects an invalid preference threshold %s at the planning boundary',
|
|
(thresholdDays) => {
|
|
const outcome = calculatePlanningInternal({
|
|
...gap,
|
|
rules: [
|
|
{
|
|
...deficit,
|
|
configuration: {
|
|
...OPTIMIZATION_PRESETS.shortSequenceDeficit,
|
|
thresholdDays,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
expect(outcome).toMatchObject({
|
|
kind: 'InvalidInput',
|
|
reasons: [
|
|
expect.objectContaining({
|
|
code: 'invalid-optimization-configuration',
|
|
ruleIds: ['deficit'],
|
|
}),
|
|
],
|
|
});
|
|
},
|
|
);
|
|
|
|
it('captures distinct threshold values in explanations and date comparisons', async () => {
|
|
vi.stubGlobal('window', undefined);
|
|
try {
|
|
const five = structuredClone(deficit);
|
|
const three: OptimizationPlanningRule = {
|
|
...deficit,
|
|
id: 'three',
|
|
configuration: { ...OPTIMIZATION_PRESETS.shortSequenceDeficit, thresholdDays: 3 },
|
|
};
|
|
const calculation = createPlanningCalculation({ ...gap, rules: [three, five] });
|
|
if (five.configuration.measure !== 'work-sequence-deficit')
|
|
throw new Error('Expected deficit');
|
|
five.configuration.thresholdDays = 1;
|
|
const outcome = await calculation.outcome;
|
|
if (outcome.kind !== 'Plan') throw new Error('Expected a plan');
|
|
expect(outcome.placements).toHaveLength(3);
|
|
expect(
|
|
calculation.snapshot.rules.map((rule) =>
|
|
'configuration' in rule ? rule.configuration : undefined,
|
|
),
|
|
).toMatchObject([{ thresholdDays: 3 }, { thresholdDays: 5 }]);
|
|
const explanation = await calculation.explanation;
|
|
if (explanation?.kind !== 'PlanExplanation') throw new Error('Expected an explanation');
|
|
expect(
|
|
explanation.placements[0].comparison.before.objectives
|
|
.filter(({ id }) => id.startsWith('optimization:'))
|
|
.map(({ value }) => value),
|
|
).toEqual([2, 4]);
|
|
const counterfactual = await calculation.analyzeCounterfactual(outcome.placements[0]);
|
|
if (counterfactual.kind !== 'Counterfactual')
|
|
throw new Error('Expected a counterfactual');
|
|
expect(
|
|
counterfactual.baseline.objectives
|
|
.filter(({ id }) => id.startsWith('optimization:'))
|
|
.map(({ id, value }) => ({ id, value })),
|
|
).toEqual([
|
|
{ id: 'optimization:three', value: 2 },
|
|
{ id: 'optimization:deficit', value: 4 },
|
|
]);
|
|
} finally {
|
|
vi.unstubAllGlobals();
|
|
}
|
|
});
|
|
});
|