Files
qinglong/cli/test/internal/cronEntrypoint.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

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