diff --git a/scripts/dev/realtime-talk-live-smoke.ts b/scripts/dev/realtime-talk-live-smoke.ts index a3807149a9bb..48d4b166d9ba 100644 --- a/scripts/dev/realtime-talk-live-smoke.ts +++ b/scripts/dev/realtime-talk-live-smoke.ts @@ -26,6 +26,10 @@ const GOOGLE_REALTIME_VOICE = process.env.OPENCLAW_REALTIME_GOOGLE_VOICE?.trim() const GOOGLE_LIVE_WS_URL = "wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContentConstrained"; +type RealtimeSmokeCliOptions = { + help: boolean; +}; + type SmokeResult = { name: string; ok: boolean; @@ -53,6 +57,33 @@ type OpenAIWebRtcSmokeGlobal = typeof globalThis & { openclawReadBoundedRealtimeResponseText?: OpenAIRealtimeBrowserResponseReader; }; +class CliArgumentError extends Error { + override name = "CliArgumentError"; +} + +function usage(): string { + return [ + "Usage: node --import tsx scripts/dev/realtime-talk-live-smoke.ts [options]", + "", + "Options:", + " -h, --help Show this help", + "", + "Environment:", + " OPENAI_API_KEY", + " GEMINI_API_KEY or GOOGLE_API_KEY", + ].join("\n"); +} + +function parseRealtimeSmokeArgs(argv = process.argv.slice(2)): RealtimeSmokeCliOptions { + for (const arg of argv) { + if (arg === "--help" || arg === "-h") { + continue; + } + throw new CliArgumentError(`Unknown argument: ${arg}`); + } + return { help: argv.includes("--help") || argv.includes("-h") }; +} + function getEnv(name: string): string | undefined { const value = process.env[name]?.trim(); return value ? value : undefined; @@ -729,7 +760,12 @@ try { } } -async function main(): Promise { +async function main(argv = process.argv.slice(2)): Promise { + const cli = parseRealtimeSmokeArgs(argv); + if (cli.help) { + console.log(usage()); + return; + } const openAIKey = getEnv("OPENAI_API_KEY"); const googleKey = getEnv("GEMINI_API_KEY") ?? getEnv("GOOGLE_API_KEY"); const browser = await chromium.launch({ @@ -781,7 +817,7 @@ async function main(): Promise { if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { await main().catch((error: unknown) => { - console.error(shortError(error)); + console.error(error instanceof CliArgumentError ? error.message : shortError(error)); process.exitCode = 1; }); } @@ -789,9 +825,11 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { export const testing = { OPENAI_HTTP_RESPONSE_MAX_BYTES, createOpenAIClientSecret, + parseRealtimeSmokeArgs, readOpenAIRealtimeBrowserResponseText, readBoundedText, resolveOpenAIHttpTimeoutMs, + usage, }; function toLintErrorObject(value: unknown, fallbackMessage: string): Error { diff --git a/scripts/perf/summarize-cpuprofile.mjs b/scripts/perf/summarize-cpuprofile.mjs index 5e84545b9435..df9b48a92d67 100644 --- a/scripts/perf/summarize-cpuprofile.mjs +++ b/scripts/perf/summarize-cpuprofile.mjs @@ -8,6 +8,22 @@ import { parsePositiveInt } from "../lib/numeric-options.mjs"; const DEFAULT_LIMIT = 30; +export function usage() { + return "Usage: scripts/perf/summarize-cpuprofile.mjs [--limit N] "; +} + +export function shouldPrintHelp(argv) { + for (const arg of argv) { + if (arg === "--") { + return false; + } + if (arg === "--help" || arg === "-h") { + return true; + } + } + return false; +} + /** * Parses CPU profile file paths and --limit. */ @@ -24,6 +40,13 @@ export function parseArgs(argv) { limit = parsePositiveInt(arg.slice("--limit=".length), "--limit"); continue; } + if (arg === "--") { + files.push(...argv.slice(index + 1)); + break; + } + if (arg.startsWith("-")) { + throw new Error(`Unknown option: ${arg}`); + } files.push(arg); } return { files, limit }; @@ -125,6 +148,10 @@ export function summarizeProfile(file, limit) { } function main() { + if (shouldPrintHelp(process.argv.slice(2))) { + console.log(usage()); + return; + } let options; try { options = parseArgs(process.argv.slice(2)); @@ -133,7 +160,7 @@ function main() { process.exit(1); } if (options.files.length === 0) { - console.error("usage: scripts/perf/summarize-cpuprofile.mjs [--limit N] "); + console.error(usage()); process.exit(2); } try { diff --git a/test/scripts/dev-tooling-safety.test.ts b/test/scripts/dev-tooling-safety.test.ts index 51191d41a21a..9b3d40a21aff 100644 --- a/test/scripts/dev-tooling-safety.test.ts +++ b/test/scripts/dev-tooling-safety.test.ts @@ -399,6 +399,44 @@ describe("script-specific dev tooling hardening", () => { ); }); + it("prints OpenAI realtime smoke help without launching live checks", () => { + expect(realtimeSmokeTesting.parseRealtimeSmokeArgs(["--help"])).toEqual({ help: true }); + + const result = spawnSync( + process.execPath, + ["--import", "tsx", "scripts/dev/realtime-talk-live-smoke.ts", "--help"], + { + cwd: process.cwd(), + encoding: "utf8", + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain( + "Usage: node --import tsx scripts/dev/realtime-talk-live-smoke.ts", + ); + expect(result.stderr).toBe(""); + }); + + it("rejects unknown OpenAI realtime smoke args before launching live checks", () => { + expect(() => realtimeSmokeTesting.parseRealtimeSmokeArgs(["--wat"])).toThrow( + "Unknown argument: --wat", + ); + + const result = spawnSync( + process.execPath, + ["--import", "tsx", "scripts/dev/realtime-talk-live-smoke.ts", "--wat"], + { + cwd: process.cwd(), + encoding: "utf8", + }, + ); + + expect(result.status).toBe(1); + expect(result.stdout).toBe(""); + expect(result.stderr.trim()).toBe("Unknown argument: --wat"); + }); + it("bounds OpenAI realtime smoke response body reads by content-length", async () => { const maxBytes = realtimeSmokeTesting.OPENAI_HTTP_RESPONSE_MAX_BYTES; const response = new Response("{}", { diff --git a/test/scripts/summarize-cpuprofile.test.ts b/test/scripts/summarize-cpuprofile.test.ts index 9afcc543d2d7..d683f4bfa528 100644 --- a/test/scripts/summarize-cpuprofile.test.ts +++ b/test/scripts/summarize-cpuprofile.test.ts @@ -4,7 +4,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { parseArgs } from "../../scripts/perf/summarize-cpuprofile.mjs"; +import { parseArgs, shouldPrintHelp } from "../../scripts/perf/summarize-cpuprofile.mjs"; describe("scripts/perf/summarize-cpuprofile.mjs", () => { it("parses split and inline positive limit flags", () => { @@ -16,6 +16,28 @@ describe("scripts/perf/summarize-cpuprofile.mjs", () => { files: ["a.cpuprofile", "b.cpuprofile"], limit: 7, }); + expect(parseArgs(["--limit", "5", "--", "--dash.cpuprofile"])).toEqual({ + files: ["--dash.cpuprofile"], + limit: 5, + }); + }); + + it("prints help without treating it as a profile path", () => { + expect(shouldPrintHelp(["--help"])).toBe(true); + expect(shouldPrintHelp(["--", "--help"])).toBe(false); + + const result = spawnSync( + process.execPath, + ["scripts/perf/summarize-cpuprofile.mjs", "--help"], + { + cwd: process.cwd(), + encoding: "utf8", + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("Usage: scripts/perf/summarize-cpuprofile.mjs"); + expect(result.stderr).toBe(""); }); it("rejects malformed limit flags instead of falling back", () => { @@ -29,6 +51,19 @@ describe("scripts/perf/summarize-cpuprofile.mjs", () => { } }); + it("rejects unknown options instead of treating them as profile paths", () => { + expect(() => parseArgs(["--wat"])).toThrow("Unknown option: --wat"); + + const result = spawnSync(process.execPath, ["scripts/perf/summarize-cpuprofile.mjs", "--wat"], { + cwd: process.cwd(), + encoding: "utf8", + }); + + expect(result.status).toBe(1); + expect(result.stdout).toBe(""); + expect(result.stderr.trim()).toBe("Unknown option: --wat"); + }); + it("rejects empty CPU profiles instead of printing zero-sample summaries", () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cpuprofile-")); const profilePath = path.join(tempDir, "empty.cpuprofile");