diff --git a/scripts/bench-sqlite-state.ts b/scripts/bench-sqlite-state.ts index de14885d7706..63f713870f0f 100644 --- a/scripts/bench-sqlite-state.ts +++ b/scripts/bench-sqlite-state.ts @@ -14,8 +14,11 @@ import { openOpenClawStateDatabase, } from "../src/state/openclaw-state-db.js"; import { parseStrictIntegerOption } from "./lib/dev-tooling-safety.ts"; - -type ProfileId = "smoke" | "default" | "large"; +import { + CliUsageError, + parseSqliteStateBenchmarkCli, + type ProfileId, +} from "./lib/sqlite-state-benchmark-cli.js"; type ProfileConfig = { agentCacheEntries: number; @@ -105,79 +108,6 @@ const PROFILES: Record = { }, }; -type CliOptions = { - output: string | null; - profile: ProfileId; - stateDir: string | null; -}; - -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 = argv[index + 1]; - if (!value || value.startsWith("-")) { - throw new CliUsageError(`${flag} requires a value`); - } - return value; -} - -function hasFlag(flag: string, argv = process.argv.slice(2)): boolean { - return argv.includes(flag); -} - -function validateArgs(argv: string[]): void { - const seenValueFlags = new Set(); - for (let index = 0; index < argv.length; index += 1) { - const arg = argv[index] ?? ""; - if (BOOLEAN_FLAGS.has(arg)) { - continue; - } - if (VALUE_FLAGS.has(arg)) { - if (seenValueFlags.has(arg)) { - throw new CliUsageError(`${arg} was provided more than once`); - } - seenValueFlags.add(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 { - if (!raw) { - return "default"; - } - if (raw === "smoke" || raw === "default" || raw === "large") { - return raw; - } - throw new CliUsageError( - `--profile must be one of smoke, default, large; got ${JSON.stringify(raw)}`, - ); -} - -function parseOptions(argv = process.argv.slice(2)): CliOptions { - validateArgs(argv); - return { - output: parseFlagValue("--output", argv) ?? null, - profile: parseProfile(parseFlagValue("--profile", argv)), - stateDir: parseFlagValue("--state-dir", argv) ?? null, - }; -} - function applyScale(config: ProfileConfig): ProfileConfig { const scale = parseStrictIntegerOption({ fallback: 1, @@ -570,12 +500,12 @@ function printProofLines(report: BenchmarkReport): void { function main(): void { const argv = process.argv.slice(2); - validateArgs(argv); - if (hasFlag("--help", argv)) { + const cli = parseSqliteStateBenchmarkCli(argv); + if (cli.help) { printUsage(); return; } - const options = parseOptions(argv); + const { options } = cli; const config = applyScale(PROFILES[options.profile]); const stateDir = options.stateDir ?? fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-perf-")); diff --git a/scripts/lib/sqlite-state-benchmark-cli.ts b/scripts/lib/sqlite-state-benchmark-cli.ts new file mode 100644 index 000000000000..2ba48cd53216 --- /dev/null +++ b/scripts/lib/sqlite-state-benchmark-cli.ts @@ -0,0 +1,77 @@ +export type ProfileId = "smoke" | "default" | "large"; + +type CliOptions = { + output: string | null; + profile: ProfileId; + stateDir: string | null; +}; + +const BOOLEAN_FLAGS = new Set(["--help"]); +const VALUE_FLAGS = new Set(["--output", "--profile", "--state-dir"]); + +export 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 = argv[index + 1]; + if (!value || value.startsWith("-")) { + throw new CliUsageError(`${flag} requires a value`); + } + return value; +} + +function validateArgs(argv: string[]): void { + const seenValueFlags = new Set(); + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index] ?? ""; + if (BOOLEAN_FLAGS.has(arg)) { + continue; + } + if (!VALUE_FLAGS.has(arg)) { + throw new CliUsageError(`Unknown argument: ${arg}`); + } + if (seenValueFlags.has(arg)) { + throw new CliUsageError(`${arg} was provided more than once`); + } + seenValueFlags.add(arg); + const value = argv[index + 1]; + if (!value || value.startsWith("-")) { + throw new CliUsageError(`${arg} requires a value`); + } + index += 1; + } +} + +function parseProfile(raw: string | undefined): ProfileId { + if (!raw) { + return "default"; + } + if (raw === "smoke" || raw === "default" || raw === "large") { + return raw; + } + throw new CliUsageError( + `--profile must be one of smoke, default, large; got ${JSON.stringify(raw)}`, + ); +} + +export function parseSqliteStateBenchmarkCli( + argv: string[], +): { help: true } | { help: false; options: CliOptions } { + validateArgs(argv); + if (argv.includes("--help")) { + return { help: true }; + } + return { + help: false, + options: { + output: parseFlagValue("--output", argv) ?? null, + profile: parseProfile(parseFlagValue("--profile", argv)), + stateDir: parseFlagValue("--state-dir", argv) ?? null, + }, + }; +} diff --git a/test/scripts/bench-sqlite-state.test.ts b/test/scripts/bench-sqlite-state.test.ts index 5d1c38838eee..84c44bb163d1 100644 --- a/test/scripts/bench-sqlite-state.test.ts +++ b/test/scripts/bench-sqlite-state.test.ts @@ -1,6 +1,7 @@ // Bench SQLite State tests cover benchmark CLI argument safety. import { spawnSync } from "node:child_process"; import { describe, expect, it } from "vitest"; +import { parseSqliteStateBenchmarkCli } from "../../scripts/lib/sqlite-state-benchmark-cli.js"; function runBench(args: string[]) { return spawnSync( @@ -23,36 +24,29 @@ describe("scripts/bench-sqlite-state", () => { }); 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"); + expect(() => parseSqliteStateBenchmarkCli(["--output", "--profile", "smoke"])).toThrow( + "--output requires a value", + ); }); it("rejects short flag output values before seeding benchmark databases", () => { - const result = runBench(["--output", "-h"]); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(""); - expect(result.stderr.trim()).toBe("error: --output requires a value"); + expect(() => parseSqliteStateBenchmarkCli(["--output", "-h"])).toThrow( + "--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"', + expect(() => parseSqliteStateBenchmarkCli(["--profile", "huge"])).toThrow( + '--profile must be one of smoke, default, large; got "huge"', ); }); it("rejects duplicate single-value controls before seeding benchmark databases", () => { - const result = runBench(["--profile", "smoke", "--profile", "large"]); - - expect(result.status).toBe(2); - expect(result.stdout).toBe(""); - expect(result.stderr.trim()).toBe("error: --profile was provided more than once"); + expect(() => + parseSqliteStateBenchmarkCli(["--profile", "smoke", "--profile", "large"]), + ).toThrow("--profile was provided more than once"); + expect(parseSqliteStateBenchmarkCli(["--help", "--profile", "huge"])).toEqual({ + help: true, + }); }); });