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
81 lines
3.4 KiB
JavaScript
81 lines
3.4 KiB
JavaScript
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',
|
|
);
|
|
});
|