diff --git a/scripts/perf/issue-78851-model-resolution.ts b/scripts/perf/issue-78851-model-resolution.ts index 6b504dd479bc..781122a551fe 100644 --- a/scripts/perf/issue-78851-model-resolution.ts +++ b/scripts/perf/issue-78851-model-resolution.ts @@ -26,6 +26,23 @@ type Options = { warmup: number; }; +const BOOLEAN_FLAGS = new Set(["--help", "-h", "--json", "--keep-temp", "--runtime-hooks"]); +const VALUE_FLAGS = new Set([ + "--agents", + "--cpu-prof-dir", + "--cpu-prof-output", + "--lookups", + "--models-per-provider", + "--output", + "--providers", + "--runs", + "--warmup", +]); + +class CliArgumentError extends Error { + override name = "CliArgumentError"; +} + type PhaseSample = { ensureMs: number; resolveMs: number; @@ -73,7 +90,11 @@ function parseFlagValue(flag: string): string | undefined { if (index === -1) { return undefined; } - return process.argv[index + 1]; + const value = process.argv[index + 1]; + if (!value || value.startsWith("--")) { + throw new CliArgumentError(`${flag} requires a value`); + } + return value; } function hasFlag(flag: string): boolean { @@ -85,9 +106,12 @@ function parsePositiveInt(flag: string, fallback: number): number { if (!raw) { return fallback; } - const value = Number.parseInt(raw, 10); + const value = Number(raw); if (!Number.isFinite(value) || value <= 0) { - throw new Error(`${flag} must be a positive integer`); + throw new CliArgumentError(`${flag} must be a positive integer`); + } + if (!Number.isInteger(value)) { + throw new CliArgumentError(`${flag} must be a positive integer`); } return value; } @@ -97,14 +121,32 @@ function parseNonNegativeInt(flag: string, fallback: number): number { if (!raw) { return fallback; } - const value = Number.parseInt(raw, 10); + const value = Number(raw); if (!Number.isFinite(value) || value < 0) { - throw new Error(`${flag} must be a non-negative integer`); + throw new CliArgumentError(`${flag} must be a non-negative integer`); + } + if (!Number.isInteger(value)) { + throw new CliArgumentError(`${flag} must be a non-negative integer`); } return value; } +function validateCliArgs(args = process.argv.slice(2)): 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)) { + index += 1; + continue; + } + throw new CliArgumentError(`Unknown argument: ${arg}`); + } +} + function parseOptions(): Options { + validateCliArgs(); return { agentCount: parsePositiveInt("--agents", 8), cpuProfDir: parseFlagValue("--cpu-prof-dir"), @@ -494,6 +536,10 @@ async function main(): Promise { } main().catch((error: unknown) => { + if (error instanceof CliArgumentError) { + process.stderr.write(`${error.message}\n`); + process.exit(1); + } const message = error instanceof Error ? (error.stack ?? error.message) : String(error); process.stderr.write(`${message}\n`); process.exit(1); diff --git a/test/scripts/issue-78851-model-resolution.test.ts b/test/scripts/issue-78851-model-resolution.test.ts new file mode 100644 index 000000000000..18a5118af5de --- /dev/null +++ b/test/scripts/issue-78851-model-resolution.test.ts @@ -0,0 +1,43 @@ +// Issue 78851 profiler CLI tests cover argument handling before work starts. +import { spawnSync } from "node:child_process"; +import { describe, expect, it } from "vitest"; + +function runProfiler(...args: string[]) { + return spawnSync( + process.execPath, + ["--import", "tsx", "scripts/perf/issue-78851-model-resolution.ts", ...args], + { + cwd: process.cwd(), + encoding: "utf8", + }, + ); +} + +describe("issue 78851 model resolution profiler CLI", () => { + it("prints help without starting the profiler", () => { + const result = runProfiler("--help"); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("OpenClaw issue #78851 model-resolution profiler"); + expect(result.stdout).toContain( + "node --import tsx scripts/perf/issue-78851-model-resolution.ts [options]", + ); + expect(result.stderr).toBe(""); + }); + + it("rejects unknown arguments before starting the profiler", () => { + const result = runProfiler("--wat"); + + expect(result.status).toBe(1); + expect(result.stdout).toBe(""); + expect(result.stderr.trim()).toBe("Unknown argument: --wat"); + }); + + it("rejects partial numeric arguments before starting the profiler", () => { + const result = runProfiler("--providers", "48junk"); + + expect(result.status).toBe(1); + expect(result.stdout).toBe(""); + expect(result.stderr.trim()).toBe("--providers must be a positive integer"); + }); +});