mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-17 07:20:51 +08:00
fix: 修复页面升级、ql update 与静态资源版本同步 (#3086)
* fix: validate panel upgrades and synchronize versioned static builds * fix: add static-only mirror backfill workflow * fix: avoid lifecycle self-kill and stop cancelled upgrade groups * fix: build images when the shared upgrade engine changes * fix: dispatch upgrade lifecycle through a fixed script * fix: reclaim upgrade locks safely and normalize health probes * fix: match backend base URL normalization exactly
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { replaceAndReload } = require('../../shell/upgrade.cjs');
|
||||
|
||||
async function fixture(t) {
|
||||
const root = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), 'ql-upgrade-transaction-'),
|
||||
);
|
||||
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
const source = path.join(root, 'source'),
|
||||
target = path.join(root, 'target');
|
||||
for (const directory of [source, target]) await fs.mkdir(directory);
|
||||
await fs.writeFile(path.join(source, 'version'), 'new');
|
||||
await fs.writeFile(path.join(target, 'version'), 'old');
|
||||
return { root, source, target };
|
||||
}
|
||||
|
||||
for (const failure of ['backup', 'remove', 'replacement', 'start'])
|
||||
test(`Bash transaction restores the full original after an overlay ${failure} failure`, async (t) => {
|
||||
const f = await fixture(t);
|
||||
const rename = fs.rename,
|
||||
cp = fs.cp,
|
||||
rm = fs.rm;
|
||||
t.mock.method(fs, 'rename', async (source, target) => {
|
||||
if (source === f.target || source.includes('.ql-backup-')) {
|
||||
const error = new Error('overlay rename');
|
||||
error.code = 'EXDEV';
|
||||
throw error;
|
||||
}
|
||||
return rename(source, target);
|
||||
});
|
||||
let injected = false;
|
||||
t.mock.method(fs, 'cp', async (source, target, options) => {
|
||||
if (
|
||||
!injected &&
|
||||
((failure === 'backup' && source === f.target) ||
|
||||
(failure === 'replacement' && source === f.source))
|
||||
) {
|
||||
injected = true;
|
||||
await fs.mkdir(target, { recursive: true });
|
||||
await fs.writeFile(path.join(target, 'partial'), 'partial');
|
||||
throw new Error('copy failed');
|
||||
}
|
||||
return cp(source, target, options);
|
||||
});
|
||||
t.mock.method(fs, 'rm', async (target, options) => {
|
||||
if (!injected && failure === 'remove' && target === f.target) {
|
||||
injected = true;
|
||||
await fs.unlink(path.join(target, 'version'));
|
||||
throw new Error('remove failed');
|
||||
}
|
||||
return rm(target, options);
|
||||
});
|
||||
let starts = 0;
|
||||
await assert.rejects(
|
||||
replaceAndReload([f], {
|
||||
stop: async () => {},
|
||||
start: async () => {
|
||||
if (++starts === 1 && failure === 'start')
|
||||
throw new Error('start failed');
|
||||
assert.equal(
|
||||
await fs.readFile(path.join(f.target, 'version'), 'utf8'),
|
||||
'old',
|
||||
);
|
||||
},
|
||||
}),
|
||||
/failed/,
|
||||
);
|
||||
assert.deepEqual(await fs.readdir(f.target), ['version']);
|
||||
assert.ok(
|
||||
!(await fs.readdir(f.root)).some((name) => name.includes('.ql-backup-')),
|
||||
);
|
||||
});
|
||||
|
||||
test('interrupted upgrade recovers the previous service outside cancellation', async (t) => {
|
||||
const f = await fixture(t),
|
||||
controller = new AbortController();
|
||||
let starts = 0;
|
||||
await assert.rejects(
|
||||
replaceAndReload([f], {
|
||||
signal: controller.signal,
|
||||
stop: async () => {},
|
||||
start: async (recover) => {
|
||||
if (++starts === 1) {
|
||||
assert.equal(
|
||||
await fs.readFile(path.join(f.target, 'version'), 'utf8'),
|
||||
'new',
|
||||
);
|
||||
controller.abort(new Error('interrupted'));
|
||||
} else {
|
||||
assert.equal(recover, true);
|
||||
assert.equal(
|
||||
await fs.readFile(path.join(f.target, 'version'), 'utf8'),
|
||||
'old',
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
/interrupted/,
|
||||
);
|
||||
assert.equal(starts, 2);
|
||||
});
|
||||
|
||||
test('failed stop during rollback retains backups without overwriting running files', async (t) => {
|
||||
const f = await fixture(t);
|
||||
let stops = 0;
|
||||
await assert.rejects(
|
||||
replaceAndReload([f], {
|
||||
stop: async () => {
|
||||
if (++stops === 2) throw new Error('stop failed');
|
||||
},
|
||||
start: async () => {
|
||||
throw new Error('start failed');
|
||||
},
|
||||
}),
|
||||
/stop failed/,
|
||||
);
|
||||
const backup = (await fs.readdir(f.root)).find((name) =>
|
||||
name.includes('.ql-backup-'),
|
||||
);
|
||||
assert.ok(backup);
|
||||
assert.equal(
|
||||
await fs.readFile(path.join(f.root, backup, 'version'), 'utf8'),
|
||||
'old',
|
||||
);
|
||||
assert.equal(
|
||||
await fs.readFile(path.join(f.target, 'version'), 'utf8'),
|
||||
'new',
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user