const assert = require('node:assert/strict'); const { spawnSync } = require('node:child_process'); const { EventEmitter } = require('node:events'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { test } = require('node:test'); const packageRoot = path.resolve(__dirname, '..'); const moduleDirectory = path.join(packageRoot, 'dist', 'product-cli'); const cliPath = path.join(moduleDirectory, 'cli.js'); const manifest = JSON.parse( fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'), ); const { QINGLONG3_CLUSTER_PRODUCT_COMMANDS, loadQingLong3ClusterProductVersion, qingLong3ClusterProductHelp, resolveQingLong3ClusterProductCommand, } = require('../dist/product-cli/productCommand.js'); const { clusterProductSignalExitCode, forwardClusterProductSignals, } = require('../dist/product-cli/cli.js'); const { loadQingLong3ClusterProductContext, resolveQingLong3ClusterProductContextArguments, } = require('../dist/product-cli/productContext.js'); function runCli(args) { return spawnSync(process.execPath, [cliPath, ...args], { cwd: packageRoot, encoding: 'utf8', }); } function privateFile(directory, name, contents) { const filePath = path.join(directory, name); fs.writeFileSync(filePath, contents, { mode: 0o600 }); return filePath; } function contextFixture(t) { const directory = fs.realpathSync( fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-cluster-context-')), ); t.after(() => fs.rmSync(directory, { recursive: true, force: true })); const runConfig = privateFile(directory, 'run-client.json', '{}'); const packageConfig = privateFile(directory, 'package-client.json', '{}'); const kubernetes = privateFile(directory, 'kubernetes.json', '{}'); const contextFile = privateFile( directory, 'operator-context.json', JSON.stringify({ schemaVersion: 1, commands: { run: { configFile: runConfig }, 'package-kubernetes': { configFile: packageConfig, kubernetesFile: kubernetes, }, }, }), ); return { directory, runConfig, packageConfig, kubernetes, contextFile }; } test('catalog exposes only reviewed remote clients from the same package', () => { assert.equal(manifest.bin['ql3-cluster-admin'], 'dist/product-cli/cli.js'); assert.equal(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length, 7); assert.equal( new Set(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.map(({ name }) => name)).size, QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length, ); assert.equal( new Set(QINGLONG3_CLUSTER_PRODUCT_COMMANDS.map(({ binary }) => binary)) .size, QINGLONG3_CLUSTER_PRODUCT_COMMANDS.length, ); for (const command of QINGLONG3_CLUSTER_PRODUCT_COMMANDS) { assert.equal(manifest.bin[command.binary], `dist/${command.target}`); assert.equal( fs.lstatSync(path.join(packageRoot, 'dist', command.target)).isFile(), true, ); assert.equal(command.binary.includes('-client'), true); } for (const forbidden of [ 'ql3-cluster-migrate', 'ql3-plugin-package-recover', 'ql3-plugin-package-manage', 'ql3-plugin-package-execute', 'ql3-worker-credential-manage', 'ql3-worker-credential-execute', 'ql3-prompt-output-key-rotate', 'ql3-prompt-output-gc', ]) { assert.equal( QINGLONG3_CLUSTER_PRODUCT_COMMANDS.some( ({ binary }) => binary === forbidden, ), false, ); } }); test('help and version are bounded installation-derived product facts', () => { const help = qingLong3ClusterProductHelp(); assert.match(help, /^Usage: ql3-cluster-admin \[arguments\]/); assert.match(help, /\n run\s+retry or stop Runs/); assert.match(help, /Server, migration, recovery, executor and key-custody/); assert.equal(help.includes('plugin-package-manage'), false); assert.equal( loadQingLong3ClusterProductVersion(moduleDirectory), manifest.version, ); assert.deepEqual(resolveQingLong3ClusterProductCommand([], moduleDirectory), { kind: 'help', output: help, }); assert.deepEqual( resolveQingLong3ClusterProductCommand(['--version'], moduleDirectory), { kind: 'version', output: manifest.version }, ); }); test('resolves only static remote-client targets and preserves opaque arguments', () => { const argv = [ 'run', '--config=/private/client config.json', '--literal=$() && *', ]; const result = resolveQingLong3ClusterProductCommand(argv, moduleDirectory); assert.equal(result.kind, 'invoke'); assert.equal(result.command.binary, 'ql3-run-client'); assert.equal( result.targetFilePath, path.join( packageRoot, 'dist', 'run-management', 'runManagementClientCli.js', ), ); assert.deepEqual(result.argv, argv.slice(1)); assert.equal(Object.isFrozen(result.argv), true); assert.equal(Object.isFrozen(result), true); for (const candidate of [ '../../tmp/owned', '/absolute/command', 'run/../../owned', 'run-manage', 'migrate', 'execute', ]) { const rejected = resolveQingLong3ClusterProductCommand( [candidate, '--help'], moduleDirectory, ); assert.equal(rejected.kind, 'invalid'); assert.equal(rejected.code, 'QL3_CLUSTER_PRODUCT_CLI_USAGE_INVALID'); } }); test('injects only stable paths from an explicit owner-private operator context', (t) => { const fixture = contextFixture(t); const context = loadQingLong3ClusterProductContext(fixture.contextFile); assert.deepEqual(context, { schemaVersion: 1, commands: { run: { configFile: fixture.runConfig }, 'package-kubernetes': { configFile: fixture.packageConfig, kubernetesFile: fixture.kubernetes, }, }, }); assert.equal(Object.isFrozen(context), true); assert.equal(Object.isFrozen(context.commands), true); assert.equal(Object.isFrozen(context.commands.run), true); const run = resolveQingLong3ClusterProductCommand( [ 'run', `--context=${fixture.contextFile}`, '--command=/private/command.json', '--assertion=/private/assertion.jwt', ], moduleDirectory, ); assert.equal(run.kind, 'invoke'); assert.deepEqual(run.argv, [ `--config=${fixture.runConfig}`, '--command=/private/command.json', '--assertion=/private/assertion.jwt', ]); const tunnel = resolveQingLong3ClusterProductContextArguments( fixture.contextFile, 'package-kubernetes', ['--command=/private/command.json', '--assertion=/private/assertion.jwt'], ); assert.deepEqual(tunnel, [ `--config=${fixture.packageConfig}`, `--kubernetes=${fixture.kubernetes}`, '--command=/private/command.json', '--assertion=/private/assertion.jwt', ]); }); test('operator context rejects weak files, unknown or secret fields and argument conflicts', (t) => { const fixture = contextFixture(t); const cases = [ { schemaVersion: 1, commands: {} }, { schemaVersion: 2, commands: { run: { configFile: fixture.runConfig } } }, { schemaVersion: 1, commands: { unknown: { configFile: fixture.runConfig } }, }, { schemaVersion: 1, commands: { run: { configFile: fixture.runConfig, assertionFile: '/private/assertion.jwt', }, }, }, { schemaVersion: 1, commands: { run: { configFile: fixture.runConfig, privateKeyFile: '/private/key' }, }, }, { schemaVersion: 1, commands: { 'package-kubernetes': { configFile: fixture.packageConfig }, }, }, ]; for (const [index, value] of cases.entries()) { const filePath = privateFile( fixture.directory, `invalid-${index}.json`, JSON.stringify(value), ); assert.throws( () => loadQingLong3ClusterProductContext(filePath), /operator context is invalid/, ); } const publicContext = privateFile( fixture.directory, 'public.json', JSON.stringify({ schemaVersion: 1, commands: { run: { configFile: fixture.runConfig } }, }), ); fs.chmodSync(publicContext, 0o644); assert.throws(() => loadQingLong3ClusterProductContext(publicContext)); const symlink = path.join(fixture.directory, 'context-link.json'); fs.symlinkSync(fixture.contextFile, symlink); assert.throws(() => loadQingLong3ClusterProductContext(symlink)); const binaryRejected = runCli([ 'run', `--context=${publicContext}`, '--command=/private/command.json', '--assertion=/private/assertion.jwt', ]); assert.equal(binaryRejected.status, 78); assert.equal(binaryRejected.stdout, ''); assert.deepEqual(JSON.parse(binaryRejected.stderr), { schemaVersion: 1, component: 'qinglong3-cluster-product-cli', code: 'QL3_CLUSTER_PRODUCT_CONTEXT_INVALID', message: 'QingLong 3.0 Cluster operator context is invalid', }); assert.equal(binaryRejected.stderr.includes(fixture.directory), false); assert.throws( () => resolveQingLong3ClusterProductCommand( [ 'approval', `--context=${fixture.contextFile}`, '--command=/private/command.json', '--assertion=/private/assertion.jwt', ], moduleDirectory, ), /operator context is invalid/, ); for (const args of [ ['run', '--context'], ['run', '--context='], [ 'run', `--context=${fixture.contextFile}`, `--context=${fixture.contextFile}`, ], ]) { const result = resolveQingLong3ClusterProductCommand(args, moduleDirectory); assert.equal(result.kind, 'invalid'); } assert.throws( () => resolveQingLong3ClusterProductCommand( ['run', `--context=${fixture.contextFile}`, '--config'], moduleDirectory, ), /operator context is invalid/, ); }); test('rejects symlink targets and package manifests', (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-cluster-product-')); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const fakePackageRoot = path.join(root, 'package'); const fakeModuleDirectory = path.join(fakePackageRoot, 'dist', 'product-cli'); const targetDirectory = path.join(fakePackageRoot, 'dist', 'run-management'); fs.mkdirSync(fakeModuleDirectory, { recursive: true }); fs.mkdirSync(targetDirectory, { recursive: true }); const external = path.join(root, 'external.js'); fs.writeFileSync(external, 'process.exit(0);\n'); fs.symlinkSync( external, path.join(targetDirectory, 'runManagementClientCli.js'), ); assert.throws( () => resolveQingLong3ClusterProductCommand( ['run', '--help'], fakeModuleDirectory, ), /unavailable/, ); fs.writeFileSync( path.join(fakePackageRoot, 'package.real.json'), JSON.stringify({ name: '@qinglong/cluster-admin', version: '3.0.0' }), ); fs.symlinkSync( path.join(fakePackageRoot, 'package.real.json'), path.join(fakePackageRoot, 'package.json'), ); assert.throws( () => loadQingLong3ClusterProductVersion(fakeModuleDirectory), /unavailable/, ); }); test('binary exposes help/version and delegates without a shell', () => { const help = runCli(['--help']); assert.equal(help.status, 0); assert.match(help.stdout, /^Usage: ql3-cluster-admin /); assert.match(help.stdout, /--context=\/absolute\/operator-context\.json/); assert.equal(help.stderr, ''); const version = runCli(['--version']); assert.equal(version.status, 0); assert.equal(version.stdout.trim(), manifest.version); assert.equal(version.stderr, ''); const delegatedHelp = runCli(['run', '--help']); assert.equal(delegatedHelp.status, 0); assert.match(delegatedHelp.stdout, /^Usage: ql3-run-client /); assert.equal(delegatedHelp.stderr, ''); const rejected = runCli(['../../tmp/not-a-command']); assert.equal(rejected.status, 64); assert.equal(rejected.stdout, ''); const failure = JSON.parse(rejected.stderr); assert.equal(failure.code, 'QL3_CLUSTER_PRODUCT_CLI_USAGE_INVALID'); assert.equal(JSON.stringify(failure).includes('/tmp'), false); }); test('forwards only bounded signals and removes handlers', () => { const signalHost = new EventEmitter(); const received = []; const child = { exitCode: null, signalCode: null, kill(signal) { received.push(signal); return true; }, }; const remove = forwardClusterProductSignals(child, signalHost); signalHost.emit('SIGINT'); signalHost.emit('SIGTERM'); signalHost.emit('SIGHUP'); assert.deepEqual(received, ['SIGINT', 'SIGTERM', 'SIGHUP']); assert.equal(clusterProductSignalExitCode('SIGINT'), 130); assert.equal(clusterProductSignalExitCode('SIGTERM'), 143); remove(); signalHost.emit('SIGTERM'); assert.deepEqual(received, ['SIGINT', 'SIGTERM', 'SIGHUP']); child.exitCode = 0; const removeTerminal = forwardClusterProductSignals(child, signalHost); signalHost.emit('SIGTERM'); removeTerminal(); assert.deepEqual(received, ['SIGINT', 'SIGTERM', 'SIGHUP']); });