From 7dca9210c93df06ff96eda10df6150fb4d3a3fcc Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 20 Jun 2026 02:27:50 +0200 Subject: [PATCH] fix(test): guard dev smoke cli args --- scripts/dev/gateway-smoke.ts | 79 ++++++++++++++++--- scripts/dev/ios-node-e2e.ts | 50 +++++++++++- scripts/dev/test-device-pair-telegram.ts | 58 +++++++++++++- .../qa-lab/runtime/gateway-smoke.e2e.test.ts | 40 ++++++++++ test/scripts/ios-node-e2e.test.ts | 39 +++++++++ .../scripts/test-device-pair-telegram.test.ts | 33 +++++++- 6 files changed, 281 insertions(+), 18 deletions(-) diff --git a/scripts/dev/gateway-smoke.ts b/scripts/dev/gateway-smoke.ts index 8dd92626010..c15f78df8fe 100644 --- a/scripts/dev/gateway-smoke.ts +++ b/scripts/dev/gateway-smoke.ts @@ -15,13 +15,15 @@ function writeStderrLine(message: string): void { } function writeUsage(): void { - writeStderrLine( - "Usage: bun scripts/dev/gateway-smoke.ts --url --token \n" + - "Or set env: OPENCLAW_GATEWAY_URL / OPENCLAW_GATEWAY_TOKEN", - ); + writeStderrLine(usage()); } type GatewaySmokeClient = ReturnType; +type GatewaySmokeCliOptions = { + help: boolean; + token?: string; + urlRaw?: string; +}; type GatewaySmokeDeps = { createClient?: typeof createGatewayWsClient; @@ -29,6 +31,54 @@ type GatewaySmokeDeps = { stdout?: (message: string) => void; }; +class GatewaySmokeArgError extends Error {} + +const BOOLEAN_FLAGS = new Set(["--help", "-h"]); +const VALUE_FLAGS = new Set(["--url", "--token"]); + +function usage(): string { + return [ + "Usage: bun scripts/dev/gateway-smoke.ts --url --token ", + "Or set env: OPENCLAW_GATEWAY_URL / OPENCLAW_GATEWAY_TOKEN", + "", + "Options:", + " --url Gateway websocket URL", + " --token Gateway auth token", + " -h, --help Show this help", + ].join("\n"); +} + +function validateArgs(argv: readonly string[]): void { + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index] ?? ""; + if (BOOLEAN_FLAGS.has(arg)) { + continue; + } + if (VALUE_FLAGS.has(arg)) { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) { + throw new GatewaySmokeArgError(`${arg} requires a value`); + } + index += 1; + continue; + } + throw new GatewaySmokeArgError(`Unknown argument: ${arg}`); + } +} + +function parseGatewaySmokeCli( + argv = process.argv.slice(2), + env: NodeJS.ProcessEnv = process.env, +): GatewaySmokeCliOptions { + validateArgs(argv); + const { get: getArg, has } = createArgReader([...argv]); + return { + help: has("--help") || has("-h"), + token: getArg("--token") ?? env.OPENCLAW_GATEWAY_TOKEN, + urlRaw: getArg("--url") ?? env.OPENCLAW_GATEWAY_URL, + }; +} + function isRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } @@ -158,14 +208,25 @@ export async function runGatewaySmoke( } if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { - const { get: getArg } = createArgReader(); - const urlRaw = getArg("--url") ?? process.env.OPENCLAW_GATEWAY_URL; - const token = getArg("--token") ?? process.env.OPENCLAW_GATEWAY_TOKEN; + let cli: GatewaySmokeCliOptions; + try { + cli = parseGatewaySmokeCli(); + } catch (error) { + writeStderrLine(error instanceof Error ? error.message : String(error)); + process.exit(1); + } - if (!urlRaw || !token) { + if (cli.help) { + writeStdoutLine(usage()); + } else if (!cli.urlRaw || !cli.token) { writeUsage(); process.exitCode = 1; } else { - process.exitCode = await runGatewaySmoke({ token, urlRaw }); + process.exitCode = await runGatewaySmoke({ token: cli.token, urlRaw: cli.urlRaw }); } } + +export const testing = { + parseGatewaySmokeCli, + usage, +}; diff --git a/scripts/dev/ios-node-e2e.ts b/scripts/dev/ios-node-e2e.ts index 5f22605f1d7..2027a6c2279 100644 --- a/scripts/dev/ios-node-e2e.ts +++ b/scripts/dev/ios-node-e2e.ts @@ -17,12 +17,57 @@ function writeStderrLine(message: string): void { process.stderr.write(`${message}\n`); } +function usage(): string { + return [ + "Usage: bun scripts/dev/ios-node-e2e.ts --url --token [options]", + "Or set env: OPENCLAW_GATEWAY_URL / OPENCLAW_GATEWAY_TOKEN", + "", + "Options:", + " --node Select a connected iOS node", + " --wait-seconds Seconds to wait for an iOS node (default: 25)", + " --dangerous Include camera/screen commands", + " --json Print JSON results", + " -h, --help Show this help", + ].join("\n"); +} + const argv = process.argv.slice(2); const getArg = (flag: string) => { const index = argv.indexOf(flag); return index === -1 ? undefined : argv[index + 1]; }; const hasFlag = (flag: string) => argv.includes(flag); +const BOOLEAN_FLAGS = new Set(["--dangerous", "--help", "-h", "--json"]); +const VALUE_FLAGS = new Set(["--node", "--token", "--url", "--wait-seconds"]); + +function failCli(message: string): never { + writeStderrLine(message); + process.exit(1); +} + +function validateArgs(): void { + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index] ?? ""; + if (BOOLEAN_FLAGS.has(arg)) { + continue; + } + if (VALUE_FLAGS.has(arg)) { + const value = argv[index + 1]; + if (!value || value.startsWith("--")) { + failCli(`${arg} requires a value`); + } + index += 1; + continue; + } + failCli(`Unknown argument: ${arg}`); + } +} + +if (hasFlag("--help") || hasFlag("-h")) { + writeStdoutLine(usage()); + process.exit(0); +} +validateArgs(); type NodeListPayload = { ts?: number; @@ -46,10 +91,7 @@ const dangerous = hasFlag("--dangerous") || process.env.OPENCLAW_RUN_DANGEROUS = const jsonOut = hasFlag("--json"); if (!urlRaw || !token) { - writeStderrLine( - "Usage: bun scripts/dev/ios-node-e2e.ts --url --token [--node ] [--dangerous] [--json]\n" + - "Or set env: OPENCLAW_GATEWAY_URL / OPENCLAW_GATEWAY_TOKEN", - ); + writeStderrLine(usage()); process.exit(1); } diff --git a/scripts/dev/test-device-pair-telegram.ts b/scripts/dev/test-device-pair-telegram.ts index d78eb8e6635..675328370c9 100644 --- a/scripts/dev/test-device-pair-telegram.ts +++ b/scripts/dev/test-device-pair-telegram.ts @@ -32,6 +32,17 @@ class UsageError extends Error { readonly exitCode = 1; } +class CliArgumentError extends UsageError {} + +type DevicePairTelegramArgs = { + accountId?: string; + chatId?: string; + help: boolean; +}; + +const BOOLEAN_FLAGS = new Set(["--help", "-h"]); +const VALUE_FLAGS = new Set(["--account", "-a", "--chat", "-c"]); + function writeStdoutLine(...parts: string[]): void { process.stdout.write(`${parts.join(" ")}\n`); } @@ -57,9 +68,41 @@ function readArg(args: string[], flag: string, short?: string): string | undefin function usage(): string { return [ "Usage: bun scripts/dev/test-device-pair-telegram.ts --chat [--account ]", + "", + "Options:", + " --chat, -c Telegram chat id", + " --account, -a Telegram account id", + " -h, --help Show this help", ].join("\n"); } +function validateArgs(args: readonly string[]): void { + for (let index = 0; index < args.length; index += 1) { + const arg = args[index] ?? ""; + if (BOOLEAN_FLAGS.has(arg)) { + continue; + } + if (VALUE_FLAGS.has(arg)) { + const value = args[index + 1]; + if (!value || value.startsWith("--")) { + throw new CliArgumentError(`${arg} requires a value`); + } + index += 1; + continue; + } + throw new CliArgumentError(`Unknown argument: ${arg}`); + } +} + +function parseDevicePairTelegramArgs(args: readonly string[]): DevicePairTelegramArgs { + validateArgs(args); + return { + accountId: readArg([...args], "--account", "-a"), + chatId: readArg([...args], "--chat", "-c"), + help: args.includes("--help") || args.includes("-h"), + }; +} + async function loadTelegramRuntimeSendMessage(): Promise { const specifier = "../../extensions/telegram/runtime-api.js"; const runtime = (await import(specifier)) as { sendMessageTelegram?: SendMessageTelegram }; @@ -86,8 +129,10 @@ async function runDevicePairTelegram( args = process.argv.slice(2), deps: DevicePairTelegramDeps = createDefaultDeps(), ): Promise { - const chatId = readArg(args, "--chat", "-c"); - const accountId = readArg(args, "--account", "-a"); + const { accountId, chatId, help } = parseDevicePairTelegramArgs(args); + if (help) { + throw new UsageError(usage()); + } if (!chatId) { throw new UsageError(usage()); } @@ -133,7 +178,12 @@ async function runDevicePairTelegram( async function main(): Promise { try { - const result = await runDevicePairTelegram(); + const args = process.argv.slice(2); + if (args.includes("--help") || args.includes("-h")) { + writeStdoutLine(usage()); + return; + } + const result = await runDevicePairTelegram(args); writeStdoutLine( "Sent split /pair messages to", result.chatId, @@ -150,4 +200,4 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { await main(); } -export { runDevicePairTelegram }; +export { parseDevicePairTelegramArgs, runDevicePairTelegram }; diff --git a/test/e2e/qa-lab/runtime/gateway-smoke.e2e.test.ts b/test/e2e/qa-lab/runtime/gateway-smoke.e2e.test.ts index 6fbd6ebbe35..9cdc06c351a 100644 --- a/test/e2e/qa-lab/runtime/gateway-smoke.e2e.test.ts +++ b/test/e2e/qa-lab/runtime/gateway-smoke.e2e.test.ts @@ -1,4 +1,5 @@ // Gateway Smoke tests cover QA Lab gateway smoke evidence. +import { spawnSync } from "node:child_process"; import { createServer, type Server } from "node:http"; import { afterEach, describe, expect, it } from "vitest"; import { WebSocket, WebSocketServer } from "ws"; @@ -163,6 +164,45 @@ describe("gateway-smoke", () => { }; } + it("prints CLI help without connecting", () => { + const result = spawnSync( + process.execPath, + ["--import", "tsx", "scripts/dev/gateway-smoke.ts", "--help"], + { + cwd: process.cwd(), + encoding: "utf8", + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("Usage: bun scripts/dev/gateway-smoke.ts"); + expect(result.stderr).toBe(""); + }); + + it("rejects unknown CLI args before connecting", () => { + const result = spawnSync( + process.execPath, + [ + "--import", + "tsx", + "scripts/dev/gateway-smoke.ts", + "--url", + "ws://127.0.0.1:9", + "--token", + "token", + "--wat", + ], + { + cwd: process.cwd(), + encoding: "utf8", + }, + ); + + expect(result.status).toBe(1); + expect(result.stdout).toBe(""); + expect(result.stderr.trim()).toBe("Unknown argument: --wat"); + }); + it("passes against a loopback gateway websocket using the real client", async () => { const stdout: string[] = []; const stderr: string[] = []; diff --git a/test/scripts/ios-node-e2e.test.ts b/test/scripts/ios-node-e2e.test.ts index 8a93a21285e..2762cdf24d2 100644 --- a/test/scripts/ios-node-e2e.test.ts +++ b/test/scripts/ios-node-e2e.test.ts @@ -216,7 +216,46 @@ function runScript(url: string, extraArgs: readonly string[] = []): Promise { + return new Promise((resolve) => { + const child = spawn( + process.execPath, + ["--import", "tsx", "scripts/dev/ios-node-e2e.ts", ...args], + { + stdio: "pipe", + }, + ); + const stdout = createBoundedChildOutput(); + const stderr = createBoundedChildOutput(); + child.stdout.on("data", (chunk) => { + stdout.append(chunk); + }); + child.stderr.on("data", (chunk) => { + stderr.append(chunk); + }); + child.on("close", (status, signal) => { + resolve({ status, signal, stdout: stdout.text(), stderr: stderr.text(), timedOut: false }); + }); + }); +} + describe("ios-node-e2e", () => { + it("prints CLI help without connecting", async () => { + const result = await runScriptRaw(["--help"]); + + expect(result).toMatchObject({ signal: null, status: 0, timedOut: false }); + expect(result.stdout).toContain("Usage: bun scripts/dev/ios-node-e2e.ts"); + expect(result.stderr).toBe(""); + }); + + it("rejects unknown CLI args before connecting", async () => { + const result = await runScript("ws://127.0.0.1:9", ["--wat"]); + + expect(result).toMatchObject({ signal: null, status: 1, timedOut: false }); + expect(result.stderr.trim()).toBe("Unknown argument: --wat"); + expect(result.stdout).toBe(""); + }); + it("rejects malformed wait seconds before connecting", async () => { const result = await runScript("ws://127.0.0.1:9", ["--wait-seconds", "1e3"]); diff --git a/test/scripts/test-device-pair-telegram.test.ts b/test/scripts/test-device-pair-telegram.test.ts index c2085573ad0..bdf06559b2f 100644 --- a/test/scripts/test-device-pair-telegram.test.ts +++ b/test/scripts/test-device-pair-telegram.test.ts @@ -1,7 +1,10 @@ // Test Device Pair Telegram tests cover the dev Telegram pairing smoke helper. import { pathToFileURL } from "node:url"; import { describe, expect, it, vi } from "vitest"; -import { runDevicePairTelegram } from "../../scripts/dev/test-device-pair-telegram.ts"; +import { + parseDevicePairTelegramArgs, + runDevicePairTelegram, +} from "../../scripts/dev/test-device-pair-telegram.ts"; const scriptUrl = pathToFileURL("scripts/dev/test-device-pair-telegram.ts").href; @@ -10,6 +13,34 @@ describe("scripts/dev/test-device-pair-telegram.ts", () => { await expect(import(`${scriptUrl}?case=load-${Date.now()}`)).resolves.toBeDefined(); }); + it("parses help without requiring Telegram config", () => { + expect(parseDevicePairTelegramArgs(["--help"])).toEqual({ + accountId: undefined, + chatId: undefined, + help: true, + }); + }); + + it("rejects unknown args before loading OpenClaw plugins", async () => { + const cfg = { channels: { telegram: { enabled: true } } }; + const loadOpenClawPlugins = vi.fn(); + const executePluginCommand = vi.fn(); + const sendMessageTelegram = vi.fn(); + + await expect( + runDevicePairTelegram(["--chat", "chat-123", "--wat"], { + executePluginCommand, + getRuntimeConfig: () => cfg, + loadOpenClawPlugins, + matchPluginCommand: () => ({ args: "from-match", command: { name: "pair" } as never }), + sendMessageTelegram, + }), + ).rejects.toThrow("Unknown argument: --wat"); + expect(loadOpenClawPlugins).not.toHaveBeenCalled(); + expect(executePluginCommand).not.toHaveBeenCalled(); + expect(sendMessageTelegram).not.toHaveBeenCalled(); + }); + it("sends the generated /pair reply through the injected Telegram runtime", async () => { const cfg = { channels: { telegram: { enabled: true } } }; const loadOpenClawPlugins = vi.fn();