Files
qinglong/test/back/stop-failure-feedback.test.cjs
whyour 15d54866a8 feat: migrate SQLite runtime and verify complete upgrade bundles
Use MikroORM's official Node SQLite dialect with bounded database isolation, preserve legacy schema and data for rollback, and require Node 22.17 or newer. Update Debian runtimes and retain ARMv6 musl compatibility without a SQLite addon.

Complete short archive writes, stop orphaned SQL workers after owner death, preserve buffered startup requests, and prestart the archived backend on every platform. Cover real package installation, migration, failure recovery, and legacy database compatibility.

Keep authentication storage failures distinct from expired sessions with a pinned Keyv runtime, propagated cache errors, and retryable HTTP 503 responses. Preserve genuine 401 failures and verify legacy cache bytes, initialization safety, and session recovery.
2026-10-22 13:47:26 +08:00

165 lines
5.0 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const load = require('../helpers/load-security-module.cjs');
const logger = { info() {}, error() {} };
function cronFixture(killTask) {
const events = [];
const { runCron } = load('back/shared/runCron.ts', {
'cross-spawn': {
spawn: () => {
events.push('spawn');
return {};
},
},
'./childProcess': {
observeChildProcess: () => ({ completed: Promise.resolve({ code: 0 }) }),
asError: (e) => e,
},
'./pLimit': {
runWithCronLimit: async (_, fn) => fn(),
removeQueuedCron: () => events.push('release'),
},
'../loaders/logger': logger,
'../config/util': { killTask },
'../data/cron': {
CrontabModel: {
findOne: async (where, read = {}) => {
return {
pid: 100,
status: 0,
allow_multiple_instances: 0,
};
},
nativeUpdate: async (where, data) => {
return events.push('idle');
},
},
CrontabStatus: { running: 0, idle: 1, queued: 3 },
},
'../data/runningInstance': {
RunningInstanceModel: {
nativeUpdate: async (where, data) => {
return events.push('stopped');
},
},
InstanceStatus: { running: 0, stopped: 2 },
},
});
return { run: () => runCron('ignored', { id: '1' }), events };
}
test('single-instance replacement does not spawn or finalize when termination fails', async () => {
const f = cronFixture(async () => {
throw Error('EPERM');
});
await f.run();
assert.deepEqual(f.events, ['release']);
});
test('single-instance replacement waits for verified exit before spawning', async () => {
let finish, entered, verified;
const pending = new Promise((resolve) => {
entered = resolve;
});
const gate = new Promise((resolve) => {
finish = resolve;
});
const f = cronFixture(async (pid, wait) => {
assert.equal(pid, 100);
verified = wait;
entered();
await gate;
});
const running = f.run();
await pending;
assert.deepEqual(f.events, []);
finish();
await running;
assert.equal(verified, true);
assert.deepEqual(f.events, ['stopped', 'idle', 'spawn', 'release']);
});
function subscriptionFixture(killTask) {
const updates = [];
const Service = load('back/services/subscription.ts', {
'../config': {},
'../data/cron': {},
'../config/const': {},
'../data/subscription': {
SubscriptionModel: {
find: async (where, read = {}) => {
return [
{ id: 1, pid: 101 },
{ id: 2, pid: 102 },
];
},
nativeUpdate: async (where, values) => {
const query = {
where: where,
};
return updates.push({ values, query });
},
},
SubscriptionStatus: { idle: 1 },
},
'../config/util': { killTask },
'../config/subscription': {},
'../shared/i18n': {},
'../shared/pLimit': {},
'../shared/logReader': {},
'../shared/logStreamManager': {},
'./schedule': {},
'./sock': {},
'./sshKey': {},
'./cron': {},
}).default;
return { service: new Service(logger, {}, {}, {}, {}), updates };
}
test('batch subscription stop preserves failed items and reports failure while stopping others', async () => {
const calls = [];
const denied = Error('EPERM');
const f = subscriptionFixture(async (pid, wait) => {
calls.push([pid, wait]);
if (pid === 101) throw denied;
});
await assert.rejects(f.service.stop([1, 2]), (e) => e === denied);
assert.deepEqual(calls, [
[101, true],
[102, true],
]);
assert.equal(f.updates.length, 1);
assert.equal(f.updates[0].query.where.id, 2);
assert.equal(f.updates[0].values.status, 1);
});
function scriptFixture(killTask, pid = 100) {
const Service = load('back/services/script.ts', {
'../config': { scriptPath: '/scripts' },
'../config/const': { TASK_COMMAND: 'task' },
'../config/util': { killTask, getPid: async () => pid },
'../shared/pLimit': { removeQueuedCron() {} },
'./sock': {},
'./cron': {},
'./schedule': {},
'../shared/fileAccess': {},
}).default;
return new Service(logger, {}, {}, {});
}
test('script stop propagates failure instead of returning success', async () => {
const denied = Error('EPERM');
const service = scriptFixture(async () => {
throw denied;
});
await assert.rejects(
service.stopScript('/scripts/test.js', 100),
(e) => e === denied,
);
});
test('script stop verifies exit and treats an absent queued process as already stopped', async () => {
const calls = [];
const service = scriptFixture(async (...args) => calls.push(args), undefined);
assert.equal((await service.stopScript('/scripts/test.js', 100)).code, 200);
assert.deepEqual(calls, [[100, true]]);
const absent = scriptFixture(async () => assert.fail('no PID to stop'), null);
assert.equal(
(await absent.stopScript('/scripts/test.js', undefined)).code,
200,
);
});