refactor(planning): remove the original decision trace
CI / policy (push) Successful in 7s
CI / validate (push) Successful in 1m15s
CI / test (push) Successful in 3m18s
CI / benchmark (push) Failing after 5m53s
CI / build (push) Successful in 1m23s
CI / security (push) Successful in 7m6s
CI / policy (pull_request) Successful in 7s
CI / validate (pull_request) Successful in 1m9s
CI / build (pull_request) Successful in 43s
CI / test (pull_request) Successful in 3m19s
CI / images (push) Successful in 3m7s
CI / required (push) Successful in 2s
CI / benchmark (pull_request) Failing after 5m28s
CI / security (pull_request) Successful in 6m37s
CI / images (pull_request) Successful in 3m18s
CI / required (pull_request) Successful in 1s

This commit is contained in:
nessar committed 2026-10-15 20:42:33 +02:00
1 parent ea4256107f
commit d9d9befa12
43 files changed
+729 -8880

No files matched your search

+159 -594
View File
@@ -1,6 +1,6 @@
import { createOptimizationPresetRules } from './rule-service';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createPlanningCalculation, applyReplayDeltas } from './leave-planner.service';
import { createPlanningCalculation } from './leave-planner.service';
const balance = (
id: string,
@@ -18,8 +18,92 @@ afterEach(() => vi.useRealTimers());
const presets = createOptimizationPresetRules();
describe('planner explanation and trace seam', () => {
it('replays a reported occurrence without borrowing its future balance from the final plan', async () => {
describe('planner explanation and alternative dates', () => {
it('delivers only the displayed plan, explanation and state needed for alternative dates', async () => {
const calculation = createPlanningCalculation({
horizon: { start: '2024-01-02', end: '2024-01-10' },
balances: [balance('cp', 'cp', 1, '2024-01-09')],
rules: [presets[0]],
});
const outcome = await calculation.outcome;
if (outcome.kind !== 'Plan') throw new Error('Expected a plan.');
expect(Object.keys(calculation).sort()).toEqual([
'analyzeCounterfactual',
'cancel',
'explanation',
'explanationError',
'id',
'outcome',
'snapshot',
'timings',
]);
const core = await import('./leave-planner.core');
let artifacts: import('./leave-planner.core').PlanningArtifacts = {};
expect(
core.calculatePlanningInternal(
calculation.snapshot,
undefined,
(value) => (artifacts = value),
),
).toEqual(outcome);
expect(Object.keys(artifacts).sort()).toEqual([
'explanation',
'explanationError',
'finalState',
'timings',
]);
expect(artifacts.finalState?.placements).toEqual(outcome.placements);
expect(Object.keys(artifacts.timings ?? {})).toEqual(['planningWorkMs']);
const original = structuredClone(outcome);
const analysis = await calculation.analyzeCounterfactual(outcome.placements[0]);
expect(analysis.results).toHaveLength(8);
for (const result of analysis.results) {
expect(result).not.toHaveProperty('historicalStatus');
expect(result).not.toHaveProperty('historicalReason');
}
expect(outcome).toEqual(original);
});
it('excludes observers from the technical budget and keeps the outcome after explanation delivery fails', async () => {
const core = await import('./leave-planner.core');
let elapsed = 0;
const clock = vi.spyOn(performance, 'now').mockImplementation(() => elapsed);
let delivered = false;
let artifacts: import('./leave-planner.core').PlanningArtifacts = {};
try {
const outcome = core.calculatePlanningInternal(
{
horizon: { start: '2024-01-02', end: '2024-01-10' },
balances: [],
rules: [],
deadlineMs: 10,
progress: () => (elapsed += 100),
},
() => (elapsed += 500),
(value) => (artifacts = value),
() => (delivered = true),
() => {
expect(delivered).toBe(true);
elapsed += 1000;
throw new Error('Explanation delivery failed.');
},
);
expect(outcome.kind).toBe('Plan');
expect(artifacts.explanationError).toMatchObject({
code: 'explanation-error',
message: 'Explanation delivery failed.',
});
expect(artifacts.finalState?.horizon).toEqual({
start: '2024-01-02',
end: '2024-01-10',
});
expect(artifacts.timings?.planningWorkMs).toBe(0);
} finally {
clock.mockRestore();
}
});
it('keeps earned availability after a reported occurrence is extended', async () => {
vi.setSystemTime(new Date('2024-05-03T12:00:00Z'));
const core = await import('./leave-planner.core');
let artifacts: import('./leave-planner.core').PlanningArtifacts = {};
@@ -67,42 +151,25 @@ describe('planner explanation and trace seam', () => {
undefined,
(value) => (artifacts = value),
);
if (outcome.kind !== 'Plan' || !artifacts.trace || !artifacts.finalState)
throw new Error('Expected the extended generator to finance a traced plan.');
const replay = {
balances: artifacts.trace.initialState.balances.map((item) => ({ ...item })),
placements: new Map(
artifacts.trace.initialState.placements.map((item) => [item.date, item]),
),
earnedActivations: [...artifacts.trace.initialState.earnedActivations],
};
if (outcome.kind !== 'Plan' || !artifacts.finalState)
throw new Error('Expected the extended generator to finance a plan.');
const earnedId = 'earned:producer:2024-05-01';
let recognized = false;
for (const decision of artifacts.trace.phases.flatMap(({ decisions }) => decisions)) {
applyReplayDeltas(replay, decision.deltas, artifacts.finalState);
if (
decision.deltas.some(
({ kind, balanceId }) => kind === 'earned-activated' && balanceId === earnedId,
)
) {
recognized = true;
expect(replay.earnedActivations.map(({ id }) => id)).toContain(earnedId);
expect(replay.balances.map(({ id }) => id)).not.toContain(earnedId);
}
}
expect(recognized).toBe(true);
expect(replay.balances).toEqual(artifacts.finalState.balances);
expect([...replay.placements.values()]).toEqual(artifacts.finalState.placements);
expect(replay.earnedActivations).toEqual(artifacts.finalState.earnedActivations);
expect(replay.balances.find(({ id }) => id === earnedId)).toMatchObject({
remaining: 0,
availableFrom: '2024-05-06',
expiryDate: '2025-05-06',
});
expect(artifacts.finalState.balances).toContainEqual(
expect.objectContaining({
id: earnedId,
availableFrom: '2024-05-06',
expiryDate: '2025-05-06',
remaining: 0,
}),
);
expect(outcome.placements.find(({ balanceId }) => balanceId === earnedId)?.date).toBe(
'2024-05-07',
);
expect(artifacts.finalState.placements).toEqual(outcome.placements);
});
it.each([5, 8])(
'keeps %s unrelated obligation branches linear when a declared entitlement is already reported',
'keeps %s unrelated obligations valid without duplicating a reported entitlement',
async (count) => {
vi.setSystemTime(new Date('2024-05-06T12:00:00Z'));
const core = await import('./leave-planner.core');
@@ -116,74 +183,58 @@ describe('planner explanation and trace seam', () => {
'2024-04-25',
'2024-04-26',
].slice(0, count);
const branchCounts: number[] = [];
for (const rewardDays of [0, 1]) {
let artifacts: import('./leave-planner.core').PlanningArtifacts = {};
const outcome = core.calculatePlanningInternal(
{
horizon: { start: '2024-04-17', end: '2024-05-08' },
declaredDays: [
{ date: '2024-04-29', type: 'cp' },
{ date: '2024-04-30', type: 'cp' },
],
balances: [
{
id: 'other',
type: 'other',
quantity: dates.length,
availableFrom: '2024-04-17',
expiryDate: '2025-12-31',
},
],
rules: [
...dates.map((date) => ({
id: date,
type: 'time_window' as const,
force: 'obligation' as const,
startDate: date,
endDate: date,
placeDays: 1,
placeType: 'other' as const,
})),
{
id: 'producer',
type: 'if_then',
force: 'preference',
dayOfWeek: 3,
placement: 'before',
placeDays: 2,
placeType: 'cp',
rewardDays,
rewardType: 'other',
},
],
},
undefined,
(value) => (artifacts = value),
);
if (outcome.kind !== 'Plan' || !artifacts.trace)
throw new Error('Expected a traced completed plan.');
const outcome = core.calculatePlanningInternal({
horizon: { start: '2024-04-17', end: '2024-05-08' },
declaredDays: [
{ date: '2024-04-29', type: 'cp' },
{ date: '2024-04-30', type: 'cp' },
],
balances: [
{
id: 'other',
type: 'other',
quantity: dates.length,
availableFrom: '2024-04-17',
expiryDate: '2025-12-31',
},
],
rules: [
...dates.map((date) => ({
id: date,
type: 'time_window' as const,
force: 'obligation' as const,
startDate: date,
endDate: date,
placeDays: 1,
placeType: 'other' as const,
})),
{
id: 'producer',
type: 'if_then',
force: 'preference',
dayOfWeek: 3,
placement: 'before',
placeDays: 2,
placeType: 'cp',
rewardDays,
rewardType: 'other',
},
],
});
if (outcome.kind !== 'Plan') throw new Error('Expected a completed plan.');
expect(outcome.placements.map(({ date }) => date)).toEqual(dates);
expect(outcome.remainingBalances).toEqual([]);
branchCounts.push(
artifacts.trace.phases
.find(({ name }) => name === 'obligations')
?.decisions.filter(({ deltas }) =>
deltas.some(({ placement }) => placement !== undefined),
).length ?? 0,
);
if (rewardDays)
expect(outcome.ifThenContributions?.[0].earnedEntitlement).toMatchObject({
availableFrom: '2024-05-02',
added: false,
});
}
expect(branchCounts[0]).toBeGreaterThan(0);
expect(branchCounts[1]).toBe(branchCounts[0]);
},
);
it('replays an already-reported entitlement without inventing a transient earned balance under explicit priorities', async () => {
it('keeps reported entitlements out of the final state under explicit priorities', async () => {
vi.setSystemTime(new Date('2024-05-06T12:00:00Z'));
const core = await import('./leave-planner.core');
let artifacts: import('./leave-planner.core').PlanningArtifacts = {};
@@ -224,69 +275,17 @@ describe('planner explanation and trace seam', () => {
undefined,
(value) => (artifacts = value),
);
if (outcome.kind !== 'Plan' || !artifacts.trace || !artifacts.finalState)
throw new Error('Expected a traced completed plan.');
if (outcome.kind !== 'Plan' || !artifacts.finalState)
throw new Error('Expected a completed plan.');
expect(outcome.placements).toMatchObject([{ date: '2024-05-07', balanceId: 'cp' }]);
expect(outcome.remainingBalances).toEqual([]);
expect(outcome.ifThenContributions?.[0].earnedEntitlement).toMatchObject({
availableFrom: '2024-05-06',
added: false,
});
const decisions = artifacts.trace.phases.flatMap(({ decisions }) => decisions);
const deltas = decisions.flatMap(({ deltas }) => deltas);
expect(deltas).toContainEqual(
expect.objectContaining({ kind: 'balance-changed', balanceId: 'cp' }),
);
expect(deltas).not.toContainEqual(
expect.objectContaining({
kind: 'balance-changed',
balanceId: 'earned:producer:2024-05-01',
}),
);
const initial = artifacts.trace.initialState;
const replay = {
balances: initial.balances.map((balance) => ({ ...balance })),
placements: new Map(
initial.placements.map((placement) => [
placement.date,
{ ...placement, ruleIds: [...placement.ruleIds] },
]),
),
earnedActivations: initial.earnedActivations.map((activation) => ({
...activation,
dates: [...activation.dates],
})),
};
for (const decision of decisions) applyReplayDeltas(replay, decision.deltas);
expect(replay.balances).toEqual(artifacts.finalState.balances);
expect(replay.earnedActivations).toEqual(artifacts.finalState.earnedActivations);
expect([...replay.placements.values()]).toEqual(outcome.placements);
});
it('records only actual evaluations and publishes non-working dates as metadata', async () => {
const calculation = createPlanningCalculation({
horizon: { start: '2024-01-01', end: '2024-01-07' },
balances: [],
rules: [],
});
const outcome = await calculation.outcome;
const trace = await calculation.trace;
expect(outcome.kind).toBe('Plan');
expect(trace?.status).toBe('ready');
expect(trace?.nonWorkingDates?.map(({ date }) => date)).toEqual([
'2024-01-01',
'2024-01-06',
]);
expect(
trace?.phases
.flatMap((phase) => phase.decisions)
.flatMap((decision) => decision.candidates),
).toEqual([]);
expect(trace?.phases.every((phase) => phase.status === 'completed')).toBe(true);
});
it('keeps joint obligation branches atomic and associates the selected shared placement', async () => {
it('associates every joint obligation with its shared placement', async () => {
const calculation = createPlanningCalculation({
horizon: { start: '2024-01-01', end: '2024-01-08' },
balances: [balance('cp', 'cp', 2, '2024-12-31')],
@@ -312,151 +311,13 @@ describe('planner explanation and trace seam', () => {
],
});
const outcome = await calculation.outcome;
const trace = await calculation.trace;
expect(outcome.kind).toBe('Plan');
if (outcome.kind !== 'Plan' || !trace) return;
const obligations = trace.phases.find((phase) => phase.name === 'obligations');
const branch = obligations?.decisions
.flatMap((decision) => decision.candidates)
.find((candidate) => candidate.status === 'chosen' && candidate.dates?.length === 2);
expect(branch?.dates).toHaveLength(2);
expect(
branch?.dates?.every((date) => outcome.placements.some((item) => item.date === date)),
).toBe(true);
if (outcome.kind !== 'Plan') throw new Error('Expected a plan.');
const sharedPlacement = outcome.placements.find(
(placement) =>
placement.ruleIds.includes('joint') && placement.ruleIds.includes('shared'),
);
expect(sharedPlacement).toBeDefined();
const acceptedMutation = obligations?.decisions.find((decision) =>
decision.deltas.some(
(delta) =>
(delta.kind === 'placement-added' || delta.kind === 'placement-updated') &&
delta.placement !== undefined &&
delta.placement.date === sharedPlacement?.date &&
delta.placement.ruleIds.includes('joint') &&
delta.placement.ruleIds.includes('shared'),
),
);
expect(acceptedMutation).toBeDefined();
expect(acceptedMutation?.branch?.deltas).toContainEqual(
expect.objectContaining({
kind: 'placement-updated',
placement: expect.objectContaining({
date: sharedPlacement?.date,
ruleIds: ['joint', 'shared'],
}),
}),
);
const sameDatesOnRejectedParent = obligations?.decisions.filter(
(decision) =>
decision.id !== acceptedMutation?.id &&
decision.candidates.some(
(candidate) =>
candidate.status === 'evaluated-rejected' &&
candidate.dates?.includes(sharedPlacement?.date ?? ''),
),
);
expect(
sameDatesOnRejectedParent?.some((decision) => decision.branch?.parentDecisionId),
).toBe(true);
});
it('records at-risk assignments as sequential decisions with exact prior-state deltas', async () => {
const calculation = createPlanningCalculation({
horizon: { start: '2024-01-01', end: '2024-01-08' },
balances: [balance('cp', 'cp', 2, '2024-01-07')],
rules: [presets[0]],
});
const outcome = await calculation.outcome;
const trace = await calculation.trace;
expect(outcome.kind).toBe('Plan');
if (outcome.kind !== 'Plan' || !trace) return;
const atRisk = trace.phases.find(
(phase) => phase.name === 'optimization:default-unplaced-at-risk',
);
const jan2 = atRisk?.decisions.find((decision) =>
decision.deltas.some(
(delta) => delta.kind === 'placement-added' && delta.date === '2024-01-02',
),
);
const jan3 = atRisk?.decisions.find((decision) =>
decision.deltas.some(
(delta) => delta.kind === 'placement-added' && delta.date === '2024-01-03',
),
);
expect(jan2?.candidates.some((candidate) => candidate.status === 'chosen')).toBe(true);
expect(jan2?.candidates.find((candidate) => candidate.status === 'chosen')).toMatchObject({
technicalComparison: [
{ name: 'obligation-validity', direction: 'minimize', before: 0, after: 0 },
{
name: `optimization:${presets[0].id}`,
direction: 'minimize',
before: 2,
after: 1,
},
],
firstDecisiveComparator: `optimization:${presets[0].id}`,
});
expect(jan2?.deltas).toContainEqual(
expect.objectContaining({
kind: 'balance-changed',
remainingBefore: 2,
remainingAfter: 1,
}),
);
expect(jan3?.candidates.some((candidate) => candidate.status === 'chosen')).toBe(true);
expect(jan3?.candidates.find((candidate) => candidate.status === 'chosen')).toMatchObject({
technicalComparison: [
{ name: 'obligation-validity', direction: 'minimize', before: 0, after: 0 },
{
name: `optimization:${presets[0].id}`,
direction: 'minimize',
before: 1,
after: 0,
},
],
firstDecisiveComparator: `optimization:${presets[0].id}`,
});
expect(jan3?.deltas).toContainEqual(
expect.objectContaining({
kind: 'balance-changed',
remainingBefore: 1,
remainingAfter: 0,
}),
);
for (const [date, decision] of [
['2024-01-02', jan2],
['2024-01-03', jan3],
] as const) {
expect(
decision?.candidates.filter(
(candidate) => candidate.status === 'chosen' && candidate.date === date,
),
).toHaveLength(1);
expect(
decision?.deltas.filter(
(delta) => delta.kind === 'placement-added' && delta.date === date,
),
).toHaveLength(1);
}
const selected = jan3?.deltas.find(
(delta) => delta.kind === 'placement-added' && delta.date === '2024-01-03',
)?.placement;
expect(selected).toBeDefined();
if (!jan3 || !selected) return;
const analysis = await calculation.analyzeCounterfactual({
decisionId: jan3.id,
placement: selected,
});
expect(analysis.kind).toBe('Counterfactual');
if (analysis.kind === 'Counterfactual')
expect(analysis.results.find((item) => item.date === '2024-01-02')).toMatchObject({
status: 'ineligible',
reason: { code: 'occupied' },
});
});
it('puts actual obligation validity first in stable plan explanations', async () => {
@@ -478,6 +339,7 @@ describe('planner explanation and trace seam', () => {
const outcome = await calculation.outcome;
const explanation = await calculation.explanation;
expect(outcome.kind).toBe('Plan');
if (outcome.kind !== 'Plan') throw new Error('Expected a plan.');
expect(explanation?.kind).toBe('PlanExplanation');
if (explanation?.kind !== 'PlanExplanation') return;
@@ -498,7 +360,7 @@ describe('planner explanation and trace seam', () => {
expect(placement.firstDecisiveObjective).toBe('obligation-validity');
});
it('records the balance order used for a shared ANY preference placement', async () => {
it('uses the earlier-expiring balance for shared ANY preferences', async () => {
const calculation = createPlanningCalculation({
horizon: { start: '2024-01-01', end: '2024-01-08' },
balances: [
@@ -528,9 +390,8 @@ describe('planner explanation and trace seam', () => {
],
});
const outcome = await calculation.outcome;
const trace = await calculation.trace;
expect(outcome.kind).toBe('Plan');
if (outcome.kind !== 'Plan' || !trace) return;
if (outcome.kind !== 'Plan') throw new Error('Expected a plan.');
const sharedPlacement = outcome.placements.find((placement) =>
placement.ruleIds.includes('any-preference'),
@@ -540,116 +401,9 @@ describe('planner explanation and trace seam', () => {
balanceId: 'rtt-first',
ruleIds: ['any-preference', 'rtt-preference'],
});
const candidate = trace.phases
.find((phase) => phase.name === 'preference:any-preference')
?.decisions.flatMap((decision) => decision.candidates)
.find((item) => item.status === 'chosen' && item.date === '2024-01-03');
expect(candidate).toMatchObject({
firstDecisiveComparator: 'preference:rtt-preference',
placement: expect.objectContaining({ balanceId: 'rtt-first' }),
});
const compared = trace.phases
.find((phase) => phase.name === 'preference:any-preference')
?.decisions.flatMap((decision) => decision.candidates);
expect(compared).toContainEqual(
expect.objectContaining({
status: 'evaluated-rejected',
placement: expect.objectContaining({ balanceId: 'rtt-second' }),
technicalComparison: expect.arrayContaining([
{
name: 'preference:any-preference',
direction: 'maximize',
before: 1,
after: 1,
},
{
name: 'preference:rtt-preference',
direction: 'maximize',
before: 1,
after: 1,
},
{
name: 'balance-expiry',
direction: 'minimize',
before: '2025-01-05',
after: '2025-01-06',
},
]),
firstDecisiveComparator: 'balance-expiry',
}),
);
});
it('records actual IfThen leaf comparisons and selected branch identity', async () => {
const calculation = createPlanningCalculation({
horizon: { start: '2024-01-01', end: '2025-01-01' },
balances: [balance('cp', 'cp', 2, '2024-12-31')],
rules: [
{
id: 'if-then-preference',
type: 'if_then',
force: 'preference',
dayOfWeek: 3,
placeDays: 1,
placeType: 'cp',
placement: 'auto',
rewardDays: 0,
},
],
});
const outcome = await calculation.outcome;
const trace = await calculation.trace;
expect(outcome.kind).toBe('Plan');
if (outcome.kind !== 'Plan' || !trace) return;
const candidates = trace.phases
.flatMap((phase) => phase.decisions)
.flatMap((decision) =>
decision.candidates
.filter((candidate) =>
candidate.technicalComparison?.some(
(item) => item.name === 'preference:if-then-preference',
),
)
.map((candidate) => ({ candidate, decision })),
);
const chosen = candidates.find(({ candidate }) => candidate.status === 'chosen');
const preferenceLeaves =
trace.phases
.find((phase) => phase.name === 'preference:if-then-preference')
?.decisions.flatMap((decision) => decision.candidates)
.filter((candidate) =>
candidate.technicalComparison?.some(
(item) => item.name === 'preference:if-then-preference',
),
) ?? [];
expect(preferenceLeaves.length).toBeGreaterThan(1);
expect(preferenceLeaves.some((candidate) => candidate.status === 'chosen')).toBe(true);
expect(
preferenceLeaves.some((candidate) => candidate.status === 'evaluated-rejected'),
).toBe(true);
expect(candidates.some(({ candidate }) => candidate.status === 'evaluated-rejected')).toBe(
true,
);
expect(chosen?.candidate.technicalComparison).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'preference:if-then-preference',
direction: 'maximize',
}),
]),
);
expect(
(chosen?.decision.branch?.deltas ?? chosen?.decision.deltas ?? []).length,
).toBeGreaterThan(0);
expect(
chosen?.candidate.dates?.every((date) =>
outcome.placements.some((p) => p.date === date),
),
).toBe(true);
});
it('records earned obligation branches with the activated entitlement and its consumer', async () => {
it('keeps an earned obligation entitlement and its consumer chronological', async () => {
const calculation = createPlanningCalculation({
horizon: { start: '2024-04-25', end: '2024-05-08' },
balances: [
@@ -680,29 +434,13 @@ describe('planner explanation and trace seam', () => {
],
});
const outcome = await calculation.outcome;
const trace = await calculation.trace;
expect(outcome.kind).toBe('Plan');
if (outcome.kind !== 'Plan' || !trace) return;
if (outcome.kind !== 'Plan') throw new Error('Expected a plan.');
const earned = outcome.placements.find((placement) =>
placement.balanceId.startsWith('earned:'),
);
expect(earned?.ruleIds).toContain('consumer');
const decisions = trace.phases.flatMap((phase) => phase.decisions);
const activation = decisions
.flatMap((decision) => decision.deltas)
.find(
(delta) =>
delta.kind === 'earned-activated' && delta.balanceId === earned?.balanceId,
);
expect(activation?.activation).toMatchObject({ ruleId: 'producer', dates: ['2024-05-02'] });
const branch = decisions
.flatMap((decision) => decision.candidates)
.find(
(candidate) =>
candidate.status === 'chosen' && candidate.dates?.includes(earned?.date ?? ''),
);
expect(branch).toBeDefined();
});
it('rejects a producer move that delays its earned entitlement past the consumer', async () => {
@@ -737,7 +475,7 @@ describe('planner explanation and trace seam', () => {
});
const outcome = await calculation.outcome;
expect(outcome.kind).toBe('Plan');
if (outcome.kind !== 'Plan') return;
if (outcome.kind !== 'Plan') throw new Error('Expected a plan.');
const producer = outcome.placements.find((placement) =>
placement.ruleIds.includes('producer'),
);
@@ -802,7 +540,7 @@ describe('planner explanation and trace seam', () => {
});
const outcome = await calculation.outcome;
expect(outcome.kind).toBe('Plan');
if (outcome.kind !== 'Plan') return;
if (outcome.kind !== 'Plan') throw new Error('Expected a plan.');
const atRisk = outcome.placements.find(
(placement) =>
placement.balanceId === 'cp2' &&
@@ -823,7 +561,7 @@ describe('planner explanation and trace seam', () => {
).toBeUndefined();
});
it('records earned balance availability metadata changes when a shared period extends', async () => {
it('keeps earned balance metadata consistent when a shared period extends', async () => {
const calculation = createPlanningCalculation({
horizon: { start: '2024-04-25', end: '2024-05-15' },
balances: [
@@ -863,30 +601,13 @@ describe('planner explanation and trace seam', () => {
],
});
const outcome = await calculation.outcome;
const trace = await calculation.trace;
expect(outcome.kind).toBe('Plan');
if (outcome.kind !== 'Plan' || !trace) return;
if (outcome.kind !== 'Plan') throw new Error('Expected a plan.');
const earnedBalanceId = outcome.placements.find((placement) =>
placement.ruleIds.includes('rtt-preference'),
)?.balanceId;
expect(earnedBalanceId).toBeDefined();
const changes = trace.phases
.flatMap((phase) => phase.decisions)
.flatMap((decision) => decision.deltas)
.filter(
(delta) => delta.kind === 'balance-changed' && delta.balanceId === earnedBalanceId,
);
expect(changes).toContainEqual(
expect.objectContaining({
remainingBefore: 1,
remainingAfter: 1,
balance: expect.objectContaining({
availableFrom: '2024-05-06',
expiryDate: '2025-05-06',
}),
}),
);
});
it('compares every horizon date while preserving the selected placement provenance', async () => {
@@ -907,7 +628,7 @@ describe('planner explanation and trace seam', () => {
});
const outcome = await calculation.outcome;
expect(outcome.kind).toBe('Plan');
if (outcome.kind !== 'Plan') return;
if (outcome.kind !== 'Plan') throw new Error('Expected a plan.');
const selected = outcome.placements.find((placement) =>
placement.ruleIds.includes('exact-date'),
);
@@ -1022,27 +743,6 @@ describe('planner explanation and trace seam', () => {
if (outcome.kind !== 'PlanningConflict') continue;
expect(outcome.reasons[0].code).toBe(conflictCodes[index]);
expect((await calculation.explanation)?.kind).toBe('ConflictExplanation');
const trace = await calculation.trace;
expect(trace?.phases.some((phase) => phase.name === 'obligations')).toBe(true);
expect(
trace?.phases.some(
(phase) => phase.name === 'optimization:default-holiday-week-value',
),
).toBe(false);
if (index === 1)
expect(
trace?.phases
.find((phase) => phase.name === 'obligations')
?.decisions.flatMap((decision) => decision.candidates).length,
).toBeGreaterThan(0);
if (index === 2)
expect(
trace?.phases
.find((phase) => phase.name === 'obligations')
?.decisions.some((decision) =>
decision.candidates.some((candidate) => candidate.dates?.length),
),
).toBe(true);
}
const invalid = createPlanningCalculation({
@@ -1052,7 +752,6 @@ describe('planner explanation and trace seam', () => {
});
expect((await invalid.outcome).kind).toBe('InvalidInput');
expect(await invalid.explanation).toBeUndefined();
expect(await invalid.trace).toBeUndefined();
});
it('does not classify a joint-date conflict as earned when only a preference generates leave', async () => {
@@ -1094,7 +793,7 @@ describe('planner explanation and trace seam', () => {
it('recovers an incumbent explanation from the exact delivered checkpoint state', async () => {
const core = await import('./leave-planner.core');
let checkpointPlan: import('./leave-planner.core').PlanOutcome | undefined;
let checkpointState: import('./leave-planner.core').PlanningTraceState | undefined;
let checkpointState: import('./leave-planner.core').PlanningStateSnapshot | undefined;
const request = {
horizon: { start: '2024-01-01', end: '2024-01-10' },
balances: [balance('cp', 'cp', 2, '2024-01-31')],
@@ -1124,7 +823,7 @@ describe('planner explanation and trace seam', () => {
expect(recovered.explanation.incomplete?.reason.code).toBe('worker-error');
expect(recovered.explanation.placements).toHaveLength(checkpointPlan.placements.length);
let completedState: import('./leave-planner.core').PlanningTraceState | undefined;
let completedState: import('./leave-planner.core').PlanningStateSnapshot | undefined;
const completed = core.calculatePlanningInternal(
{ ...request, progress: undefined },
(plan, state) => {
@@ -1144,116 +843,7 @@ describe('planner explanation and trace seam', () => {
expect(completedRecovery.explanation.incomplete).toBeUndefined();
});
it('does not charge a deliberately slow incremental trace consumer to planning work', async () => {
const core = await import('./leave-planner.core');
let clock = 0;
vi.stubGlobal('performance', { now: () => ++clock });
const request = {
horizon: { start: '2024-01-01', end: '2024-01-15' },
balances: [balance('cp', 'cp', 3, '2024-01-31')],
rules: presets,
deadlineMs: 100_000,
};
const plain = core.calculatePlanningInternal(request);
clock = 0;
let callbackCount = 0;
let artifacts: import('./leave-planner.core').PlanningArtifacts | undefined;
const operations: import('./leave-planner.core').PlanningTraceOperation[] = [];
const traced = core.calculatePlanningInternal(
{
...request,
traceProgress: (
_totalMs: number,
update: import('./leave-planner.core').PlanningTraceUpdate,
) => {
callbackCount++;
clock += 10_000;
operations.push(...update.operations);
},
},
undefined,
(value) => (artifacts = value),
);
expect(traced).toEqual(plain);
expect(callbackCount).toBeGreaterThan(0);
expect(artifacts?.trace?.status).toBe('ready');
expect(artifacts?.timings?.planningWorkMs).toBeLessThan(request.deadlineMs);
expect(artifacts?.timings?.traceTransferMs).toBeGreaterThanOrEqual(callbackCount * 10_000);
expect(artifacts?.timings?.traceTransferMs).toBeGreaterThan(request.deadlineMs);
const decisions = artifacts?.trace?.phases.flatMap((phase) => phase.decisions) ?? [];
const candidateUpdates = operations.filter((operation) => operation.kind === 'candidate');
expect(candidateUpdates.some((operation) => operation.mode === 'replace')).toBe(true);
const streamedCandidates = new Map<string, unknown>();
for (const operation of candidateUpdates)
if (operation.kind === 'candidate')
streamedCandidates.set(
`${operation.phaseId}/${operation.decisionId}/${operation.index}`,
operation.candidate,
);
expect(streamedCandidates.size).toBe(
decisions.flatMap((decision) => decision.candidates).length,
);
expect(operations.filter((operation) => operation.kind === 'delta')).toHaveLength(
decisions.flatMap((decision) => decision.deltas).length,
);
});
it('coalesces pending candidate snapshots without losing later flushed replacements', async () => {
const core = await import('./leave-planner.core');
vi.stubGlobal('performance', { now: () => 0 });
const request = {
horizon: { start: '2024-01-01', end: '2024-01-08' },
balances: [balance('cp', 'cp', 2, '2024-01-07')],
rules: [presets[0]],
};
let plainArtifacts: import('./leave-planner.core').PlanningArtifacts | undefined;
const plain = core.calculatePlanningInternal(request, undefined, (value) => {
plainArtifacts = value;
});
const operations: import('./leave-planner.core').PlanningTraceOperation[] = [];
let streamedArtifacts: import('./leave-planner.core').PlanningArtifacts | undefined;
const streamed = core.calculatePlanningInternal(
{
...request,
traceProgress: (
_totalMs: number,
update: import('./leave-planner.core').PlanningTraceUpdate,
) => operations.push(...update.operations),
},
undefined,
(value) => (streamedArtifacts = value),
);
expect(streamed).toEqual(plain);
expect(streamedArtifacts?.trace).toEqual(plainArtifacts?.trace);
const decisions =
streamedArtifacts?.trace?.phases.flatMap((phase) => phase.decisions) ?? [];
const candidates = decisions.flatMap((decision) =>
decision.candidates.map((candidate, index) => ({
key: `${decision.id}/${index}`,
candidate,
})),
);
expect(candidates.length).toBeGreaterThan(0);
const candidateOperations = operations.filter(
(operation): operation is Extract<typeof operation, { kind: 'candidate' }> =>
operation.kind === 'candidate',
);
expect(candidateOperations).toHaveLength(candidates.length);
const finalStreamedCandidates = new Map(
candidateOperations.map((operation) => [
`${operation.decisionId}/${operation.index}`,
operation,
]),
);
for (const { key, candidate } of candidates) {
expect(finalStreamedCandidates.get(key)?.candidate).toEqual(candidate);
if (candidate.status === 'chosen')
expect(finalStreamedCandidates.get(key)?.mode).toBe('append');
}
});
it('reports matching completed, interrupted, and not-run phases at both interruption boundaries', async () => {
it('explains incomplete calculations before and after a valid plan', async () => {
const early = createPlanningCalculation({
horizon: { start: '2024-01-01', end: '2024-01-10' },
balances: [balance('cp', 'cp', 1, '2024-01-31')],
@@ -1261,22 +851,9 @@ describe('planner explanation and trace seam', () => {
deadlineMs: 0,
});
const earlyOutcome = await early.outcome;
const earlyTrace = await early.trace;
expect(earlyOutcome.kind).toBe('PlanningIncomplete');
if (earlyOutcome.kind === 'PlanningIncomplete')
expect(earlyOutcome.completedStages).toEqual(['validation']);
expect(earlyTrace?.completedStages).toEqual(['validation']);
expect(earlyTrace?.phases.find((phase) => phase.name === 'validation')?.status).toBe(
'completed',
);
expect(earlyTrace?.phases.find((phase) => phase.name === 'obligations')?.status).toBe(
'interrupted',
);
expect(
earlyTrace?.phases.find(
(phase) => phase.name === 'optimization:default-unplaced-at-risk',
)?.status,
).toBe('not-run');
const quality = createPlanningCalculation({
horizon: { start: '2024-01-01', end: '2024-02-01' },
@@ -1289,7 +866,6 @@ describe('planner explanation and trace seam', () => {
},
});
const qualityOutcome = await quality.outcome;
const qualityTrace = await quality.trace;
expect(qualityOutcome.kind).toBe('PlanningIncomplete');
if (qualityOutcome.kind !== 'PlanningIncomplete') return;
expect(qualityOutcome.reasons[0].code).toBe('planner-error');
@@ -1298,16 +874,5 @@ describe('planner explanation and trace seam', () => {
'obligations',
'optimization:default-unplaced-at-risk',
]);
expect(qualityTrace?.completedStages).toEqual(qualityOutcome.completedStages);
expect(
qualityTrace?.phases.find(
(phase) => phase.name === 'optimization:default-holiday-week-value',
)?.status,
).toBe('interrupted');
expect(
qualityTrace?.phases.find(
(phase) => phase.name === 'optimization:default-non-working-day-gain',
)?.status,
).toBe('not-run');
});
});