Repository navigation
refactor(cli): return effect/Result from argument and config validation - #173
sunwrobert wants to merge 2 commits into
Conversation
|
I'll fix CI failures and address comments from users with write access that start with 'Devin'.
Original prompt from Devin Bot
|
There was a problem hiding this comment.
✅ Approved. Expected CLI validation failures now flow through typed CliError Results to a single reporting boundary. No actionable regressions found.
Scope and evidence
Reviewed both changed files (src/cli/index.ts and src/cli/index.test.ts, 461 additions / 209 deletions) at 9d0b5919e1ab4a2e4a8981d658ecbf214b7f7adc against the actual-base diff for base c6a05aa324948d4fd66b89c2c7e8b9a3065ee01a. Two independent correctness passes covered both files, with additional checks of error paths, security, callers, dependencies and reuse. Examined Result short-circuiting, validation order, formatted failures, config construction and the async CLI reporting boundary.
Risk
The principal compatibility change is the Result-returning parser/config-builder API; the repository callers are updated with it. Expected input failures deliberately lose the uncaught-exception stack trace. No benchmark implementation or configuration schema changes are introduced. Static inspection found no defect in the successful-config or validation-failure paths.
Verification and CI
This was a static review: no PR code, tests, installs or builds were run, and the author's smoke-test claims were not independently reproduced. Inspected the converted validation assertions and added failure-kind cases. At the reviewed head, validate, CodeQL and both analysis checks were successful; the commit-status API also reported success.
throw-to-result: src/cli/index.ts (vendored in openrouter-web as
packages/bench-harness/src/cli/index.ts)remaining: 13 → 0 in this module (
git grep -E "^\s*throw new Error\(" -- src/cli/index.ts | grep -vE "// invariant:" | wc -l); openrouter-web inventorygit grep -E "^\s*throw new Error\(" -- 'packages/**/*.ts' 'services/**/*.ts' ':!*.test.ts' ':!*.integration.test.ts' | grep -vE "// invariant:" | wc -lis 1298 on main and becomes 1285 once this lands upstream andscripts/subtree-pull-bench-harness.shvendors it (the subtree is read-only in the monorepo, so the fix has to land here first).TL;DR
Every expected user-input / environment failure in the Kepler CLI (
src/cli/index.ts) is now aCliErrorliteral-union value carried in aneffect/Resultinstead of athrow new Error(msg);main()renders it once withformatCliError, writeserror: <msg>to stderr and exits 1, so the user-visible message line and exit code are unchanged.Convention choice:
effect/Result(Result.fail/Result.succeed/Result.gen/Result.isFailure), the package's existing convention (src/internal/zod.ts,src/internal/result.ts,src/internal/testing.ts);@openrouter-monorepo/lib-resultis not a dependency of this standalone repo.What changed?
type CliError— onekindper distinct failure, carrying the interpolated data, never the message string:formatCliError(error): string— exhaustiveswitchending inerror satisfies never; produces the exact message text eachthrowproduced before.validateImageDetail,validateCostTier,validateReasoningEffort,requireModel,resolveApiKey,resolveSessionId,parseSolverConfig,buildSchemaValidatedConfig,buildBenchmarkConfig,parseArgsnow returnResult.Result<T, CliError>;parseArgs,buildSchemaValidatedConfigand the newprepareRuncompose withResult.genso the first failure short-circuits.parseSolverConfigJSON,BenchmarkRunConfigSchema,DracoPanelConfigSchema) now propagate theZodError/ parse cause inside theCliErrorinstead of re-throwing its message.main()isasync;prepareRun(argv)collects args, benchmark, API key, session id and solver config;reportCliErroris the single boundary (stderr+process.exitCode = 1), the same pattern the existingBenchmark failed:branch already uses.main()insrc/cli/index.ts;src/cli/index.test.ts. No other importer ofparseArgs/buildBenchmarkConfigexists (git grep -n "from \"../cli\"\|cli/index"→ tests only).// invariant:markers added: none.src/cli/index.test.ts: everytoThrowcase is now anassertFailure+expect(failure).toEqual({ kind, ... })/formatCliErrorassertion with the same inputs; new cases forinvalid_image_detail,model_required,unsupported_benchmark,invalid_draco_panel_config(every kind reachable through the exported functions now has a test;unknown_benchmark,api_key_missing,unsafe_session_id,invalid_solver_config_jsonare exercised by the CLI smoke below since they readprocess.env/ the registry inmain()).Why?
Code-scan finding
sfind-0fd69853fe6b48d5829462cec63a8681(throw-to-result): these 13 sites are expected failures, not invariants, and the module already had Result values in hand that it converted back into throws.How to test
Expected: each prints exactly one
error: <same message as on main>line to stderr and exits 1.Benchmark impact
None: no solver, scorer, prompt, dataset or config-schema change; successful invocations build the identical
BenchmarkRunConfig.Reviewer focus
error:line; theerror: <message>line and exit status 1 are byte-identical for all 10 smoke cases (base-vs-head diff ofexit=+error:lines is empty).--modelrequired before unknown-option / schema checks).src/cli/index.ts(solver-config forwarding,num()TypeError); it does not carry this marker and is a different change, but will conflict textually with whichever lands second.runBenchmarkById, notmain()); worst case is a wrong message text for one failure kind, caught by the per-kind tests.Show me
index.test.tson base source: 0 pass / 1 fail (compile error —parseArgson base is not a Result), so every converted assertion is proven to depend on the change; on head: 29 pass.bun run format:check,bun run check(no findings insrc/cli/; pre-existing warnings elsewhere unchanged),bun run typecheck,bun test(1833 pass),bun run build: green.Proof matrix
CliErrorkind reachable viaparseArgs/buildBenchmarkConfig(9 kinds)index.test.tsper-kindassertFailure+toEqual({ kind, ... }); base fails (compile), head passesunknown_benchmark,api_key_missing,unsafe_session_id,invalid_solver_config_json(env / registry, insidemain())process.env; same shape as the 9 above)assertSuccess, pass on headThermo-nuclear fix loop
2 passes. Pass 1 findings fixed: explicit
isFailure/fail(x.failure)chains replaced byResult.geninparseArgs,prepareRun,buildSchemaValidatedConfig;NonNullable<ReturnType<typeof getBenchmark>>replaced by theBenchmarktype;error.errorfield renamedcause;failCli→reportCliError;main()madeasync; test helpers typed withCliArgs/BenchmarkRunConfiginstead ofReturnType<…>["success"]. Pass 2: clean. deslop-micro: no invented terminology left after pass 1 renames (prepareRun,RunSetup,reportCliError,CliError,formatCliError).Visualization
Checklist
parseArgs/buildBenchmarkConfigreturnResult; only consumers aremain()and the colocated test)Link to Devin session: https://openrouter.devinenterprise.com/sessions/4e5e881590874d60a3a5cf4a79a9be6e
Open in Devin Desktop: https://openrouter.devinenterprise.com/desktop/session/4e5e881590874d60a3a5cf4a79a9be6e?variant=devin
Requested by: @sunwrobert