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
102 lines
3.7 KiB
JavaScript
102 lines
3.7 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 { createContext } = require('../../dist/internal/runtime/context');
|
|
const { bootstrapPanel } = require('../../dist/internal/maintenance/bootstrap');
|
|
const { stopPanel } = require('../../dist/internal/maintenance/operator');
|
|
const { runProcess } = require('../../dist/internal/runtime/process');
|
|
|
|
test(
|
|
'real PM2 and nginx bootstrap reload serves requests and replaces configuration',
|
|
{ timeout: 45000 },
|
|
async (t) => {
|
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-services-'));
|
|
const context = createContext(
|
|
{ root },
|
|
{
|
|
PATH: process.env.PATH,
|
|
PM2_HOME: path.join(root, 'pm2'),
|
|
HOME: root,
|
|
},
|
|
);
|
|
const nginxConfig = path.join(root, 'nginx.conf');
|
|
const options = {
|
|
cwd: root,
|
|
env: context.env,
|
|
output: () => {},
|
|
timeoutMs: 10000,
|
|
};
|
|
t.after(async () => {
|
|
await runProcess('nginx', ['-c', nginxConfig, '-s', 'quit'], options);
|
|
await runProcess('pm2', ['kill'], options);
|
|
await fs.rm(root, { recursive: true, force: true });
|
|
});
|
|
await fs.mkdir(path.join(root, 'sample'));
|
|
for (const name of [
|
|
'config.sample.sh',
|
|
'task.sample.sh',
|
|
'extra.sample.sh',
|
|
'notify.py',
|
|
'notify.js',
|
|
'ql_sample.js',
|
|
'ql_sample.py',
|
|
])
|
|
await fs.writeFile(path.join(root, 'sample', name), '');
|
|
await fs.writeFile(path.join(root, '.env.example'), 'FIXTURE=1\n');
|
|
await fs.mkdir(path.join(root, 'static/build'), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(root, 'static/build/app.js'),
|
|
'require("http").createServer((q,s)=>s.end(JSON.stringify({pid:process.pid}))).listen(5801,"127.0.0.1")',
|
|
);
|
|
await fs.writeFile(
|
|
path.join(root, 'ecosystem.config.js'),
|
|
'module.exports={apps:[{name:"fixture-panel",script:"static/build/app.js",instances:1,exec_mode:"fork"}]}',
|
|
);
|
|
const writeNginx = (generation) =>
|
|
fs.writeFile(
|
|
nginxConfig,
|
|
`pid ${root}/nginx.pid;\nerror_log ${root}/nginx-error.log;\nevents {}\nhttp { access_log off; server { listen 127.0.0.1:5802; location / { add_header X-Fixture ${generation}; proxy_pass http://127.0.0.1:5801; } } }\n`,
|
|
);
|
|
await writeNginx('one');
|
|
const system = {
|
|
nginxConfig,
|
|
nginxIncludes: path.join(root, 'nginx-includes'),
|
|
nginxRun: path.join(root, 'nginx-run'),
|
|
background: async () => {
|
|
throw new Error('reload must not dispatch hooks');
|
|
},
|
|
};
|
|
async function response(generation) {
|
|
const deadline = Date.now() + 5000;
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const result = await fetch('http://127.0.0.1:5802', {
|
|
signal: AbortSignal.timeout(500),
|
|
});
|
|
const body = await result.json();
|
|
if (result.ok && result.headers.get('x-fixture') === generation)
|
|
return body;
|
|
} catch {}
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
}
|
|
throw new Error(`nginx did not serve generation ${generation}`);
|
|
}
|
|
const first = await bootstrapPanel(context, true, system);
|
|
assert.equal(first.service.manager, 'pm2');
|
|
const before = await response('one');
|
|
await writeNginx('two');
|
|
const second = await bootstrapPanel(context, true, system);
|
|
assert.equal(second.service.manager, 'pm2');
|
|
const after = await response('two');
|
|
assert.notEqual(after.pid, before.pid);
|
|
await stopPanel(context);
|
|
const list = await runProcess('pm2', ['jlist'], {
|
|
...options,
|
|
capture: true,
|
|
});
|
|
assert.deepEqual(JSON.parse(list.stdout), []);
|
|
},
|
|
);
|