Files
ApiCore/ApiCore-Backend/src/season/varroa-due.spec.ts
T
Nick SzaunigandClaude Opus 5.5 d99a697739 feat: T4 waves 1-2 - rule configs, schema, suggestions core, season, rule forms
- Rule configs with typed parsers and registry; Berlin day helpers
- Schema: T4 enums, Suggestion table, feed vocabulary (feedStatus as
  ColonyCondition with legacy text mapping), split queen source,
  varroa sample size; migration harness for data migrations
- SuggestionsService: propose/refresh/resolve/obsolete/expire,
  accept/dismiss/reopen incl. bulk, lock order, concurrency e2e
- SeasonService: phenology-based segments, inspection/varroa due dates,
  queen age; TimeEventsStep with JobPartialFailure
- Frontend: per-rule settings forms with visible value sources

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-14 13:42:32 +02:00

129 lines
3.7 KiB
TypeScript

import { DEFAULT_SEASON_AUTOPILOT_CONFIG } from '../automation/rule-config/season-autopilot.config';
import { varroaMeasurementDue } from './varroa-due';
const CONFIG = DEFAULT_SEASON_AUTOPILOT_CONFIG;
const SOMMER = { segment: 'SOMMER', springSince: '2026-03-20' } as const;
const WINTERRUHE = {
segment: 'WINTERRUHE',
springSince: '2026-03-20',
} as const;
const NEVER_MEASURED = {
createdOn: '2025-01-01',
lastMeasuredOn: null,
lastMeasurementId: null,
};
describe('varroaMeasurementDue', () => {
it('is due 28 days after the last measurement outside WINTERRUHE', () =>
expect(
varroaMeasurementDue(
{
createdOn: '2025-01-01',
lastMeasuredOn: '2026-06-01',
lastMeasurementId: 'm-1',
},
SOMMER,
CONFIG,
'2026-06-30',
),
).toEqual({ dueOn: '2026-06-29', kind: 'INTERVAL', anchor: 'm-1' }));
it('counts from createdOn without a measurement and anchors on none', () =>
expect(
varroaMeasurementDue(
{ ...NEVER_MEASURED, createdOn: '2026-04-20' },
SOMMER,
CONFIG,
'2026-06-30',
),
).toEqual({ dueOn: '2026-05-18', kind: 'INTERVAL', anchor: 'none' }));
it('uses the configured interval', () =>
expect(
varroaMeasurementDue(
{ ...NEVER_MEASURED, createdOn: '2026-04-20' },
SOMMER,
{ ...CONFIG, varroaMeasureIntervalDays: 14 },
'2026-06-30',
),
).toMatchObject({ dueOn: '2026-05-04' }));
it('is due on 11-15 in the winter window without a measurement since 11-01', () =>
expect(
varroaMeasurementDue(
{
createdOn: '2025-01-01',
lastMeasuredOn: '2026-09-20',
lastMeasurementId: 'm-1',
},
WINTERRUHE,
CONFIG,
'2026-11-20',
),
).toEqual({
dueOn: '2026-11-15',
kind: 'WINTER_WINDOW',
anchor: 'winter-2026',
}));
it('is not due in the winter window after a measurement since 11-01', () =>
expect(
varroaMeasurementDue(
{
createdOn: '2025-01-01',
lastMeasuredOn: '2026-11-03',
lastMeasurementId: 'm-2',
},
WINTERRUHE,
CONFIG,
'2026-11-20',
),
).toBeNull());
it('is not due in WINTERRUHE outside the winter window', () => {
expect(
varroaMeasurementDue(NEVER_MEASURED, WINTERRUHE, CONFIG, '2026-12-21'),
).toBeNull();
expect(
varroaMeasurementDue(NEVER_MEASURED, WINTERRUHE, CONFIG, '2026-02-10'),
).toBeNull();
});
it('opens the winter window on its first and last day', () => {
expect(
varroaMeasurementDue(NEVER_MEASURED, WINTERRUHE, CONFIG, '2026-11-15'),
).toMatchObject({ kind: 'WINTER_WINDOW' });
expect(
varroaMeasurementDue(NEVER_MEASURED, WINTERRUHE, CONFIG, '2026-12-20'),
).toMatchObject({ kind: 'WINTER_WINDOW' });
});
it('moves an interval due day inside the winter to the start of FRUEHJAHR', () =>
expect(
varroaMeasurementDue(
{
createdOn: '2025-01-01',
lastMeasuredOn: '2026-11-20',
lastMeasurementId: 'm-3',
},
{ segment: 'FRUEHJAHR', springSince: '2027-03-20' },
CONFIG,
'2027-03-25',
),
).toEqual({ dueOn: '2027-03-20', kind: 'INTERVAL', anchor: 'm-3' }));
it('keeps a due day after the start of FRUEHJAHR', () =>
expect(
varroaMeasurementDue(
{
createdOn: '2025-01-01',
lastMeasuredOn: '2027-03-10',
lastMeasurementId: 'm-4',
},
{ segment: 'FRUEHJAHR', springSince: '2027-03-20' },
CONFIG,
'2027-04-01',
),
).toMatchObject({ dueOn: '2027-04-07' }));
});