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
@@ -3,7 +3,11 @@ import { JobRun, Prisma } from '../../generated/prisma';
|
||||
import { JOB_NAMES } from '../jobs/job-names';
|
||||
import { NotificationsService } from '../notifications/notifications.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { JOB_ABORTED_ERROR, JobRunService } from './job-run.service';
|
||||
import {
|
||||
JOB_ABORTED_ERROR,
|
||||
JobPartialFailure,
|
||||
JobRunService,
|
||||
} from './job-run.service';
|
||||
|
||||
type RawCall = [readonly string[], ...unknown[]];
|
||||
|
||||
@@ -125,6 +129,26 @@ describe('JobRunService', () => {
|
||||
expect(JSON.stringify(errorSpy.mock.calls)).not.toContain('Mustermann');
|
||||
});
|
||||
|
||||
it('stores the counters of a partial failure with the FAILED run', async () => {
|
||||
const run = await service.run('daily-evaluation', () =>
|
||||
Promise.reject(
|
||||
new JobPartialFailure({ WithdrawalEnded: 2, TaskDue: 1 }, [
|
||||
'time-events',
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
||||
expect(run.status).toBe('FAILED');
|
||||
const [finish] = prisma.$queryRaw.mock.calls;
|
||||
expect(finish.slice(1)).toEqual([
|
||||
'FAILED',
|
||||
'{"WithdrawalEnded":2,"TaskDue":1}',
|
||||
'Daily evaluation failed for: time-events',
|
||||
'run-1',
|
||||
]);
|
||||
expect(notifications.recordForPermission).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// finishedAt of every run - finished, failed or aborted - comes from
|
||||
// the database clock, like the comparisons of cleanup.
|
||||
it.each([
|
||||
|
||||
@@ -20,9 +20,26 @@ const JOB_RUN_RELATED_TYPE = 'JobRun';
|
||||
|
||||
const NEWEST_FIRST = [{ startedAt: 'desc' as const }, { id: 'desc' as const }];
|
||||
|
||||
/**
|
||||
* A run of several steps in which some failed: `result` holds the counters
|
||||
* of all steps, the finished ones included. JobRunService stores it with the
|
||||
* FAILED run, so the System page shows what was done.
|
||||
*/
|
||||
export class JobPartialFailure extends Error {
|
||||
constructor(
|
||||
readonly result: JobResult,
|
||||
readonly failedSteps: readonly string[],
|
||||
/** What the stored error names, e.g. "time-events: QueenAged". */
|
||||
details: readonly string[] = failedSteps,
|
||||
) {
|
||||
super(`Daily evaluation failed for: ${details.join(', ')}`);
|
||||
this.name = 'JobPartialFailure';
|
||||
}
|
||||
}
|
||||
|
||||
type RunOutcome =
|
||||
| { status: 'SUCCEEDED'; result: JobResult }
|
||||
| { status: 'FAILED'; error: string };
|
||||
| { status: 'FAILED'; error: string; result?: JobResult };
|
||||
|
||||
/**
|
||||
* Bookkeeping of the CronJob runs: every run is one JobRun row (RUNNING,
|
||||
@@ -116,6 +133,7 @@ export class JobRunService {
|
||||
const failed = await this.finish(runId, {
|
||||
status: 'FAILED',
|
||||
error: message,
|
||||
...(error instanceof JobPartialFailure ? { result: error.result } : {}),
|
||||
});
|
||||
await this.notifyFailure(job, runId);
|
||||
return failed;
|
||||
@@ -127,7 +145,7 @@ export class JobRunService {
|
||||
*/
|
||||
private async finish(runId: string, outcome: RunOutcome): Promise<JobRun> {
|
||||
const result =
|
||||
outcome.status === 'SUCCEEDED' ? JSON.stringify(outcome.result) : null;
|
||||
outcome.result === undefined ? null : JSON.stringify(outcome.result);
|
||||
const error = outcome.status === 'FAILED' ? outcome.error : null;
|
||||
const [finished] = await this.prisma.$queryRaw<JobRun[]>`
|
||||
UPDATE "JobRun"
|
||||
|
||||
Reference in new issue
Block a user