mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-01 05:13:50 +08:00
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
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { installCronEntrypoint } = require('../../dist/local/cronEntrypoint');
|
||||
|
||||
test('cron bridge scopes environment to the panel table and preserves native reads, failures and unrelated tables', async (t) => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-cron-entry-'));
|
||||
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
const native = path.join(root, 'native'),
|
||||
bin = path.join(root, 'selected bin');
|
||||
await fs.mkdir(native);
|
||||
await fs.mkdir(bin);
|
||||
const state = path.join(root, 'installed');
|
||||
await fs.writeFile(
|
||||
path.join(native, 'crontab'),
|
||||
`#!${process.execPath}\nconst fs=require('node:fs');const args=process.argv.slice(2);if(args[0]==='-l')process.stdout.write(fs.readFileSync(process.env.TABLE));else if(args[0]==='-r'){process.stderr.write('native failure');process.exitCode=9;}else fs.writeFileSync(process.env.TABLE,fs.readFileSync(args[0]));`,
|
||||
{ mode: 0o755 },
|
||||
);
|
||||
const source = path.join(root, 'panel table');
|
||||
const original =
|
||||
'# retain this original comment as is\n* * * * * task "job with spaces.js"\n';
|
||||
await fs.writeFile(source, original);
|
||||
const env = {
|
||||
QL_LANG: 'en',
|
||||
PATH: `${bin}:${native}`,
|
||||
QL_DIR: root,
|
||||
QL_DATA_DIR: path.join(root, 'data'),
|
||||
PYTHONPATH: "/python 'path' $(literal)",
|
||||
QL_CLIENT_SECRET: 'must-not-be-captured',
|
||||
};
|
||||
const install = () =>
|
||||
installCronEntrypoint(bin, path.resolve(__dirname, '../..'), source, env);
|
||||
await install();
|
||||
await install();
|
||||
const wrapper = await fs.readFile(path.join(bin, 'crontab'), 'utf8');
|
||||
assert.doesNotMatch(wrapper, /must-not-be-captured|QL_CLIENT_SECRET/);
|
||||
const run = (args) =>
|
||||
spawnSync(path.join(bin, 'crontab'), args, {
|
||||
env: { TABLE: state },
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.equal(run([source]).status, 0);
|
||||
const installed = await fs.readFile(state, 'utf8');
|
||||
assert.ok(installed.endsWith('task "job with spaces.js"\n'));
|
||||
assert.ok(installed.includes(`PATH='${bin}:${native}'`));
|
||||
assert.ok(installed.includes('# retain this original comment as is\n'));
|
||||
const job = installed
|
||||
.split('\n')
|
||||
.find((line) => line.startsWith('* * * * * '));
|
||||
const prefix = job.slice('* * * * * '.length, job.indexOf('task "'));
|
||||
const shell = spawnSync(
|
||||
'/bin/sh',
|
||||
['-c', prefix + 'printf "%s" "$PYTHONPATH"'],
|
||||
{ encoding: 'utf8', env: {} },
|
||||
);
|
||||
assert.equal(shell.status, 0, shell.stderr);
|
||||
assert.equal(shell.stdout, env.PYTHONPATH);
|
||||
assert.equal(await fs.readFile(source, 'utf8'), original);
|
||||
assert.equal(run(['-l']).stdout, original);
|
||||
const failed = run(['-r']);
|
||||
assert.equal(failed.status, 9);
|
||||
assert.equal(failed.stderr, 'native failure');
|
||||
const other = path.join(root, 'other');
|
||||
await fs.writeFile(other, 'OTHER\n');
|
||||
assert.equal(run([other]).status, 0);
|
||||
assert.equal(await fs.readFile(state, 'utf8'), 'OTHER\n');
|
||||
env.QL_DIR = 'invalid\nvalue';
|
||||
await install();
|
||||
assert.equal(run([source]).status, 1);
|
||||
assert.equal(await fs.readFile(state, 'utf8'), 'OTHER\n');
|
||||
await fs.writeFile(path.join(bin, 'crontab'), '# unrelated command\n');
|
||||
await assert.rejects(install(), /Refusing to replace/);
|
||||
assert.equal(
|
||||
await fs.readFile(path.join(bin, 'crontab'), 'utf8'),
|
||||
'# unrelated command\n',
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user