mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-17 07:20:51 +08:00
Translate UI, scheduler, validation, preload and maintenance messages dynamically, and propagate locale changes between workers. Harden update checks, downloads and terminal states, verify Debian pnpm, and ignore generated test artifacts.
99 lines
5.7 KiB
JavaScript
99 lines
5.7 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const load = require('../helpers/load-security-module.cjs');
|
|
const tick = () => new Promise(setImmediate);
|
|
function gate() { let release; const promise = new Promise(r => release = r); return { promise, release }; }
|
|
|
|
async function fixture() {
|
|
const logger = { info() {}, error() {}, warn() {} }, notices = [];
|
|
const limit = load('back/shared/pLimit.ts', {
|
|
'../data/system': { AuthDataType: { systemConfig: 'systemConfig' }, SystemModel: { sync: async () => {}, findOne: async () => ({ info: { cronConcurrency: 1 } }) } },
|
|
'../loaders/logger': logger,
|
|
'../services/notify': class {},
|
|
'../shared/i18n': { t: s => s, tf: s => s },
|
|
'../config': { grpcPort: 1 },
|
|
'../protos/api': { ApiClient: class { systemNotify(value, cb) { notices.push(value); cb(); } } },
|
|
'../config/grpcCerts': { getGrpcCerts: () => null },
|
|
}).default;
|
|
await tick();
|
|
const spawned = [], records = new Map(), jobs = new Map();
|
|
let readGate, killGate;
|
|
const { runCron } = load('back/shared/runCron.ts', {
|
|
'cross-spawn': { spawn: command => { spawned.push(command); return {}; } },
|
|
'./pLimit': limit,
|
|
'../loaders/logger': logger,
|
|
'../data/cron': { CrontabModel: { findOne: async ({ where }) => { const row = records.get(String(where.id)); if (readGate) await readGate.promise; return row; }, update: async () => {} }, CrontabStatus: { running: 0, queued: 3, idle: 1 } },
|
|
'../data/runningInstance': { RunningInstanceModel: { update: async () => {} }, InstanceStatus: { running: 0, stopped: 2 } },
|
|
'../config/util': { killTask: async () => { if (killGate) await killGate.promise; } },
|
|
'./childProcess': { observeChildProcess: () => ({ completed: Promise.resolve({ code: 0 }) }), asError: e => e },
|
|
});
|
|
const { addCron } = load('back/schedule/addCron.ts', {
|
|
'../shared/cronScheduler': { createCronJob: (schedule, callback) => ({ start() {}, cancel() {}, fire: callback }) },
|
|
'../shared/cronSchedule': { isValidCronSchedule: x => x !== 'invalid' },
|
|
'./data': { scheduleStacks: jobs }, '../shared/runCron': { runCron }, '../loaders/logger': logger,
|
|
'../shared/i18n': { t: (s) => s, tf: s => s },
|
|
});
|
|
const { delCron } = load('back/schedule/delCron.ts', { './data': { scheduleStacks: jobs }, '../loaders/logger': logger });
|
|
function add(id = '1', command = 'old', replace = false) {
|
|
records.set(id, { isDisabled: 0, allow_multiple_instances: 1 });
|
|
addCron({ request: { crons: [{ id, command, schedule: '* * * * * *' }], replace } }, err => assert.ifError(err));
|
|
return jobs.get(id)[0];
|
|
}
|
|
function remove(id = '1') { delCron({ request: { ids: [id] } }, err => assert.ifError(err)); }
|
|
async function block() { const g = gate(); const done = limit.runWithCronLimit({ id: 'blocker' }, async () => { await g.promise; }); await tick(); return { release: async () => { g.release(); await done; } }; }
|
|
return { limit, spawned, records, jobs, add, remove, block, notices, setReadGate: g => readGate = g, setKillGate: g => killGate = g };
|
|
}
|
|
|
|
for (const operation of ['disable', 'delete', 'update', 'replace', 'disable-enable']) {
|
|
test(`queued scheduled execution is invalidated by ${operation}`, async () => {
|
|
const f = await fixture(), old = f.add(), blocker = await f.block();
|
|
const queued = old.fire();
|
|
if (operation === 'disable' || operation === 'disable-enable') { f.records.get('1').isDisabled = 1; f.remove(); }
|
|
if (operation === 'delete') { f.records.delete('1'); f.remove(); }
|
|
if (operation === 'update') f.add('1', 'new');
|
|
if (operation === 'replace') f.add('1', 'new', true);
|
|
if (operation === 'disable-enable') f.add('1', 'old');
|
|
const next = f.jobs.get('1')?.[0].fire();
|
|
await blocker.release(); await queued; await next;
|
|
assert.deepEqual(f.spawned, ['update', 'replace'].includes(operation) ? ['new'] : operation === 'disable-enable' ? ['old'] : []);
|
|
assert.equal(f.limit.cronLimitPendingCount, 0);
|
|
assert.equal(f.limit.cronLimitActiveCount, 0);
|
|
});
|
|
}
|
|
|
|
test('stale callbacks cannot re-enqueue after cancellation', async () => {
|
|
const f = await fixture(), old = f.add(); f.remove(); await old.fire();
|
|
assert.deepEqual(f.spawned, []);
|
|
});
|
|
|
|
test('invalidation during an asynchronous database read prevents spawning', async () => {
|
|
const f = await fixture(), old = f.add(), pending = gate(); f.setReadGate(pending);
|
|
const run = old.fire(); await tick(); f.remove(); pending.release(); await run;
|
|
assert.deepEqual(f.spawned, []);
|
|
});
|
|
|
|
test('invalidation while replacing a running instance prevents the old spawn', async () => {
|
|
const f = await fixture(), old = f.add(), pending = gate();
|
|
f.records.set('1', { isDisabled: 0, allow_multiple_instances: 0, pid: 10, status: 0 }); f.setKillGate(pending);
|
|
const run = old.fire(); await tick(); f.remove(); pending.release(); await run;
|
|
assert.deepEqual(f.spawned, []);
|
|
});
|
|
|
|
test('database removal or disable is respected before scheduler RPC arrives', async () => {
|
|
for (const disabled of [true, false]) {
|
|
const f = await fixture(), old = f.add(), blocker = await f.block(); const run = old.fire();
|
|
if (disabled) f.records.get('1').isDisabled = 1; else f.records.delete('1');
|
|
await blocker.release(); await run; assert.deepEqual(f.spawned, []);
|
|
}
|
|
});
|
|
|
|
test('five stale runs do not suppress the new revision or erase its repeat accounting', async () => {
|
|
const f = await fixture(), old = f.add(), blocker = await f.block();
|
|
const pending = Array.from({ length: 5 }, () => old.fire());
|
|
const current = f.add('1', 'new'); pending.push(...Array.from({ length: 5 }, () => current.fire()));
|
|
assert.equal(f.notices.length, 0);
|
|
await blocker.release(); await Promise.all(pending);
|
|
assert.deepEqual(f.spawned, Array(5).fill('new'));
|
|
await current.fire(); assert.equal(f.spawned.length, 6);
|
|
});
|