diff --git a/extensions/qa-lab/src/cli.test.ts b/extensions/qa-lab/src/cli.test.ts index 1533a6b2a845..e9ed4e43fff3 100644 --- a/extensions/qa-lab/src/cli.test.ts +++ b/extensions/qa-lab/src/cli.test.ts @@ -800,6 +800,33 @@ describe("qa cli registration", () => { await expect(invalidProgram.parseAsync(["node", "openclaw", ...args])).rejects.toThrow(message); }); + it.each([ + [["qa", "ui", "--port", "65536"], "--port must be a TCP port between 1 and 65535."], + [ + ["qa", "ui", "--advertise-port", "999999"], + "--advertise-port must be a TCP port between 1 and 65535.", + ], + [ + ["qa", "docker-scaffold", "--output-dir", "/tmp/qa", "--gateway-port", "65536"], + "--gateway-port must be a TCP port between 1 and 65535.", + ], + [ + ["qa", "up", "--qa-lab-port", "65536"], + "--qa-lab-port must be a TCP port between 1 and 65535.", + ], + [["qa", "aimock", "--port", "65536"], "--port must be a TCP port between 1 and 65535."], + ])("rejects out-of-range QA port option %j", async (args, message) => { + const invalidProgram = new Command(); + invalidProgram.exitOverride(); + invalidProgram.configureOutput({ + writeErr: () => {}, + writeOut: () => {}, + }); + registerQaLabCli(invalidProgram); + + await expect(invalidProgram.parseAsync(["node", "openclaw", ...args])).rejects.toThrow(message); + }); + it("shows an enable hint when a discovered runner plugin is installed but blocked", async () => { listQaRunnerCliContributions.mockReset().mockReturnValue([createBlockedQaRunnerContribution()]); const blockedProgram = new Command(); diff --git a/extensions/qa-lab/src/cli.ts b/extensions/qa-lab/src/cli.ts index 575866d18e70..c1277074b99c 100644 --- a/extensions/qa-lab/src/cli.ts +++ b/extensions/qa-lab/src/cli.ts @@ -62,6 +62,7 @@ const QA_RUN_PROFILE_ONLY_OPTIONS = [ ] as const; const QA_RUN_SELF_CHECK_ONLY_OPTIONS = [{ optionName: "output", flag: "--output" }] as const; +const MAX_QA_CLI_TCP_PORT = 65_535; type QaSuiteCliOptions = QaScenarioRunCliOptions & { channelDriver?: QaSuiteCommandOptions["channelDriver"]; @@ -105,6 +106,14 @@ function parseQaCliPositiveIntegerOption(value: string, flag: string): number { return parsed; } +function parseQaCliTcpPortOption(value: string, flag: string): number { + const parsed = parseQaCliPositiveIntegerOption(value, flag); + if (parsed > MAX_QA_CLI_TCP_PORT) { + throw invalidQaCliArgument(`${flag} must be a TCP port between 1 and 65535.`); + } + return parsed; +} + function parseQaEvidenceModeOption(value: string): QaProfileCommandOptions["evidenceMode"] { const evidenceMode = value.trim(); if (evidenceMode === "full" || evidenceMode === "slim") { @@ -867,11 +876,11 @@ export function registerQaLabCli(program: Command) { .option("--repo-root ", "Repository root to target when running from a neutral cwd") .option("--host ", "Bind host", "127.0.0.1") .option("--port ", "Bind port", (value: string) => - parseQaCliPositiveIntegerOption(value, "--port"), + parseQaCliTcpPortOption(value, "--port"), ) .option("--advertise-host ", "Optional public host to advertise in bootstrap payloads") .option("--advertise-port ", "Optional public port to advertise", (value: string) => - parseQaCliPositiveIntegerOption(value, "--advertise-port"), + parseQaCliTcpPortOption(value, "--advertise-port"), ) .option("--control-ui-url ", "Optional Control UI URL to embed beside the QA panel") .option( @@ -909,10 +918,10 @@ export function registerQaLabCli(program: Command) { .option("--repo-root ", "Repository root to target when running from a neutral cwd") .requiredOption("--output-dir ", "Output directory for docker-compose + state files") .option("--gateway-port ", "Gateway host port", (value: string) => - parseQaCliPositiveIntegerOption(value, "--gateway-port"), + parseQaCliTcpPortOption(value, "--gateway-port"), ) .option("--qa-lab-port ", "QA lab host port", (value: string) => - parseQaCliPositiveIntegerOption(value, "--qa-lab-port"), + parseQaCliTcpPortOption(value, "--qa-lab-port"), ) .option("--provider-base-url ", "Provider base URL for the QA gateway") .option("--image ", "Prebaked image name", "openclaw:qa-local-prebaked") @@ -950,10 +959,10 @@ export function registerQaLabCli(program: Command) { .option("--repo-root ", "Repository root to target when running from a neutral cwd") .option("--output-dir ", "Output directory for docker-compose + state files") .option("--gateway-port ", "Gateway host port", (value: string) => - parseQaCliPositiveIntegerOption(value, "--gateway-port"), + parseQaCliTcpPortOption(value, "--gateway-port"), ) .option("--qa-lab-port ", "QA lab host port", (value: string) => - parseQaCliPositiveIntegerOption(value, "--qa-lab-port"), + parseQaCliTcpPortOption(value, "--qa-lab-port"), ) .option("--provider-base-url ", "Provider base URL for the QA gateway") .option("--image ", "Image tag", "openclaw:qa-local-prebaked") @@ -985,7 +994,7 @@ export function registerQaLabCli(program: Command) { .description(providerCommand.description) .option("--host ", "Bind host", "127.0.0.1") .option("--port ", "Bind port", (value: string) => - parseQaCliPositiveIntegerOption(value, "--port"), + parseQaCliTcpPortOption(value, "--port"), ) .action(async (opts: { host?: string; port?: number }) => { await runQaProviderServer(providerCommand.providerMode, opts);