mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-28 09:02:12 +08:00
* feat(cli): add unified Commander CLI for QingLong 2.x * fix(cli): publish via npm and address security review feedback * ci(cli): package npm artifacts and remove evaluation collateral * test(cli): use a fixed shell fixture for log retention * refactor(cli): separate remote npm client from panel tools * feat(cli): cover active panel OpenAPI resources * docs(cli): unify authentication and skill guidance * refactor(cli): isolate internal commands and generate Commander help * refactor(cli): organize remote and internal modules by responsibility * ci(cli): publish verified npm archives from master * fix(cli): publish under the whyour npm scope * ci: use npm trusted publishing for both packages * docs: introduce the published CLI on the project homepage * fix(cli): preserve server log truncation and correct login hints * fix(cli): accept dashboard record request bodies * fix(cli): preserve stdin for local task execution * fix(cli): resolve task executables after changing directory * fix(cli): preserve shell function tasks and sanitize test failures * fix(cli): preserve shell hook state and resolve workdir after hooks * fix(cli): preserve cleanup across shared shell task timeouts * fix(cli): isolate shell control descriptors and reap timed-out descendants
76 lines
3.3 KiB
JavaScript
76 lines
3.3 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs/promises');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { createContext } = require('../../dist/internal/runtime/context');
|
|
const { startPanel, stopPanel } = require('../../dist/internal/maintenance/operator');
|
|
const { reloadPanel } = require('../../dist/internal/maintenance/upgrade');
|
|
|
|
test(
|
|
'data reload preserves a real mount root while replacing contents and restarting the backend',
|
|
{ timeout: 30000 },
|
|
async (t) => {
|
|
assert.equal(
|
|
process.env.QL_MOUNT_TEST,
|
|
'1',
|
|
'Run only in the documented disposable mounted-data container',
|
|
);
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-mounted-data-'));
|
|
const data = '/mounted-data';
|
|
assert.deepEqual(await fs.readdir(data), []);
|
|
const before = await fs.stat(data);
|
|
assert.notEqual(before.dev, (await fs.stat(root)).dev);
|
|
const context = createContext(
|
|
{ root, 'data-dir': data },
|
|
{ PATH: '/nonexistent' },
|
|
);
|
|
t.after(async () => {
|
|
await stopPanel(context);
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
});
|
|
await fs.writeFile(path.join(data, 'version'), 'old');
|
|
await fs.writeFile(path.join(data, 'obsolete'), 'remove');
|
|
await fs.mkdir(path.join(root, '.tmp/data'), { recursive: true });
|
|
await fs.writeFile(path.join(root, '.tmp/data/version'), 'new');
|
|
await fs.writeFile(path.join(root, '.tmp/data/.hidden'), 'included');
|
|
await fs.mkdir(path.join(root, 'static/build'), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(root, 'static/build/app.js'),
|
|
`const fs=require('node:fs');if(fs.readFileSync(process.env.QL_DATA_DIR+'/version','utf8')==='broken')process.exit(7);const s=require('node:http').createServer((q,r)=>r.end(fs.readFileSync(process.env.QL_DATA_DIR+'/version')));s.listen(0,'127.0.0.1',()=>fs.writeFileSync(process.env.QL_DIR+'/port',String(s.address().port)));`,
|
|
);
|
|
await startPanel(context);
|
|
const response = async () => {
|
|
const port = await fs.readFile(path.join(root, 'port'), 'utf8');
|
|
return (
|
|
await fetch('http://127.0.0.1:' + port, {
|
|
signal: AbortSignal.timeout(3000),
|
|
})
|
|
).text();
|
|
};
|
|
assert.equal(await response(), 'old');
|
|
const result = await reloadPanel(context, 'data');
|
|
assert.deepEqual(result.retainedBackups, []);
|
|
assert.equal(await response(), 'new');
|
|
assert.equal((await fs.stat(data)).ino, before.ino);
|
|
assert.equal((await fs.stat(data)).dev, before.dev);
|
|
await assert.rejects(fs.access(path.join(data, 'obsolete')));
|
|
assert.equal(
|
|
await fs.readFile(path.join(data, '.hidden'), 'utf8'),
|
|
'included',
|
|
);
|
|
assert.ok(
|
|
!(await fs.readdir(data)).some((name) => name.includes('ql-backup')),
|
|
);
|
|
await fs.writeFile(path.join(root, '.tmp/data/version'), 'broken');
|
|
await fs.writeFile(path.join(root, '.tmp/data/new-only'), 'must roll back');
|
|
await assert.rejects(reloadPanel(context, 'data'), /startup/);
|
|
assert.equal(await response(), 'new');
|
|
assert.equal((await fs.stat(data)).ino, before.ino);
|
|
await assert.rejects(fs.access(path.join(data, 'new-only')));
|
|
assert.ok(
|
|
!(await fs.readdir(data)).some((name) => name.includes('ql-backup')),
|
|
);
|
|
},
|
|
);
|