Files
qinglong/cli/test/linux/published-archive.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

99 lines
3.3 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 { createHash } = require('node:crypto');
const { createContext } = require('../../dist/internal/runtime/context');
const operator = require('../../dist/internal/maintenance/operator');
const { stageUpgrade } = require('../../dist/internal/maintenance/upgrade');
const { cancellableOperation } = require('../../dist/internal/runtime/cancellation');
test(
'published master archives pass actual HTTPS download and staging validation',
{
skip: process.env.QL_ARCHIVE_INTEGRATION !== '1',
timeout: 180000,
},
async (t) => {
const mirror = process.env.QL_ARCHIVE_MIRROR || 'github';
assert.ok(['github', 'gitee'].includes(mirror));
const root = await fs.mkdtemp(
path.join(os.tmpdir(), 'ql-published-archive-'),
);
t.after(() => fs.rm(root, { recursive: true, force: true }));
await fs.writeFile(
path.join(root, 'package.json'),
'{"name":"isolated-archive-gate"}',
);
const context = createContext(
{ root },
{ PATH: process.env.PATH, HOME: root, QL_BRANCH: 'master' },
);
const installations = [];
// This gate must not execute code from the downloaded source. Real dependency
// installation and retained-data upgrade are separate existing Linux gates.
t.mock.method(
operator,
'installPanelDependencies',
async (ctx, directory) => {
assert.equal(ctx.root, root);
assert.equal(
path.dirname(path.dirname(directory)),
context.paths.dir_tmp,
);
installations.push(directory);
},
);
const controller = new AbortController();
const timer = setTimeout(() => controller.abort('SIGTERM'), 150000);
t.after(() => clearTimeout(timer));
const staged = await cancellableOperation(controller.signal, () =>
stageUpgrade(context, mirror),
);
assert.deepEqual(installations, [staged.source]);
const release = await fs.readFile(
path.join(staged.source, 'version.yaml'),
'utf8',
);
assert.match(
release,
/^version:\s*2\./m,
'Published branch must still be 2.x',
);
await fs.access(path.join(staged.static, 'build/app.js'));
const workspace = path.dirname(staged.source);
const pointer = JSON.parse(
await fs.readFile(
path.join(context.paths.dir_tmp, 'upgrade-ready-master.json'),
'utf8',
),
);
assert.equal(pointer.directory, path.basename(workspace));
assert.deepEqual(
JSON.parse(await fs.readFile(path.join(workspace, 'ready.json'), 'utf8')),
staged,
);
assert.equal(
await fs.readFile(path.join(root, 'package.json'), 'utf8'),
'{"name":"isolated-archive-gate"}',
);
const archives = {};
for (const name of ['qinglong', 'qinglong-static']) {
const content = await fs.readFile(path.join(workspace, `${name}.zip`));
archives[name] = {
bytes: content.length,
sha256: createHash('sha256').update(content).digest('hex'),
};
}
t.diagnostic(
JSON.stringify({
mirror,
release: release.match(/^version:\s*(\S+)/m)?.[1],
archives,
downloadedCodeExecuted: false,
}),
);
},
);