Files
qinglong/test/back/node-cron-migration.test.cjs
T
whyour f168efcdae refactor: migrate cron scheduling to node-cron (#3079)
* refactor: migrate cron scheduling to node-cron

* feat: support annually midnight and minutely cron macros

* fix: harden cron recovery and system scheduler compatibility
2026-09-27 22:18:39 +08:00

66 lines
3.4 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const load = require('../helpers/load-security-module.cjs');
const { createCronJob } = require('../../back/shared/cronScheduler');
const logger = { info() {}, warn() {}, error() {} };
test('native calendar skips nonexistent spring time and duplicate fall hour', async (t) => {
const previous = process.env.TZ;
process.env.TZ = 'America/New_York';
t.after(() => { if (previous === undefined) delete process.env.TZ; else process.env.TZ = previous; });
t.mock.timers.enable({ apis: ['Date', 'setTimeout', 'setInterval'], now: new Date('2026-03-08T05:00:00Z') });
const springRuns = [];
const spring = createCronJob('0 30 2 * * *', (date) => springRuns.push(date.toISOString()), { name: 'spring', logger });
t.after(() => spring.cancel());
t.mock.timers.tick(new Date('2026-03-09T06:30:00Z') - Date.now());
await new Promise(setImmediate);
assert.deepEqual(springRuns, ['2026-03-09T06:30:00.000Z']);
spring.cancel();
t.mock.timers.setTime(new Date('2026-11-01T05:30:00Z').getTime());
const fallRuns = [];
const fall = createCronJob('0 30 1 * * *', (date) => fallRuns.push(date.toISOString()), { name: 'fall', logger });
t.after(() => fall.cancel());
t.mock.timers.tick(new Date('2026-11-02T06:30:00Z') - Date.now());
await new Promise(setImmediate);
assert.deepEqual(fallRuns, ['2026-11-02T06:30:00.000Z']);
});
test('construction failure cleans staged jobs and preserves the running snapshot', async () => {
const cancelled = [];
const stacks = new Map([['old', [{ cancel: () => cancelled.push('old') }]]]);
let count = 0;
const { addCron } = load('back/schedule/addCron.ts', {
'./data': { scheduleStacks: stacks },
'../shared/cronScheduler': { createCronJob: () => {
if (++count === 2) throw Error('construction failed');
return { start() { throw Error('must not start'); }, cancel: () => cancelled.push('staged') };
} },
'../shared/runCron': {}, '../loaders/logger': logger, '../shared/i18n': { tf: require('node:util').format },
});
await assert.rejects(new Promise((resolve, reject) => addCron({ request: {
replace: true, crons: [{ id: 'new', schedule: '* * * * *', extra_schedules: [{ schedule: '0 * * * *' }] }],
} }, (error) => error ? reject(error) : resolve())), /construction failed/);
assert.deepEqual([...stacks.keys()], ['old']);
assert.deepEqual(cancelled, ['staged']);
});
test('invalid subscription schedules are isolated and immediate runs without a schedule survive', async () => {
const warnings = [], runs = [];
const Service = load('back/services/schedule.ts', {
typedi: { Service: () => (x) => x, Inject: () => () => {} },
'../shared/pLimit': {},
}).default;
const service = new Service({ ...logger, warn: (...args) => warnings.push(args) });
service.runTask = async (...args) => { runs.push(args); };
const item = { id: 123, name: 'subscription', command: 'true', runOrigin: 'subscription' };
await service.createCronTask({ ...item, schedule: 'not a cron' });
assert.equal(warnings.length, 1);
assert.equal(service.scheduleStacks.size, 0);
await service.createCronTask(item, {}, true);
assert.equal(runs.length, 1);
await service.createCronTask({ ...item, schedule: '* * * * *' });
assert.equal(service.scheduleStacks.size, 1);
await service.cancelCronTask(item);
assert.equal(service.scheduleStacks.size, 0);
});