mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-18 18:33:06 +08:00
Preserve supported expression fields instead of stringifying them, and probe individual fields only when whole-expression validation fails. Convert unsupported syntax and weekday ranges ending in 7 while preserving legacy date/weekday rules and valid OR branches. Add execution regressions for issue #3099 and 64 calendar boundary cases in UTC and Asia/Shanghai. Ensure timezone child tests actually run under the Node test runner. Validation: 745 tests passed, 8 skipped; final targeted regressions passed; backend compilation passed. Differential calendar checks matched 142,360 execution timestamps. Fixes #3099
69 lines
2.5 KiB
JavaScript
69 lines
2.5 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { spawnSync } = require('node:child_process');
|
|
const { createCronJob } = require('../../back/shared/cronScheduler');
|
|
const { isValidCronSchedule } = require('../../back/shared/cronSchedule');
|
|
const native = require('node-cron');
|
|
const fixture = require('../fixtures/cron-calendar-boundaries.json');
|
|
|
|
// Each deployment gets a fresh timezone formatter cache in node-cron.
|
|
const timezone = process.env.QL_CRON_BOUNDARY_TEST_TZ;
|
|
if (!timezone) {
|
|
for (const tz of ['UTC', 'Asia/Shanghai']) {
|
|
test(`cron calendar boundary regressions in ${tz}`, () => {
|
|
const env = { ...process.env, TZ: tz, QL_CRON_BOUNDARY_TEST_TZ: tz };
|
|
// A nested --test runner otherwise exits successfully without running tests.
|
|
delete env.NODE_TEST_CONTEXT;
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
['-r', 'ts-node/register/transpile-only', '--test', __filename],
|
|
{
|
|
env,
|
|
encoding: 'utf8',
|
|
},
|
|
);
|
|
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
assert.match(result.stdout, /calendar boundary:/);
|
|
});
|
|
}
|
|
} else {
|
|
for (const { schedule, currentDate, next } of fixture.cases) {
|
|
test(`calendar boundary: ${schedule} from ${currentDate}`, async (t) => {
|
|
t.mock.timers.enable({
|
|
apis: ['Date', 'setTimeout', 'setInterval'],
|
|
now: new Date(currentDate),
|
|
});
|
|
const before = native.getTasks().size;
|
|
const actual = [];
|
|
const job = createCronJob(
|
|
schedule,
|
|
(date) => actual.push(date.toISOString()),
|
|
{ name: 'calendar-boundary', logger: { warn() {}, error() {} } },
|
|
);
|
|
t.after(() => job.cancel());
|
|
|
|
for (const [index, expected] of next[timezone].entries()) {
|
|
t.mock.timers.tick(new Date(expected).getTime() - Date.now() - 1);
|
|
await new Promise(setImmediate);
|
|
assert.deepEqual(actual, next[timezone].slice(0, index));
|
|
t.mock.timers.tick(1);
|
|
await new Promise(setImmediate);
|
|
assert.deepEqual(actual, next[timezone].slice(0, index + 1));
|
|
}
|
|
job.cancel();
|
|
assert.equal(native.getTasks().size, before);
|
|
});
|
|
}
|
|
|
|
test('unreachable dates and unsupported L spellings remain invalid', () => {
|
|
for (const schedule of [
|
|
'0 0 31 2,4 *',
|
|
'0 0 31 2,4 0-7',
|
|
'0 0 1L 2,4 MON',
|
|
'0 0 * * 05L',
|
|
]) {
|
|
assert.equal(isValidCronSchedule(schedule), false, schedule);
|
|
}
|
|
});
|
|
}
|