mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-28 09:02:12 +08:00
* refactor: migrate cron scheduling to node-cron * feat: support annually midnight and minutely cron macros * fix: harden cron recovery and system scheduler compatibility
66 lines
3.4 KiB
JavaScript
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);
|
|
});
|