Files
qinglong/test/back/schema-migrations.test.cjs
T
whyour a94e665054 fix: 修复任务生命周期与调度就绪,优化执行和构建开销 (#3069)
* 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
2026-09-13 00:32:41 +08:00

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/);
});