refactor(planning): give planning calculations one state owner
PlanningCalculationService now builds the submitted request, owns the pending and accepted calculation state, and delivers explanations for the accepted calculation only. The duration estimator reads the same request. The calendar and leave-balance summary consume the accepted outcome instead of coordinating writable state on the input service. Refs #275 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b69b3c3690
commit
5cb0b93339
12 files changed
+708
-322
No files matched your search
@@ -5,6 +5,7 @@ import { RuleService, RuleType } from './rule-service';
|
||||
import { UserInputService } from './user-input-service';
|
||||
import { createPlanningCalculation } from './leave-planner.service';
|
||||
import { PlanningDurationService } from './planning-duration.service';
|
||||
import { PlanningCalculationService } from './planning-calculation.service';
|
||||
import { OPTIMIZATION_PRESETS } from './planning-objectives';
|
||||
|
||||
describe('Planning duration estimate', () => {
|
||||
@@ -31,7 +32,8 @@ describe('Planning duration estimate', () => {
|
||||
it('offers a coarse seconds range on the first launch without calculating', () => {
|
||||
const duration = TestBed.inject(PlanningDurationService);
|
||||
expect(duration.estimate()).toEqual({ minimum: 1, maximum: 3, unit: 'seconds' });
|
||||
expect(TestBed.inject(UserInputService).computeTriggerSignal()).toBe(0);
|
||||
expect(TestBed.inject(PlanningCalculationService).isCalculating()).toBe(false);
|
||||
expect(TestBed.inject(PlanningCalculationService).accepted()).toBeNull();
|
||||
expect(TestBed.inject(RuleService).rules).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -47,7 +49,7 @@ describe('Planning duration estimate', () => {
|
||||
});
|
||||
const duration = TestBed.inject(PlanningDurationService);
|
||||
expect(Number.isFinite(duration.estimate()!.maximum)).toBe(true);
|
||||
duration.recordCompletion(duration.request()!, 20_000);
|
||||
duration.recordCompletion(TestBed.inject(PlanningCalculationService).request()!, 20_000);
|
||||
expect(duration.estimate()).toEqual({ minimum: 10, maximum: 60, unit: 'seconds' });
|
||||
});
|
||||
|
||||
@@ -81,19 +83,20 @@ describe('Planning duration estimate', () => {
|
||||
expect(duration.estimate()).toEqual(moreLeave);
|
||||
settings.setPlanningDates('2026-12-31', '2024-01-01');
|
||||
expect(duration.estimate()).toBeNull();
|
||||
expect(input.computeTriggerSignal()).toBe(0);
|
||||
expect(TestBed.inject(PlanningCalculationService).isCalculating()).toBe(false);
|
||||
expect(TestBed.inject(PlanningCalculationService).accepted()).toBeNull();
|
||||
});
|
||||
|
||||
it('refines the submitted configuration using elapsed calendar time and displays minutes for a long wait', () => {
|
||||
const duration = TestBed.inject(PlanningDurationService);
|
||||
const submitted = duration.request()!;
|
||||
const submitted = TestBed.inject(PlanningCalculationService).request()!;
|
||||
duration.recordCompletion(submitted, 120_000);
|
||||
expect(duration.estimate()).toEqual({ minimum: 1, maximum: 6, unit: 'minutes' });
|
||||
});
|
||||
|
||||
it('calibrates against the submitted snapshot even when the form has since changed', () => {
|
||||
const duration = TestBed.inject(PlanningDurationService);
|
||||
const submitted = duration.request()!;
|
||||
const submitted = TestBed.inject(PlanningCalculationService).request()!;
|
||||
TestBed.inject(CalendarSettingsService).setPlanningDates('2024-01-01', '2026-12-31');
|
||||
duration.recordCompletion(submitted, 20_000);
|
||||
expect(duration.estimate()!.maximum).toBeGreaterThan(60);
|
||||
@@ -114,14 +117,16 @@ describe('Planning duration estimate', () => {
|
||||
placeType: SelectableDayType.CP,
|
||||
});
|
||||
const duration = TestBed.inject(PlanningDurationService);
|
||||
const calculation = createPlanningCalculation(duration.request()!);
|
||||
const calculation = createPlanningCalculation(
|
||||
TestBed.inject(PlanningCalculationService).request()!,
|
||||
);
|
||||
duration.recordCompletion(calculation.snapshot, 20_000);
|
||||
expect(duration.estimate()).toEqual({ minimum: 10, maximum: 60, unit: 'seconds' });
|
||||
});
|
||||
|
||||
it('uses recent typical waits rather than an isolated slow calculation, and resets on a fresh visit', () => {
|
||||
const duration = TestBed.inject(PlanningDurationService);
|
||||
const submitted = duration.request()!;
|
||||
const submitted = TestBed.inject(PlanningCalculationService).request()!;
|
||||
for (const elapsed of [20_000, 25_000, 900_000])
|
||||
duration.recordCompletion(submitted, elapsed);
|
||||
expect(duration.estimate()).toEqual({ minimum: 12, maximum: 75, unit: 'seconds' });
|
||||
@@ -136,7 +141,7 @@ describe('Planning duration estimate', () => {
|
||||
it.each([0, -1, NaN, Infinity])('ignores unusable elapsed measurements: %s', (elapsed) => {
|
||||
const duration = TestBed.inject(PlanningDurationService);
|
||||
const initial = duration.estimate();
|
||||
duration.recordCompletion(duration.request()!, elapsed);
|
||||
duration.recordCompletion(TestBed.inject(PlanningCalculationService).request()!, elapsed);
|
||||
expect(duration.estimate()).toEqual(initial);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user