Files
qinglong/cli/test/linux/mounted-data.test.cjs
T
whyour 801a71d740 feat(cli): cover OpenAPI with a remote npm CLI and internal panel tools (#3074)
* 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
2026-09-25 23:24:41 +08:00

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')),
);
},
);