Files
qinglong/cli/test/linux/image-selection.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

75 lines
2.5 KiB
JavaScript

const assert = require('node:assert/strict');
const fs = require('node:fs/promises');
const { spawnSync } = require('node:child_process');
const { createContext } = require('/opt/qinglong-cli/dist/internal/runtime/context');
const { LocalApi } = require('/opt/qinglong-cli/dist/internal/runtime/api');
assert.equal(process.env.QL_PANEL_INTEGRATION, '1');
(async () => {
const api = new LocalApi(createContext({ root: '/ql' }, process.env));
await fs.writeFile(
'/ql/data/scripts/image-selection.js',
'console.log("IMAGE_PARENT="+JSON.stringify(require("node:fs").readFileSync("/proc/"+process.ppid+"/cmdline","utf8").split("\\0")));',
);
const task = (
await api.call('crons', 'POST', {
name: 'image selection fixture',
command: 'task image-selection.js',
schedule: '0 0 1 1 *',
})
).data;
try {
await api.call('crons/run', 'PUT', [task.id]);
let log = '';
for (let i = 0; i < 50; i++) {
await new Promise((resolve) => setTimeout(resolve, 200));
log = (await api.call(`crons/${task.id}/log`)).data;
if (log.includes('IMAGE_PARENT=')) break;
}
const parent = JSON.parse(log.match(/IMAGE_PARENT=(.*)/)[1]);
assert.match(parent[0], /(?:^|\/)node$/);
assert.equal(parent[1], '/root/bin/task');
assert.match(
await fs.readFile('/root/bin/task', 'utf8'),
/\/opt\/qinglong-cli\/dist\/task.js/,
);
const reload = spawnSync('ql', ['reload'], {
encoding: 'utf8',
timeout: 30000,
});
assert.equal(reload.status, 0, reload.stderr);
assert.equal(JSON.parse(reload.stdout).code, 200);
let healthy = false;
for (let i = 0; i < 50; i++) {
try {
healthy =
(
await (
await fetch('http://127.0.0.1:5700/api/system', {
signal: AbortSignal.timeout(1000),
})
).json()
).code === 200;
} catch {}
if (healthy) break;
await new Promise((resolve) => setTimeout(resolve, 200));
}
assert.ok(healthy);
const help = spawnSync('ql', ['--help'], { encoding: 'utf8' });
assert.equal(help.status, 0);
assert.match(help.stdout, /ql-compat/);
console.log(
JSON.stringify({
packagedTaskScheduled: true,
packagedReload: true,
healthyAfterReload: true,
selectionRetained: true,
}),
);
} finally {
await api.call('crons', 'DELETE', [task.id]);
}
})().catch((error) => {
console.error(error.stack);
process.exitCode = 1;
});