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]>
This commit is contained in:
1 parent
7097fcd97c
commit
d99a697739
116 files changed
+11239
-801
No files matched your search
@@ -1,13 +1,11 @@
|
||||
import { INestApplicationContext } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { Prisma } from '../generated/prisma';
|
||||
import { DailyEvaluationJob } from '../src/jobs/daily-evaluation.job';
|
||||
import { JobsModule } from '../src/jobs/jobs.module';
|
||||
import { TimeEventsStep } from '../src/jobs/steps/time-events.step';
|
||||
import { PrismaService } from '../src/prisma/prisma.service';
|
||||
|
||||
const d = (day: string): Date => new Date(`${day}T00:00:00.000Z`);
|
||||
/** When the CronJob runs on `day` (04:30 Europe/Berlin). */
|
||||
const at = (day: string): Date => new Date(`${day}T02:30:00.000Z`);
|
||||
|
||||
const NOTHING_NEW = {
|
||||
WithdrawalEnded: 0,
|
||||
@@ -21,14 +19,14 @@ const NOTHING_NEW = {
|
||||
* The job evaluates the whole database. The e2e files share it and run in
|
||||
* parallel, so the evaluation runs on days in 2001: no other file has
|
||||
* records there, and the events of this file stay its own. Called through
|
||||
* evaluate(day) instead of runCli, which would also write JobRun rows that
|
||||
* test/jobs.e2e-spec.ts counts.
|
||||
* TimeEventsStep.run(day) instead of runCli, which would also write JobRun
|
||||
* rows that test/jobs.e2e-spec.ts counts.
|
||||
*/
|
||||
describe('daily-evaluation (e2e)', () => {
|
||||
const suffix = `${Date.now()}-${Math.round(Math.random() * 1e6)}`;
|
||||
let app: INestApplicationContext;
|
||||
let prisma: PrismaService;
|
||||
let job: DailyEvaluationJob;
|
||||
let step: TimeEventsStep;
|
||||
const ids: Record<string, string> = {};
|
||||
|
||||
const eventsOf = (aggregateId: string) =>
|
||||
@@ -50,7 +48,7 @@ describe('daily-evaluation (e2e)', () => {
|
||||
}).compile();
|
||||
app = await moduleRef.init();
|
||||
prisma = app.get(PrismaService);
|
||||
job = app.get(DailyEvaluationJob);
|
||||
step = app.get(TimeEventsStep);
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
@@ -99,7 +97,8 @@ describe('daily-evaluation (e2e)', () => {
|
||||
},
|
||||
});
|
||||
ids.dead = dead.id;
|
||||
// Inspected 2001-04-28: the next inspection is overdue on 2001-05-19.
|
||||
// Inspected 2001-04-28, in the swarm season (every 7 days): the next
|
||||
// inspection is due 2001-05-05 and overdue on 2001-05-06.
|
||||
for (const colonyId of [living.id, dead.id]) {
|
||||
await prisma.inspection.create({
|
||||
data: { colonyId, userId: user.id, date: d('2001-04-28') },
|
||||
@@ -180,10 +179,10 @@ describe('daily-evaluation (e2e)', () => {
|
||||
});
|
||||
|
||||
it('running the job twice yields exactly one WithdrawalEnded per treatment and day', async () => {
|
||||
const first = await job.evaluate(at('2001-05-20'));
|
||||
const second = await job.evaluate(at('2001-05-20'));
|
||||
const first = await step.run('2001-05-20');
|
||||
const second = await step.run('2001-05-20');
|
||||
// The next day the occurrence is still in the window - and still known.
|
||||
const nextDay = await job.evaluate(at('2001-05-21'));
|
||||
const nextDay = await step.run('2001-05-21');
|
||||
|
||||
expect(first.WithdrawalEnded).toBeGreaterThanOrEqual(1);
|
||||
expect(second).toEqual(NOTHING_NEW);
|
||||
@@ -201,8 +200,8 @@ describe('daily-evaluation (e2e)', () => {
|
||||
expect(await eventsOf(ids['treatment:no-withdrawal'])).toEqual([]);
|
||||
});
|
||||
|
||||
it('records due tasks and overdue checks only for open tasks and living colonies', async () => {
|
||||
await job.evaluate(at('2001-05-20'));
|
||||
it('records due tasks and varroa checks only for open tasks and living colonies', async () => {
|
||||
await step.run('2001-05-20');
|
||||
|
||||
expect(await eventsOf(ids['task:open'])).toEqual([
|
||||
{
|
||||
@@ -215,17 +214,15 @@ describe('daily-evaluation (e2e)', () => {
|
||||
]);
|
||||
expect(await eventsOf(ids['task:done'])).toEqual([]);
|
||||
expect(await eventsOf(ids.living)).toEqual([
|
||||
{
|
||||
type: 'InspectionOverdue',
|
||||
aggregateType: 'Colony',
|
||||
payload: { dueOn: '2001-05-19', lastInspectedOn: '2001-04-28' },
|
||||
dedupeKey: `InspectionOverdue:${ids.living}:2001-05-19`,
|
||||
actorUserId: null,
|
||||
},
|
||||
{
|
||||
type: 'VarroaMeasurementDue',
|
||||
aggregateType: 'Colony',
|
||||
payload: { dueOn: '2001-05-18', lastMeasuredOn: null },
|
||||
payload: {
|
||||
dueOn: '2001-05-18',
|
||||
lastMeasuredOn: null,
|
||||
kind: 'INTERVAL',
|
||||
anchor: 'none',
|
||||
},
|
||||
dedupeKey: `VarroaMeasurementDue:${ids.living}:2001-05-18`,
|
||||
actorUserId: null,
|
||||
},
|
||||
@@ -233,11 +230,70 @@ describe('daily-evaluation (e2e)', () => {
|
||||
expect(await eventsOf(ids.dead)).toEqual([]);
|
||||
});
|
||||
|
||||
it('reports active queens from their second year in the first days of January', async () => {
|
||||
await job.evaluate(at('2001-01-05'));
|
||||
expect(await job.evaluate(at('2001-01-08'))).toEqual(NOTHING_NEW);
|
||||
it('InspectionOverdue follows the segment interval', async () => {
|
||||
await step.run('2001-05-12');
|
||||
|
||||
expect(await eventsOf(ids['queen:aged'])).toEqual([
|
||||
// SCHWARMZEIT since 2001-04-15: 2001-04-28 + 7 = 2001-05-05, not + 21.
|
||||
expect(
|
||||
(await eventsOf(ids.living)).filter(
|
||||
(event) => event.type === 'InspectionOverdue',
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
type: 'InspectionOverdue',
|
||||
aggregateType: 'Colony',
|
||||
payload: {
|
||||
dueOn: '2001-05-05',
|
||||
lastInspectedOn: '2001-04-28',
|
||||
firstOfYear: false,
|
||||
},
|
||||
dedupeKey: `InspectionOverdue:${ids.living}:2001-05-06`,
|
||||
actorUserId: null,
|
||||
},
|
||||
]);
|
||||
expect(await eventsOf(ids.dead)).toEqual([]);
|
||||
});
|
||||
|
||||
it('records no inspection in WINTERRUHE and the first one the day after FRUEHJAHR begins', async () => {
|
||||
await step.run('2002-03-10');
|
||||
expect(
|
||||
(await eventsOf(ids.living)).filter((event) =>
|
||||
event.dedupeKey?.endsWith(':2002-03-21'),
|
||||
),
|
||||
).toEqual([]);
|
||||
|
||||
// FRUEHJAHR begins 2002-03-20; the colony was last inspected in 2001.
|
||||
await step.run('2002-03-21');
|
||||
expect(
|
||||
(await eventsOf(ids.living)).filter((event) =>
|
||||
event.dedupeKey?.endsWith(':2002-03-21'),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
type: 'InspectionOverdue',
|
||||
aggregateType: 'Colony',
|
||||
payload: {
|
||||
dueOn: '2002-03-20',
|
||||
lastInspectedOn: '2001-04-28',
|
||||
firstOfYear: true,
|
||||
},
|
||||
dedupeKey: `InspectionOverdue:${ids.living}:2002-03-21`,
|
||||
actorUserId: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('reports the current queen of a living colony once she is aged, on any job day of the year', async () => {
|
||||
await step.run('2001-09-09');
|
||||
await step.run('2001-09-10');
|
||||
|
||||
// One per queen and year: the 2002 run of the test above added its own.
|
||||
const events = await eventsOf(ids['queen:aged']);
|
||||
expect(events.map((event) => event.dedupeKey)).toEqual([
|
||||
`QueenAged:${ids['queen:aged']}:2001-01-01`,
|
||||
`QueenAged:${ids['queen:aged']}:2002-01-01`,
|
||||
]);
|
||||
expect(events.slice(0, 1)).toEqual([
|
||||
{
|
||||
type: 'QueenAged',
|
||||
aggregateType: 'Queen',
|
||||
|
||||
Reference in new issue
Block a user