- Schema III: per-user notifications (broadcast fan-out migration, category, link, requiredPermissions filter), notification preferences and settings endpoints, calendar withdrawal switch, occupiedFrames - Suggestions API: list/summary with permission visibility and paging, accept/dismiss/reopen incl. bulk, SuggestionView on all responses - Varroa traffic light by season and method, young colonies, treatment effect window, sample-based percentage; shared withdrawal-window helper on Berlin calendar days; GET /varroa-measurements/thresholds - ColonySplit and QueenAssigned domain events with calendar and webhook handlers - Outbox claims compare against now() rounded to timestamp(3); fixes the flaky e2e claims of rows written with SQL now() Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
272 lines
9.4 KiB
TypeScript
272 lines
9.4 KiB
TypeScript
import type { ColonyType, VarroaMethod } from '../../generated/prisma';
|
|
import { bandsFor, classify, rateMeasurement } from './varroa-bands';
|
|
import { DEFAULT_VARROA_THRESHOLD_CONFIG } from './rule-config/varroa-threshold.config';
|
|
|
|
const DEFAULTS = DEFAULT_VARROA_THRESHOLD_CONFIG;
|
|
const d = (day: string): Date => new Date(`${day}T00:00:00.000Z`);
|
|
|
|
describe('varroa bands', () => {
|
|
it.each<
|
|
[
|
|
string,
|
|
ColonyType,
|
|
VarroaMethod,
|
|
number,
|
|
'green' | 'yellow' | 'red' | null,
|
|
]
|
|
>([
|
|
['2026-08-10', 'PRODUCTION_COLONY', 'NATURAL_MITE_FALL', 10.0, 'yellow'],
|
|
['2026-08-10', 'PRODUCTION_COLONY', 'NATURAL_MITE_FALL', 10.1, 'red'],
|
|
['2026-08-10', 'PRODUCTION_COLONY', 'NATURAL_MITE_FALL', 4.9, 'green'],
|
|
['2026-08-10', 'PRODUCTION_COLONY', 'NATURAL_MITE_FALL', 5, 'yellow'],
|
|
['2026-11-20', 'PRODUCTION_COLONY', 'NATURAL_MITE_FALL', 0.6, 'red'],
|
|
['2026-11-20', 'PRODUCTION_COLONY', 'NATURAL_MITE_FALL', 0.5, 'green'],
|
|
['2026-06-10', 'YOUNG_COLONY', 'NATURAL_MITE_FALL', 5.1, 'red'],
|
|
['2026-06-10', 'YOUNG_COLONY', 'NATURAL_MITE_FALL', 2.5, 'yellow'],
|
|
['2026-11-20', 'NUCLEUS_COLONY', 'NATURAL_MITE_FALL', 0.3, 'red'],
|
|
['2026-11-20', 'NUCLEUS_COLONY', 'NATURAL_MITE_FALL', 0.25, 'green'],
|
|
['2026-07-10', 'PRODUCTION_COLONY', 'POWDERED_SUGAR', 5.1, 'red'],
|
|
['2026-07-10', 'PRODUCTION_COLONY', 'POWDERED_SUGAR', 1, 'yellow'],
|
|
['2026-07-10', 'YOUNG_COLONY', 'POWDERED_SUGAR', 4, 'yellow'],
|
|
['2026-09-10', 'PRODUCTION_COLONY', 'WASHING_METHOD', 1.5, 'green'],
|
|
['2026-09-10', 'PRODUCTION_COLONY', 'WASHING_METHOD', 2, 'yellow'],
|
|
['2026-04-10', 'PRODUCTION_COLONY', 'POWDERED_SUGAR', 9, null],
|
|
['2026-11-10', 'PRODUCTION_COLONY', 'WASHING_METHOD', 9, null],
|
|
['2026-07-10', 'PRODUCTION_COLONY', 'DRONE_BROOD_CONTROL', 50, null],
|
|
])('%s %s %s %p -> %p', (day, type, method, value, light) =>
|
|
expect(
|
|
classify(value, bandsFor(day, type, DEFAULTS, []).bands[method]),
|
|
).toBe(light),
|
|
);
|
|
|
|
it('classifies a missing value or band as no light', () => {
|
|
const band = bandsFor('2026-08-10', null, DEFAULTS, []).bands
|
|
.NATURAL_MITE_FALL;
|
|
expect(classify(null, band)).toBeNull();
|
|
expect(classify(3, undefined)).toBeNull();
|
|
expect(classify(3, null)).toBeNull();
|
|
});
|
|
|
|
it('describes the band with its reason, unit and colony kind', () => {
|
|
const autumn = bandsFor('2026-08-10', 'YOUNG_COLONY', DEFAULTS, []);
|
|
expect(autumn.day).toBe('2026-08-10');
|
|
expect(autumn.bands.NATURAL_MITE_FALL).toEqual({
|
|
method: 'NATURAL_MITE_FALL',
|
|
unit: 'MITES_PER_DAY',
|
|
yellowFrom: 2.5,
|
|
redAbove: 5,
|
|
reason: 'NATURAL_FALL_SEASON',
|
|
youngColony: true,
|
|
});
|
|
expect(autumn.bands.POWDERED_SUGAR).toMatchObject({
|
|
unit: 'PERCENT',
|
|
yellowFrom: 2,
|
|
redAbove: 5,
|
|
reason: 'INFESTATION_AUTUMN',
|
|
});
|
|
const winter = bandsFor('2026-01-10', 'PRODUCTION_COLONY', DEFAULTS, []);
|
|
expect(winter.bands.NATURAL_MITE_FALL).toMatchObject({
|
|
yellowFrom: null,
|
|
redAbove: 0.5,
|
|
reason: 'NATURAL_FALL_WINTER',
|
|
youngColony: false,
|
|
});
|
|
expect(winter.bands.POWDERED_SUGAR).toBeUndefined();
|
|
});
|
|
|
|
it('treats a colony without a type as a full colony', () =>
|
|
expect(
|
|
bandsFor('2026-08-10', null, DEFAULTS, []).bands.NATURAL_MITE_FALL,
|
|
).toMatchObject({ redAbove: 10, youngColony: false }));
|
|
|
|
it('names the recheck day while a treatment effect is in force', () => {
|
|
const treatments = [{ startDay: '2026-07-20', endDay: '2026-08-02' }];
|
|
const until = (day: string) =>
|
|
bandsFor(day, null, DEFAULTS, treatments).postTreatmentUntil;
|
|
expect(until('2026-07-25')).toBe('2026-08-16');
|
|
expect(until('2026-08-15')).toBe('2026-08-16');
|
|
expect(until('2026-08-16')).toBeNull();
|
|
expect(until('2026-07-19')).toBeNull();
|
|
});
|
|
|
|
it('uses the latest recheck day of overlapping treatments', () =>
|
|
expect(
|
|
bandsFor('2026-08-01', null, DEFAULTS, [
|
|
{ startDay: '2026-07-01', endDay: '2026-07-20' },
|
|
{ startDay: '2026-07-25', endDay: '2026-08-10' },
|
|
]).postTreatmentUntil,
|
|
).toBe('2026-08-24'));
|
|
|
|
describe('month edges', () => {
|
|
const lightOn = (
|
|
day: string,
|
|
method: VarroaMethod,
|
|
value: number,
|
|
): string | null =>
|
|
classify(
|
|
value,
|
|
bandsFor(day, 'PRODUCTION_COLONY', DEFAULTS, []).bands[method],
|
|
);
|
|
|
|
it.each<[string, VarroaMethod, number, string | null]>([
|
|
['2026-02-28', 'NATURAL_MITE_FALL', 3, 'red'],
|
|
['2026-03-01', 'NATURAL_MITE_FALL', 3, 'green'],
|
|
['2026-10-31', 'NATURAL_MITE_FALL', 3, 'green'],
|
|
['2026-11-01', 'NATURAL_MITE_FALL', 3, 'red'],
|
|
['2026-04-30', 'POWDERED_SUGAR', 3, null],
|
|
['2026-05-01', 'POWDERED_SUGAR', 3, 'yellow'],
|
|
['2026-07-31', 'POWDERED_SUGAR', 1.5, 'yellow'],
|
|
['2026-08-01', 'POWDERED_SUGAR', 1.5, 'green'],
|
|
['2026-10-31', 'POWDERED_SUGAR', 3, 'yellow'],
|
|
['2026-11-01', 'POWDERED_SUGAR', 3, null],
|
|
['2026-04-30', 'WASHING_METHOD', 3, null],
|
|
['2026-05-01', 'WASHING_METHOD', 3, 'yellow'],
|
|
['2026-07-31', 'WASHING_METHOD', 1.5, 'yellow'],
|
|
['2026-08-01', 'WASHING_METHOD', 1.5, 'green'],
|
|
['2026-10-31', 'WASHING_METHOD', 3, 'yellow'],
|
|
['2026-11-01', 'WASHING_METHOD', 3, null],
|
|
])('%s %s %p -> %p', (day, method, value, light) =>
|
|
expect(lightOn(day, method, value)).toBe(light),
|
|
);
|
|
});
|
|
|
|
it('keeps the young factor off the percent bands', () => {
|
|
const { bands } = bandsFor('2026-07-10', 'YOUNG_COLONY', DEFAULTS, []);
|
|
expect(bands.POWDERED_SUGAR).toMatchObject({
|
|
youngColony: false,
|
|
yellowFrom: 1,
|
|
redAbove: 5,
|
|
});
|
|
expect(bands.NATURAL_MITE_FALL?.youngColony).toBe(true);
|
|
});
|
|
|
|
describe('treatment effect edges', () => {
|
|
const treatments = [{ startDay: '2026-07-20', endDay: '2026-08-02' }];
|
|
const ratingOn = (day: string, method: VarroaMethod) =>
|
|
rateMeasurement(
|
|
{ date: d(day), method, calculatedValue: 12 },
|
|
null,
|
|
DEFAULTS,
|
|
treatments,
|
|
);
|
|
|
|
it('starts on the start day and ends the day before the recheck day', () => {
|
|
expect(ratingOn('2026-07-19', 'NATURAL_MITE_FALL').light).toBe('red');
|
|
expect(ratingOn('2026-07-20', 'NATURAL_MITE_FALL').light).toBe(
|
|
'POST_TREATMENT',
|
|
);
|
|
expect(ratingOn('2026-08-15', 'NATURAL_MITE_FALL').light).toBe(
|
|
'POST_TREATMENT',
|
|
);
|
|
expect(ratingOn('2026-08-16', 'NATURAL_MITE_FALL').light).toBe('red');
|
|
});
|
|
|
|
it('covers a single-day treatment', () => {
|
|
const single = [{ startDay: '2026-08-01', endDay: '2026-08-01' }];
|
|
const until = (day: string) =>
|
|
bandsFor(day, null, DEFAULTS, single).postTreatmentUntil;
|
|
expect(until('2026-08-01')).toBe('2026-08-15');
|
|
expect(until('2026-08-14')).toBe('2026-08-15');
|
|
expect(until('2026-08-15')).toBeNull();
|
|
});
|
|
|
|
it('applies to the percent methods and never to the drone brood check', () => {
|
|
expect(ratingOn('2026-07-25', 'POWDERED_SUGAR').light).toBe(
|
|
'POST_TREATMENT',
|
|
);
|
|
expect(ratingOn('2026-08-05', 'WASHING_METHOD').light).toBe(
|
|
'POST_TREATMENT',
|
|
);
|
|
expect(ratingOn('2026-07-25', 'DRONE_BROOD_CONTROL').light).toBeNull();
|
|
});
|
|
});
|
|
|
|
it('rates by the Berlin calendar day of the measurement', () => {
|
|
// 2026-10-31T23:30Z is 2026-11-01 00:30 in Berlin: winter band.
|
|
const rating = rateMeasurement(
|
|
{
|
|
date: new Date('2026-11-01T00:30:00+01:00'),
|
|
method: 'NATURAL_MITE_FALL',
|
|
calculatedValue: 3,
|
|
},
|
|
null,
|
|
DEFAULTS,
|
|
[],
|
|
);
|
|
expect(rating.reasonCode).toBe('NATURAL_FALL_WINTER');
|
|
expect(rating.light).toBe('red');
|
|
});
|
|
|
|
describe('rateMeasurement', () => {
|
|
it('a measurement 5 days after treatment end has no light and names the recheck day', () =>
|
|
expect(
|
|
rateMeasurement(
|
|
{
|
|
date: d('2026-08-07'),
|
|
method: 'NATURAL_MITE_FALL',
|
|
calculatedValue: 12,
|
|
},
|
|
'PRODUCTION_COLONY',
|
|
DEFAULTS,
|
|
[{ startDay: '2026-07-20', endDay: '2026-08-02' }],
|
|
),
|
|
).toEqual({
|
|
light: 'POST_TREATMENT',
|
|
reasonCode: 'POST_TREATMENT',
|
|
band: null,
|
|
postTreatmentUntil: '2026-08-16',
|
|
}));
|
|
|
|
it('rates outside a treatment effect with the band and reason', () => {
|
|
const rating = rateMeasurement(
|
|
{
|
|
date: d('2026-08-20'),
|
|
method: 'NATURAL_MITE_FALL',
|
|
calculatedValue: 12,
|
|
},
|
|
'PRODUCTION_COLONY',
|
|
DEFAULTS,
|
|
[{ startDay: '2026-07-20', endDay: '2026-08-02' }],
|
|
);
|
|
expect(rating).toMatchObject({
|
|
light: 'red',
|
|
reasonCode: 'NATURAL_FALL_SEASON',
|
|
postTreatmentUntil: null,
|
|
});
|
|
expect(rating.band).toMatchObject({ redAbove: 10 });
|
|
});
|
|
|
|
it('has no light for a method without a band or a missing value', () => {
|
|
expect(
|
|
rateMeasurement(
|
|
{
|
|
date: d('2026-04-10'),
|
|
method: 'POWDERED_SUGAR',
|
|
calculatedValue: 9,
|
|
},
|
|
null,
|
|
DEFAULTS,
|
|
[],
|
|
),
|
|
).toEqual({
|
|
light: null,
|
|
reasonCode: null,
|
|
band: null,
|
|
postTreatmentUntil: null,
|
|
});
|
|
expect(
|
|
rateMeasurement(
|
|
{
|
|
date: d('2026-08-10'),
|
|
method: 'NATURAL_MITE_FALL',
|
|
calculatedValue: null,
|
|
},
|
|
null,
|
|
DEFAULTS,
|
|
[],
|
|
).light,
|
|
).toBeNull();
|
|
});
|
|
});
|
|
});
|