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
118 lines
4.4 KiB
JavaScript
118 lines
4.4 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 { spawnSync } = require('node:child_process');
|
|
const { installCliEntrypoints } = require('../../dist/local/entrypoints');
|
|
|
|
test('unified ql groups and task shortcut preserve help, errors and local execution boundaries', async (t) => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-unified-'));
|
|
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
|
const env = {
|
|
PATH: process.env.PATH,
|
|
QL_DIR: root,
|
|
QL_CLI_CONFIG: path.join(root, 'credentials'),
|
|
QL_LANG: 'en',
|
|
};
|
|
const call = (entry, args) =>
|
|
spawnSync(
|
|
process.execPath,
|
|
[path.resolve(__dirname, `../../dist/${entry}.js`), ...args],
|
|
{ env, encoding: 'utf8', timeout: 10000 },
|
|
);
|
|
for (const group of ['', 'task', 'local']) {
|
|
const result = call('ql', [...(group ? [group] : []), '--help', '--json']);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(JSON.parse(result.stdout).data.help, /ql/);
|
|
}
|
|
for (const kind of ['repo', 'raw']) {
|
|
const help = call('ql', ['local', kind, '--help', '--json']);
|
|
assert.equal(help.status, 0, help.stderr);
|
|
assert.match(JSON.parse(help.stdout).data.help, /ql repo/);
|
|
}
|
|
for (const args of [
|
|
['task', 'list'],
|
|
['task', 'run', '12'],
|
|
['subscription', 'list'],
|
|
['app', 'list'],
|
|
['system', 'info'],
|
|
['auth', 'status'],
|
|
['api', 'routes'],
|
|
]) {
|
|
const result = call('ql', [...args, '--json']);
|
|
assert.equal(result.status, 2, result.stderr);
|
|
assert.equal(result.stdout, '');
|
|
}
|
|
const invalid = call('ql', ['task', 'run', 'demo.sh', '--json']);
|
|
assert.equal(invalid.status, 2);
|
|
assert.equal(invalid.stdout, '');
|
|
const local = call('ql', [
|
|
'local',
|
|
'repair-config',
|
|
'--root',
|
|
'relative',
|
|
'--json',
|
|
]);
|
|
assert.equal(local.status, 2);
|
|
assert.match(JSON.parse(local.stderr).message, /absolute/);
|
|
await fs.mkdir(path.join(root, 'data/scripts'), { recursive: true });
|
|
await fs.mkdir(path.join(root, 'data/config'), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(root, 'data/config/config.sh'),
|
|
'no_tee=false\n',
|
|
);
|
|
await fs.writeFile(
|
|
path.join(root, 'data/scripts/demo.sh'),
|
|
'printf "script:%s\\n" "$1"; return 7\n',
|
|
);
|
|
for (const [entry, args] of [
|
|
['ql', ['task', 'demo.sh', 'now', '--', '--json']],
|
|
['ql', ['task', 'exec', 'demo.sh', 'now', '--', '--json']],
|
|
['task', ['demo.sh', 'now', '--', '--json']],
|
|
['task', ['exec', 'demo.sh', 'now', '--', '--json']],
|
|
]) {
|
|
const result = call(entry, args);
|
|
assert.equal(result.status, 7, result.stderr);
|
|
assert.match(result.stdout, /script:--json/);
|
|
}
|
|
const shortcut = call('task', ['list', '--json']);
|
|
assert.equal(shortcut.status, 2, shortcut.stderr);
|
|
const legacy = call('ql', ['update', 'invalid']);
|
|
assert.equal(legacy.status, 2, legacy.stderr);
|
|
assert.ok(legacy.stderr.length > 0);
|
|
for (const args of [['raw'], ['local', 'raw'], ['repo'], ['local', 'repo']]) {
|
|
const result = call('ql', args);
|
|
assert.equal(result.status, 2, result.stderr);
|
|
}
|
|
const bin = path.join(root, 'bin');
|
|
await installCliEntrypoints(bin, path.resolve(__dirname, '../..'));
|
|
for (const name of ['ql', 'task']) {
|
|
const result = spawnSync(path.join(bin, name), ['--help'], {
|
|
env,
|
|
encoding: 'utf8',
|
|
});
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /task/);
|
|
}
|
|
});
|
|
|
|
test('internal rejection never loads remote credentials or local operators', () => {
|
|
const code = `process.stdout.write=()=>true;process.stderr.write=()=>true;
|
|
require('./cli/dist/ql').qlMain(['task','list','--json']).then(code=>{
|
|
if(code!==2) process.exit(1);
|
|
if(Object.keys(require.cache).some(p=>p.includes('/dist/internal/runtime/') || p.includes('/dist/remote/') || p.includes('/dist/remote/commands/auth') || p.includes('/dist/remote/auth/store'))) process.exit(2);
|
|
});`;
|
|
const result = spawnSync(process.execPath, ['-e', code], {
|
|
cwd: path.resolve(__dirname, '../../..'),
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
QL_CLI_CONFIG: '/nonexistent-ql-unified-config',
|
|
QL_URL: 'https://must-not-be-contacted.invalid',
|
|
QL_ACCESS_TOKEN: 'test-only',
|
|
},
|
|
encoding: 'utf8',
|
|
});
|
|
assert.equal(result.status, 0, result.stderr);
|
|
});
|