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
@@ -0,0 +1,93 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { JobPartialFailure } from '../system/job-run.service';
|
||||
import {
|
||||
DAILY_EVALUATION_STEP_ORDER,
|
||||
DailyEvaluationStepName,
|
||||
runDailyEvaluationSteps,
|
||||
} from './daily-evaluation-steps';
|
||||
import type { JobResult } from './job';
|
||||
|
||||
function step(
|
||||
name: DailyEvaluationStepName,
|
||||
run: (today: string) => Promise<JobResult>,
|
||||
): { name: DailyEvaluationStepName; run: jest.Mock } {
|
||||
return { name, run: jest.fn(run) };
|
||||
}
|
||||
|
||||
describe('runDailyEvaluationSteps', () => {
|
||||
const logger = new Logger('test');
|
||||
let errorSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
errorSpy = jest.spyOn(logger, 'error').mockImplementation(() => undefined);
|
||||
});
|
||||
|
||||
afterEach(() => jest.restoreAllMocks());
|
||||
|
||||
it('has the steps of the plan in this order', () =>
|
||||
expect(DAILY_EVALUATION_STEP_ORDER).toEqual([
|
||||
'phenology',
|
||||
'time-events',
|
||||
'autopilot',
|
||||
'suggestions-expiry',
|
||||
'digest',
|
||||
]));
|
||||
|
||||
it('runs the steps in the fixed order with the day and merges their counters', async () => {
|
||||
const order: string[] = [];
|
||||
const digest = step('digest', () => {
|
||||
order.push('digest');
|
||||
return Promise.resolve({ digests: 3 });
|
||||
});
|
||||
const timeEvents = step('time-events', () => {
|
||||
order.push('time-events');
|
||||
return Promise.resolve({ TaskDue: 1 });
|
||||
});
|
||||
|
||||
const { result, failed } = await runDailyEvaluationSteps(
|
||||
[digest, timeEvents],
|
||||
'2026-06-10',
|
||||
logger,
|
||||
);
|
||||
|
||||
expect(order).toEqual(['time-events', 'digest']);
|
||||
expect(timeEvents.run).toHaveBeenCalledWith('2026-06-10');
|
||||
expect(result).toEqual({ TaskDue: 1, digests: 3 });
|
||||
expect(failed).toEqual([]);
|
||||
});
|
||||
|
||||
it('a failing step leaves the others running and the run FAILED with all counters', async () => {
|
||||
const phenology = step('phenology', () =>
|
||||
Promise.resolve({ phenologyApiaries: 2 }),
|
||||
);
|
||||
const timeEvents = step('time-events', () =>
|
||||
Promise.reject(
|
||||
new JobPartialFailure({ WithdrawalEnded: 1, TaskDue: 0 }, ['TaskDue']),
|
||||
),
|
||||
);
|
||||
const autopilot = step('autopilot', () =>
|
||||
Promise.reject(new Error('connection reset')),
|
||||
);
|
||||
const digest = step('digest', () => Promise.resolve({ digests: 4 }));
|
||||
|
||||
const { result, failed, failures } = await runDailyEvaluationSteps(
|
||||
[phenology, timeEvents, autopilot, digest],
|
||||
'2026-06-10',
|
||||
logger,
|
||||
);
|
||||
|
||||
expect(digest.run).toHaveBeenCalled();
|
||||
expect(failed).toEqual(['time-events', 'autopilot']);
|
||||
expect(failures).toEqual(['time-events: TaskDue', 'autopilot']);
|
||||
expect(result).toEqual({
|
||||
phenologyApiaries: 2,
|
||||
WithdrawalEnded: 1,
|
||||
TaskDue: 0,
|
||||
digests: 4,
|
||||
});
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
'Step "autopilot" failed: connection reset',
|
||||
expect.any(String),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user