- 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]>
94 lines
2.8 KiB
TypeScript
94 lines
2.8 KiB
TypeScript
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),
|
|
);
|
|
});
|
|
});
|