Files
TREK/server/tests/unit/plugins/plugin-jobs.test.ts
jubnl 08dac57577 refactor(server): migrate plugin jobs off node-cron
The plugin host's job scheduler now uses the cron package (the one
@nestjs/schedule wraps), so node-cron and @types/node-cron leave the
dependency tree entirely. Verified against both real libraries that the
accepted cron grammar is identical — no plugin's declared schedule tightens —
and pinned that with an unmocked compat test.
2026-08-10 22:16:22 +02:00

51 lines
2.2 KiB
TypeScript

/**
* The host-side background-job scheduler (#plugins). Proves jobs are opt-in
* (jobs:run), invalid crons never run, the run callback receives the job id, a
* throwing job can't escape the tick, and stop() tears every task down.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
const h = vi.hoisted(() => ({ tasks: [] as Array<{ expr: string; fn: () => void; stopped: boolean; stop(): void }> }));
vi.mock('cron', () => ({
validateCronExpression: (expr: string) => ({ valid: typeof expr === 'string' && expr.trim() !== '' && expr !== 'not-a-cron' }),
CronJob: {
from: ({ cronTime, onTick }: { cronTime: string; onTick: () => void }) => {
const t = { expr: cronTime, fn: onTick, stopped: false, stop() { this.stopped = true; } };
h.tasks.push(t);
return t;
},
},
}));
import { scheduleJobs, stopJobs } from '../../../src/nest/plugins/host/plugin-jobs';
describe('plugin background jobs (scheduler)', () => {
beforeEach(() => { h.tasks.length = 0; });
const jobs = [{ id: 'nightly', schedule: '0 0 * * *' }, { id: 'broken', schedule: 'not-a-cron' }];
it('schedules nothing without the jobs:run grant (opt-in)', () => {
expect(scheduleJobs(new Set<string>(), jobs, () => {})).toEqual([]);
expect(h.tasks).toHaveLength(0);
});
it('schedules only valid-cron jobs when jobs:run is granted', () => {
const tasks = scheduleJobs(new Set(['jobs:run']), jobs, () => {});
expect(tasks).toHaveLength(1);
expect(h.tasks[0].expr).toBe('0 0 * * *'); // the invalid cron was skipped, never scheduled
});
it('fires the run callback with the job id and swallows a throwing job', () => {
const runs: string[] = [];
scheduleJobs(new Set(['jobs:run']), [{ id: 'a', schedule: '0 0 * * *' }], (id) => { runs.push(id); throw new Error('boom'); });
expect(() => h.tasks[0].fn()).not.toThrow(); // the tick wrapper contains the throw
expect(runs).toEqual(['a']);
});
it('stopJobs stops every task and is safe on undefined/empty', () => {
const tasks = scheduleJobs(new Set(['jobs:run']), [{ id: 'a', schedule: '0 0 * * *' }], () => {});
stopJobs(tasks);
expect(h.tasks[0].stopped).toBe(true);
expect(() => stopJobs(undefined)).not.toThrow();
});
});