mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-15 19:57:07 +08:00
* fix: harden task lifecycle and scheduler readiness * fix: confine log writes to the configured log directory * fix: verify complete build artifacts and untracked inputs * fix: reconcile scheduler state and make stop win startup races * fix: isolate cron generations and serialize scheduler recovery
128 lines
4.1 KiB
JavaScript
128 lines
4.1 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const test = require('node:test');
|
|
const fs = require('node:fs/promises');
|
|
const path = require('node:path');
|
|
const os = require('node:os');
|
|
const { Sequelize, QueryTypes } = require('sequelize');
|
|
const { migrateSchema } = require('../../back/shared/schemaMigrations');
|
|
const load = require('../helpers/load-security-module.cjs');
|
|
|
|
async function createLegacy(storage = ':memory:', omitEnvs = false) {
|
|
const database = new Sequelize({
|
|
dialect: 'sqlite',
|
|
storage,
|
|
logging: false,
|
|
});
|
|
for (const table of [
|
|
'CrontabViews',
|
|
'Subscriptions',
|
|
'Crontabs',
|
|
...(omitEnvs ? [] : ['Envs']),
|
|
]) {
|
|
await database.query(
|
|
`CREATE TABLE "${table}" (id INTEGER PRIMARY KEY, name TEXT)`,
|
|
);
|
|
}
|
|
await database.query(
|
|
'INSERT INTO "Crontabs" (id, name) VALUES (1, \'keep-me\')',
|
|
);
|
|
return database;
|
|
}
|
|
|
|
test('legacy database upgrades without losing rows and repeated migration is idempotent', async (t) => {
|
|
const database = await createLegacy();
|
|
t.after(() => database.close());
|
|
await migrateSchema(database);
|
|
await migrateSchema(database);
|
|
const rows = await database.query('SELECT * FROM "Crontabs"', {
|
|
type: QueryTypes.SELECT,
|
|
});
|
|
assert.equal(rows[0].name, 'keep-me');
|
|
assert.equal(rows[0].queued_token, null);
|
|
assert.ok(Object.hasOwn(rows[0], 'allow_multiple_instances'));
|
|
const applied = await database.query('SELECT id FROM "SchemaMigrations"', {
|
|
type: QueryTypes.SELECT,
|
|
});
|
|
assert.equal(applied.length, 15);
|
|
});
|
|
|
|
test('migration failure rolls back added columns and can be retried after repair', async (t) => {
|
|
const database = await createLegacy(':memory:', true);
|
|
t.after(() => database.close());
|
|
await assert.rejects(migrateSchema(database));
|
|
const columns = await database.getQueryInterface().describeTable('Crontabs');
|
|
assert.equal(Object.hasOwn(columns, 'work_dir'), false);
|
|
await database.query(
|
|
'CREATE TABLE "Envs" (id INTEGER PRIMARY KEY, name TEXT)',
|
|
);
|
|
await migrateSchema(database);
|
|
assert.ok(
|
|
Object.hasOwn(
|
|
await database.getQueryInterface().describeTable('Crontabs'),
|
|
'work_dir',
|
|
),
|
|
);
|
|
});
|
|
|
|
test('offline pre-upgrade backup restores the legacy schema and data', async (t) => {
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-migration-'));
|
|
t.after(() => fs.rm(dir, { recursive: true, force: true }));
|
|
const file = path.join(dir, 'database.sqlite');
|
|
const backup = path.join(dir, 'before.sqlite');
|
|
let database = await createLegacy(file);
|
|
await database.close();
|
|
await fs.copyFile(file, backup);
|
|
database = new Sequelize({
|
|
dialect: 'sqlite',
|
|
storage: file,
|
|
logging: false,
|
|
});
|
|
await migrateSchema(database);
|
|
await database.close();
|
|
await fs.copyFile(backup, file);
|
|
database = new Sequelize({
|
|
dialect: 'sqlite',
|
|
storage: file,
|
|
logging: false,
|
|
});
|
|
try {
|
|
assert.equal(
|
|
Object.hasOwn(
|
|
await database.getQueryInterface().describeTable('Crontabs'),
|
|
'work_dir',
|
|
),
|
|
false,
|
|
);
|
|
const rows = await database.query('SELECT name FROM "Crontabs"', {
|
|
type: QueryTypes.SELECT,
|
|
});
|
|
assert.equal(rows[0].name, 'keep-me');
|
|
} finally {
|
|
await database.close();
|
|
}
|
|
});
|
|
|
|
test('database loader rejects initialization failure rather than allowing workers to start', async () => {
|
|
const mocks = { './logger': { error() {}, info() {} } };
|
|
for (const [file, model] of [
|
|
['env', 'EnvModel'],
|
|
['cron', 'CrontabModel'],
|
|
['dependence', 'DependenceModel'],
|
|
['open', 'AppModel'],
|
|
['system', 'SystemModel'],
|
|
['subscription', 'SubscriptionModel'],
|
|
['cronView', 'CrontabViewModel'],
|
|
['cronStats', 'CrontabStatModel'],
|
|
['runningInstance', 'RunningInstanceModel'],
|
|
])
|
|
mocks[`../data/${file}`] = { [model]: { sync: async () => {} } };
|
|
mocks['../data'] = { sequelize: {} };
|
|
mocks['../shared/schemaMigrations'] = {
|
|
migrateSchema: async () => {
|
|
throw new Error('SQLITE_FULL');
|
|
},
|
|
};
|
|
const initialize = load(path.resolve('back/loaders/db.ts'), mocks).default;
|
|
await assert.rejects(initialize(), /SQLITE_FULL/);
|
|
});
|