mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-02 15:56:59 +08:00
Package production dependencies per architecture for online and offline upgrades. Validate dependencies and isolated backend startup before readiness, repair once when permitted, and preserve service/data through failed preparation or rollback. Publish release bundles with master images and expose an opt-in develop build sharing the development index across Alpine and Debian images.
158 lines
5.6 KiB
JavaScript
158 lines
5.6 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs/promises');
|
|
const path = require('node:path');
|
|
const { execFile } = require('node:child_process');
|
|
const { promisify } = require('node:util');
|
|
const execute = promisify(execFile);
|
|
const {
|
|
manualUpgradeFixture,
|
|
} = require('../helpers/manual-upgrade-fixture.cjs');
|
|
const jobs = require('../../cli/src/internal/maintenance/panelUpgrade.cjs');
|
|
|
|
for (const previousStatus of ['ready', 'failed', 'completed', 'idle', 'absent'])
|
|
for (const wrongVersion of [false, true])
|
|
test(`manual application records the selected package after ${previousStatus}; wrong version=${wrongVersion}`, async (t) => {
|
|
const f = await manualUpgradeFixture(t, wrongVersion);
|
|
await fs.writeFile(
|
|
path.join(f.root, 'package.json'),
|
|
'{"qinglongUpgradeFormat":2}',
|
|
);
|
|
const file = path.join(f.tmp, 'panel-upgrade.json');
|
|
if (previousStatus === 'absent') await fs.unlink(file);
|
|
else
|
|
await fs.writeFile(
|
|
file,
|
|
JSON.stringify({
|
|
...(await f.state()),
|
|
status: previousStatus,
|
|
error: 'previous preparation failed',
|
|
version: '9.9.9',
|
|
sourceCommit: 'f'.repeat(40),
|
|
backendSha256: 'f'.repeat(64),
|
|
stageDirectory: 'upgrade-other',
|
|
}),
|
|
);
|
|
const operation = execute(
|
|
process.execPath,
|
|
[path.resolve('shell/upgrade.cjs'), 'reload'],
|
|
{ env: f.env, timeout: 15000 },
|
|
);
|
|
if (wrongVersion)
|
|
await assert.rejects(operation, /启动版本与更新包不一致/);
|
|
else await operation;
|
|
const state = await f.state();
|
|
assert.equal(state.status, wrongVersion ? 'failed' : 'completed');
|
|
assert.equal(state.sourceCommit, f.expected.sourceCommit);
|
|
assert.equal(state.stageDirectory, f.expected.stageDirectory);
|
|
assert.equal(state.version, f.expected.version);
|
|
assert.notEqual(state.id, 'offline-prepared');
|
|
assert.equal(state.action, 'apply');
|
|
assert.notEqual(state.error, 'previous preparation failed');
|
|
if (!wrongVersion) assert.equal(state.error, undefined);
|
|
assert.equal(
|
|
await fs.readFile(path.join(f.root, 'static/build/app.js'), 'utf8'),
|
|
wrongVersion ? 'old backend' : 'new backend',
|
|
);
|
|
assert.equal(
|
|
await fs.readFile(path.join(f.root, 'data/keep'), 'utf8'),
|
|
'preserve data',
|
|
);
|
|
});
|
|
|
|
test('manual application rejects a selection that no longer matches the ready pointer', async (t) => {
|
|
const f = await manualUpgradeFixture(t);
|
|
let called = false;
|
|
await assert.rejects(
|
|
jobs.withPreparedJob(
|
|
f.tmp,
|
|
f.env,
|
|
{ ...f.selected, source: f.selected.source + '-stale' },
|
|
async () => {
|
|
called = true;
|
|
},
|
|
),
|
|
/暂存更新已变化/,
|
|
);
|
|
assert.equal(called, false);
|
|
assert.equal((await f.state()).status, 'ready');
|
|
});
|
|
|
|
test('legacy manual reload without a job or bundle capability stays self-contained', async (t) => {
|
|
const f = await manualUpgradeFixture(t);
|
|
await fs.unlink(path.join(f.tmp, 'panel-upgrade.json'));
|
|
await fs.unlink(path.join(f.tmp, 'upgrade-ready-develop.json'));
|
|
const result = await jobs.withPreparedJob(
|
|
f.tmp,
|
|
f.env,
|
|
f.selected,
|
|
async (job) => {
|
|
assert.equal(job, undefined);
|
|
return 'legacy';
|
|
},
|
|
);
|
|
assert.equal(result, 'legacy');
|
|
await assert.rejects(fs.access(path.join(f.tmp, 'panel-upgrade.json')), {
|
|
code: 'ENOENT',
|
|
});
|
|
});
|
|
|
|
for (const wrongVersion of [false, true]) {
|
|
test(`Bash reload consumes offline readiness and records the actual result; wrong version=${wrongVersion}`, async (t) => {
|
|
const f = await manualUpgradeFixture(t, wrongVersion);
|
|
const operation = execute(
|
|
process.execPath,
|
|
[path.resolve('shell/upgrade.cjs'), 'reload'],
|
|
{ env: f.env, timeout: 15000 },
|
|
);
|
|
if (wrongVersion) await assert.rejects(operation, /启动版本与更新包不一致/);
|
|
else await operation;
|
|
const state = await f.state();
|
|
assert.equal(state.status, wrongVersion ? 'failed' : 'completed');
|
|
assert.equal(state.action, 'apply');
|
|
assert.notEqual(state.id, 'offline-prepared');
|
|
assert.equal(
|
|
await fs.readFile(path.join(f.root, 'static/build/app.js'), 'utf8'),
|
|
wrongVersion ? 'old backend' : 'new backend',
|
|
);
|
|
assert.equal(
|
|
await fs.readFile(path.join(f.root, '.env'), 'utf8'),
|
|
'preserve configuration',
|
|
);
|
|
assert.equal(
|
|
await fs.readFile(path.join(f.root, 'data/keep'), 'utf8'),
|
|
'preserve data',
|
|
);
|
|
if (wrongVersion)
|
|
assert.equal(
|
|
await fs.readFile(path.join(f.root, 'node_modules/old'), 'utf8'),
|
|
'old dependency',
|
|
);
|
|
else
|
|
await assert.rejects(
|
|
fs.access(path.join(f.tmp, 'upgrade-ready-develop.json')),
|
|
);
|
|
});
|
|
}
|
|
|
|
test('manual application holds the launch lock and does not allow a competing panel job', async (t) => {
|
|
const f = await manualUpgradeFixture(t);
|
|
await assert.rejects(
|
|
jobs.withPreparedJob(f.tmp, f.env, f.selected, async (job) => {
|
|
assert.equal((await f.state()).status, 'running');
|
|
await assert.rejects(
|
|
jobs.startJob({ root: f.root, tmp: f.tmp, env: f.env }, 'apply'),
|
|
/已有更新/,
|
|
);
|
|
await job.onStop();
|
|
assert.equal((await f.state()).status, 'restarting');
|
|
throw new Error('cancel manual operation');
|
|
}),
|
|
/cancel manual operation/,
|
|
);
|
|
assert.equal((await f.state()).status, 'failed');
|
|
await assert.rejects(
|
|
fs.access(path.join(f.tmp, 'panel-job-launch/upgrade.lock')),
|
|
);
|
|
});
|