diff --git a/scripts/bench-sqlite-state.ts b/scripts/bench-sqlite-state.ts index 43d678fb2c7..0da0750e4b9 100644 --- a/scripts/bench-sqlite-state.ts +++ b/scripts/bench-sqlite-state.ts @@ -110,20 +110,45 @@ type CliOptions = { stateDir: string | null; }; -function parseFlagValue(flag: string): string | undefined { - const index = process.argv.indexOf(flag); +const BOOLEAN_FLAGS = new Set(["--help"]); +const VALUE_FLAGS = new Set(["--output", "--profile", "--state-dir"]); + +class CliUsageError extends Error { + override name = "CliUsageError"; +} + +function parseFlagValue(flag: string, argv: string[]): string | undefined { + const index = argv.indexOf(flag); if (index === -1) { return undefined; } - const value = process.argv[index + 1]; + const value = argv[index + 1]; if (!value || value.startsWith("--")) { - throw new Error(`${flag} requires a value`); + throw new CliUsageError(`${flag} requires a value`); } return value; } -function hasFlag(flag: string): boolean { - return process.argv.includes(flag); +function hasFlag(flag: string, argv = process.argv.slice(2)): boolean { + return argv.includes(flag); +} + +function validateArgs(argv: 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 CliUsageError(`${arg} requires a value`); + } + index += 1; + continue; + } + throw new CliUsageError(`Unknown argument: ${arg}`); + } } function parseProfile(raw: string | undefined): ProfileId { @@ -133,14 +158,17 @@ function parseProfile(raw: string | undefined): ProfileId { if (raw === "smoke" || raw === "default" || raw === "large") { return raw; } - throw new Error(`--profile must be one of smoke, default, large; got ${JSON.stringify(raw)}`); + throw new CliUsageError( + `--profile must be one of smoke, default, large; got ${JSON.stringify(raw)}`, + ); } -function parseOptions(): CliOptions { +function parseOptions(argv = process.argv.slice(2)): CliOptions { + validateArgs(argv); return { - output: parseFlagValue("--output") ?? null, - profile: parseProfile(parseFlagValue("--profile")), - stateDir: parseFlagValue("--state-dir") ?? null, + output: parseFlagValue("--output", argv) ?? null, + profile: parseProfile(parseFlagValue("--profile", argv)), + stateDir: parseFlagValue("--state-dir", argv) ?? null, }; } @@ -535,11 +563,13 @@ function printProofLines(report: BenchmarkReport): void { } function main(): void { - if (hasFlag("--help")) { + const argv = process.argv.slice(2); + validateArgs(argv); + if (hasFlag("--help", argv)) { printUsage(); return; } - const options = parseOptions(); + const options = parseOptions(argv); const config = applyScale(PROFILES[options.profile]); const stateDir = options.stateDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-perf-")); @@ -626,5 +656,13 @@ function main(): void { } if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { - main(); + try { + main(); + } catch (error) { + if (error instanceof CliUsageError) { + console.error(`error: ${error.message}`); + process.exit(2); + } + throw error; + } } diff --git a/test/scripts/bench-sqlite-state.test.ts b/test/scripts/bench-sqlite-state.test.ts new file mode 100644 index 00000000000..2d8086ee33f --- /dev/null +++ b/test/scripts/bench-sqlite-state.test.ts @@ -0,0 +1,42 @@ +// Bench SQLite State tests cover benchmark CLI argument safety. +import { spawnSync } from "node:child_process"; +import { describe, expect, it } from "vitest"; + +function runBench(args: string[]) { + return spawnSync( + process.execPath, + ["--import", "tsx", "scripts/bench-sqlite-state.ts", ...args], + { + cwd: process.cwd(), + encoding: "utf8", + }, + ); +} + +describe("scripts/bench-sqlite-state", () => { + it("rejects unknown args before seeding benchmark databases", () => { + const result = runBench(["--wat"]); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr.trim()).toBe("error: Unknown argument: --wat"); + }); + + it("rejects missing output values before seeding benchmark databases", () => { + const result = runBench(["--output", "--profile", "smoke"]); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr.trim()).toBe("error: --output requires a value"); + }); + + it("rejects invalid profiles without printing a stack trace", () => { + const result = runBench(["--profile", "huge"]); + + expect(result.status).toBe(2); + expect(result.stdout).toBe(""); + expect(result.stderr.trim()).toBe( + 'error: --profile must be one of smoke, default, large; got "huge"', + ); + }); +});