Files
ApiCore/ApiCore-Backend/src/jobs/daily-evaluation-steps.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

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),
);
});
});