mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-07 19:19:53 +08:00
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.
131 lines
4.3 KiB
JavaScript
131 lines
4.3 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs/promises');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
|
|
const {
|
|
getDirectorySize,
|
|
normalizeRetentionPolicy,
|
|
} = require('../../back/shared/retention');
|
|
|
|
test('retention policy defaults to disabled and clamps invalid values', () => {
|
|
assert.deepEqual(normalizeRetentionPolicy({}), {
|
|
runningInstanceRetentionDays: 0,
|
|
cronStatRetentionDays: 0,
|
|
});
|
|
assert.deepEqual(
|
|
normalizeRetentionPolicy({
|
|
runningInstanceRetentionDays: -10,
|
|
cronStatRetentionDays: 99999,
|
|
}),
|
|
{
|
|
runningInstanceRetentionDays: 0,
|
|
cronStatRetentionDays: 3650,
|
|
},
|
|
);
|
|
});
|
|
|
|
test('directory preview counts files recursively and ignores symlinks', async (t) => {
|
|
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-retention-'));
|
|
const nested = path.join(directory, 'nested');
|
|
t.after(() => fs.rm(directory, { recursive: true, force: true }));
|
|
await fs.mkdir(nested);
|
|
await fs.writeFile(path.join(directory, 'one'), Buffer.alloc(7));
|
|
await fs.writeFile(path.join(nested, 'two'), Buffer.alloc(11));
|
|
await fs.symlink(path.join(nested, 'two'), path.join(directory, 'link'));
|
|
|
|
assert.equal(await getDirectorySize(directory), 18);
|
|
assert.equal(await getDirectorySize(path.join(directory, 'missing')), 0);
|
|
});
|
|
|
|
test('cleanup previews first, protects running instances, and uses explicit options', async (t) => {
|
|
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-retention-'));
|
|
const nodeCache = path.join(directory, 'node');
|
|
await fs.mkdir(nodeCache);
|
|
await fs.writeFile(path.join(nodeCache, 'cache'), Buffer.alloc(13));
|
|
|
|
const config = require('../../back/config').default;
|
|
const originalCachePath = config.dependenceCachePath;
|
|
const RunningInstanceModel = {};
|
|
const CrontabStatModel = {};
|
|
const InstanceStatus = { running: 0 };
|
|
const sequelize = {};
|
|
const stubModule = (modulePath, exports) => {
|
|
require.cache[require.resolve(modulePath)] = {
|
|
id: require.resolve(modulePath),
|
|
filename: require.resolve(modulePath),
|
|
loaded: true,
|
|
exports,
|
|
children: [],
|
|
paths: [],
|
|
};
|
|
};
|
|
stubModule('../../back/data', { database: sequelize });
|
|
stubModule('../../back/data/cronStats', { CrontabStatModel });
|
|
stubModule('../../back/data/runningInstance', {
|
|
InstanceStatus,
|
|
RunningInstanceModel,
|
|
});
|
|
stubModule('../../back/data/system', {
|
|
AuthDataType: { systemConfig: 'systemConfig' },
|
|
SystemModel: {},
|
|
});
|
|
delete require.cache[require.resolve('../../back/services/retention')];
|
|
const RetentionService = require('../../back/services/retention').default;
|
|
|
|
t.after(async () => {
|
|
config.dependenceCachePath = originalCachePath;
|
|
await fs.rm(directory, { recursive: true, force: true });
|
|
});
|
|
|
|
let instanceWhere;
|
|
let vacuumed = false;
|
|
let instanceCountCalls = 0;
|
|
let statCountCalls = 0;
|
|
config.dependenceCachePath = directory;
|
|
RunningInstanceModel.count = async (where) => {
|
|
instanceCountCalls++;
|
|
instanceWhere = where;
|
|
return 2;
|
|
};
|
|
RunningInstanceModel.nativeDelete = async (where) => {
|
|
instanceWhere = where;
|
|
return 2;
|
|
};
|
|
CrontabStatModel.count = async () => {
|
|
statCountCalls++;
|
|
return 3;
|
|
};
|
|
CrontabStatModel.nativeDelete = async () => 3;
|
|
sequelize.transaction = async (callback) => callback({});
|
|
sequelize.query = async (query) => {
|
|
vacuumed = query === 'VACUUM';
|
|
return [];
|
|
};
|
|
|
|
const service = new RetentionService();
|
|
const disabledPreview = await service.preview({
|
|
runningInstanceRetentionDays: 0,
|
|
cronStatRetentionDays: 0,
|
|
});
|
|
assert.equal(disabledPreview.runningInstances, 0);
|
|
assert.equal(disabledPreview.cronStats, 0);
|
|
assert.equal(instanceCountCalls, 0);
|
|
assert.equal(statCountCalls, 0);
|
|
|
|
const result = await service.cleanup({
|
|
runningInstanceRetentionDays: 30,
|
|
cronStatRetentionDays: 90,
|
|
dependenceCacheTypes: ['node'],
|
|
compactDatabase: true,
|
|
});
|
|
|
|
assert.equal(instanceWhere.status.$ne, InstanceStatus.running);
|
|
assert.equal(result.deleted.runningInstances, 2);
|
|
assert.equal(result.deleted.cronStats, 3);
|
|
assert.equal(result.deleted.dependenceCacheBytes, 13);
|
|
assert.equal(vacuumed, true);
|
|
await assert.rejects(fs.stat(nodeCache), { code: 'ENOENT' });
|
|
});
|