From 916ac97bf9f7b976982e438ceb856a9692576cb1 Mon Sep 17 00:00:00 2001 From: No BS AI <144746926+369Apps@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:29:07 -0700 Subject: [PATCH] fix(cli): retain browser and Inspector input correction details (#2230) Reuse the shared safe issue projection for all six CLI validation boundaries and cover malformed inputs, credential safety, and parser diagnostics. Fixes morluto/rea#2180 Developed with assistance from OpenAI Codex. --- src/cli/browserCommands.ts | 52 +++-- src/cliJavaScriptRuntimeCommands.ts | 33 +++- .../cli/browserInputDiagnostics.test.ts | 185 ++++++++++++++++++ 3 files changed, 247 insertions(+), 23 deletions(-) create mode 100644 tests/boundary/cli/browserInputDiagnostics.test.ts diff --git a/src/cli/browserCommands.ts b/src/cli/browserCommands.ts index 876b4565a..aae6714d2 100644 --- a/src/cli/browserCommands.ts +++ b/src/cli/browserCommands.ts @@ -14,7 +14,7 @@ import { } from "../domain/browserObservation.js"; import { analyzeWebBundleInputSchema } from "../domain/webBundleAnalysis.js"; import { observeWebSessionInputSchema } from "../domain/browserSession.js"; -import { AnalysisInputError } from "../domain/analysisErrorCore.js"; +import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; import type { Logger } from "pino"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import { @@ -48,13 +48,18 @@ const registerTargetList = ( run: ({ args, options }) => logCliCommand(logger, "list-browser-targets", async () => { const context = browserContext(); - const parsed = listBrowserTargetsInputSchema.safeParse({ + const input = { cdp_endpoint: args.endpoint, allowed_origins: options.allowedOrigins, - }); + }; + const parsed = listBrowserTargetsInputSchema.safeParse(input); if (!parsed.success) return browserCliError( - new AnalysisInputError("list_browser_targets"), + analysisInputErrorFromIssues( + "list_browser_targets", + parsed.error.issues, + input, + ), ); const result = await listBrowserTargets(context.provider, parsed.data); return result.ok ? result.value : browserCliError(result.error); @@ -77,7 +82,7 @@ const registerPageInspection = ( run: ({ args, options }) => logCliCommand(logger, "inspect-web-page", async () => { const context = browserContext(); - const parsed = inspectWebPageInputSchema.safeParse({ + const input = { cdp_endpoint: args.endpoint, allowed_origins: options.allowedOrigins, target_id: args.targetId, @@ -89,9 +94,16 @@ const registerPageInspection = ( include_script_sources: options.includeScriptSources, include_storage_keys: options.includeStorageKeys, include_storage_fingerprints: options.includeStorageFingerprints, - }); + }; + const parsed = inspectWebPageInputSchema.safeParse(input); if (!parsed.success) - return browserCliError(new AnalysisInputError("inspect_web_page")); + return browserCliError( + analysisInputErrorFromIssues( + "inspect_web_page", + parsed.error.issues, + input, + ), + ); const result = await inspectWebPage(context.provider, parsed.data); return result.ok ? result.value : browserCliError(result.error); }), @@ -120,16 +132,23 @@ const registerBundleAnalysis = ( run: ({ args, options }) => logCliCommand(logger, "analyze-web-bundle", async () => { const context = browserContext(); - const parsed = analyzeWebBundleInputSchema.safeParse({ + const input = { cdp_endpoint: args.endpoint, allowed_origins: options.allowedOrigins, target_id: args.targetId, observation_ms: options.observationMs, include_script_sources: true, fetch_source_maps: options.fetchSourceMaps, - }); + }; + const parsed = analyzeWebBundleInputSchema.safeParse(input); if (!parsed.success) - return browserCliError(new AnalysisInputError("analyze_web_bundle")); + return browserCliError( + analysisInputErrorFromIssues( + "analyze_web_bundle", + parsed.error.issues, + input, + ), + ); const result = await analyzeWebBundle(context.provider, parsed.data); return result.ok ? result.value : browserCliError(result.error); }), @@ -153,14 +172,21 @@ const registerObservationSession = ( run: ({ args, options }) => logCliCommand(logger, "observe-web-session", async () => { const context = browserContext(); - const parsed = observeWebSessionInputSchema.safeParse({ + const input = { cdp_endpoint: args.endpoint, allowed_origins: options.allowedOrigins, target_id: args.targetId, observation_ms: options.observationMs, - }); + }; + const parsed = observeWebSessionInputSchema.safeParse(input); if (!parsed.success) - return browserCliError(new AnalysisInputError("observe_web_session")); + return browserCliError( + analysisInputErrorFromIssues( + "observe_web_session", + parsed.error.issues, + input, + ), + ); const result = await observeWebSession(context.provider, parsed.data); return result.ok ? result.value : browserCliError(result.error); }), diff --git a/src/cliJavaScriptRuntimeCommands.ts b/src/cliJavaScriptRuntimeCommands.ts index 5ee6f23e7..23ff1e40e 100644 --- a/src/cliJavaScriptRuntimeCommands.ts +++ b/src/cliJavaScriptRuntimeCommands.ts @@ -12,7 +12,7 @@ import { listJavaScriptRuntimeTargetsInputSchema, observeJavaScriptRuntimeInputSchema, } from "./domain/javascript/javascriptRuntimeObservation.js"; -import { AnalysisInputError } from "./domain/analysisErrorCore.js"; +import { analysisInputErrorFromIssues } from "./domain/inputIssueProjection.js"; import { projectAnalysisError } from "./domain/analysisErrorProjection.js"; import type { JsonValue } from "./domain/jsonValue.js"; import type { Logger } from "pino"; @@ -47,11 +47,19 @@ export const registerJavaScriptRuntimeObservationCommands = ( CLI_COMMANDS.listJavaScriptRuntimeTargets, async () => { const context = runtimeContext(); - const parsed = listJavaScriptRuntimeTargetsInputSchema.safeParse({ + const input = { inspector_endpoint: args.endpoint, - }); + }; + const parsed = + listJavaScriptRuntimeTargetsInputSchema.safeParse(input); if (!parsed.success) - return inputError("list_javascript_runtime_targets"); + return cliError( + analysisInputErrorFromIssues( + "list_javascript_runtime_targets", + parsed.error.issues, + input, + ), + ); const result = await listJavaScriptRuntimeTargets( context.provider, parsed.data, @@ -73,13 +81,21 @@ export const registerJavaScriptRuntimeObservationCommands = ( run: ({ args, options }) => logCliCommand(logger, CLI_COMMANDS.observeJavaScriptRuntime, async () => { const context = runtimeContext(); - const parsed = observeJavaScriptRuntimeInputSchema.safeParse({ + const input = { inspector_endpoint: args.endpoint, target_id: args.targetId, runtime_kind: options.runtimeKind, observation_ms: options.observationMs, - }); - if (!parsed.success) return inputError("observe_javascript_runtime"); + }; + const parsed = observeJavaScriptRuntimeInputSchema.safeParse(input); + if (!parsed.success) + return cliError( + analysisInputErrorFromIssues( + "observe_javascript_runtime", + parsed.error.issues, + input, + ), + ); const result = await observeJavaScriptRuntime( context.provider, parsed.data, @@ -93,9 +109,6 @@ const runtimeContext = () => ({ provider: createJavaScriptRuntimeObservationProvider(), }); -const inputError = (operation: string): JsonValue => - cliError(new AnalysisInputError(operation)); - const cliError = ( error: Parameters[0], ): JsonValue => ({ diff --git a/tests/boundary/cli/browserInputDiagnostics.test.ts b/tests/boundary/cli/browserInputDiagnostics.test.ts new file mode 100644 index 000000000..e6b1646ae --- /dev/null +++ b/tests/boundary/cli/browserInputDiagnostics.test.ts @@ -0,0 +1,185 @@ +import { describe, expect } from "vitest"; + +import { workspaceCliTest as cliTest } from "../../support/cli/workspaceCliFixture.js"; + +const malformedEndpoint = "not a url"; +const browserEndpoint = "http://127.0.0.1:9222"; +const inspectorEndpoint = "http://127.0.0.1:9229"; + +const invalidInputs = [ + { + command: "list-browser-targets", + arguments: [malformedEndpoint], + operation: "list_browser_targets", + error: "Browser observation failed", + issue: { + path: ["cdp_endpoint"], + reason: "invalid_value", + message: "Invalid CDP endpoint URL", + }, + }, + { + command: "inspect-web-page", + arguments: [browserEndpoint, ""], + operation: "inspect_web_page", + error: "Browser observation failed", + issue: { path: ["target_id"], reason: "out_of_range", minimum: 1 }, + }, + { + command: "analyze-web-bundle", + arguments: [malformedEndpoint, "target"], + operation: "analyze_web_bundle", + error: "Browser observation failed", + issue: { + path: ["cdp_endpoint"], + reason: "invalid_value", + message: "Invalid CDP endpoint URL", + }, + }, + { + command: "observe-web-session", + arguments: [browserEndpoint, ""], + operation: "observe_web_session", + error: "Browser observation failed", + issue: { path: ["target_id"], reason: "out_of_range", minimum: 1 }, + }, + { + command: "list-javascript-runtime-targets", + arguments: [""], + operation: "list_javascript_runtime_targets", + error: "JavaScript runtime observation failed", + issue: { + path: ["inspector_endpoint"], + reason: "out_of_range", + minimum: 1, + }, + }, + { + command: "observe-javascript-runtime", + arguments: [inspectorEndpoint, ""], + operation: "observe_javascript_runtime", + error: "JavaScript runtime observation failed", + issue: { path: ["target_id"], reason: "out_of_range", minimum: 1 }, + }, +] as const; + +describe("browser and Inspector CLI input diagnostics", () => { + cliTest.for(invalidInputs)( + "$command retains the named contract's correction details", + async (input, { cli, workspace }) => { + const result = await cli.run({ + arguments: [input.command, ...input.arguments, "--json"], + cwd: workspace.root, + environment: { ...workspace.environment, REA_LOG_LEVEL: "silent" }, + }); + + expect(result.exitCode).toBe(1); + expect(result.json).toMatchObject({ + error: input.error, + code: "invalid_request", + category: "invalid_input", + message: + "Analysis input is invalid. Check the arguments and try again.", + retryable: true, + remediation: { action: "Correct the listed arguments and retry." }, + details: { + operation: input.operation, + issues: expect.arrayContaining([input.issue]), + }, + }); + expect(result.stdout).not.toContain(malformedEndpoint); + expect(result.stderr).not.toContain(malformedEndpoint); + }, + ); + + cliTest.for([ + ["list-browser-targets", "cdp_endpoint"], + ["list-javascript-runtime-targets", "inspector_endpoint"], + ] as const)( + "%s does not echo rejected endpoint credentials", + async ([command, field], { cli, workspace }) => { + const endpoint = + "http://rea-diagnostic-user:rea-diagnostic-secret@127.0.0.1:9222/"; + const result = await cli.run({ + arguments: [command, endpoint, "--json"], + cwd: workspace.root, + environment: { ...workspace.environment, REA_LOG_LEVEL: "debug" }, + }); + + expect(result.exitCode).toBe(1); + expect(result.json).toMatchObject({ + code: "invalid_request", + details: { + issues: [ + { + path: [field], + reason: "invalid_value", + message: + "CDP endpoint must be an explicit-port HTTP URL on 127.0.0.1 or ::1", + }, + ], + }, + }); + const output = result.stdout + result.stderr; + expect(output).not.toContain(endpoint); + expect(output).not.toContain("rea-diagnostic-user"); + expect(output).not.toContain("rea-diagnostic-secret"); + }, + ); + + cliTest( + "retains authored cross-field correction guidance", + async ({ cli, workspace }) => { + const result = await cli.run({ + arguments: [ + "inspect-web-page", + browserEndpoint, + "target", + "--include-storage-fingerprints", + "--json", + ], + cwd: workspace.root, + environment: { ...workspace.environment, REA_LOG_LEVEL: "silent" }, + }); + + expect(result.exitCode).toBe(1); + expect(result.json).toMatchObject({ + code: "invalid_request", + details: { + operation: "inspect_web_page", + issues: expect.arrayContaining([ + { + path: ["include_storage_fingerprints"], + reason: "invalid_value", + message: "Storage fingerprints require storage key capture", + }, + ]), + }, + }); + }, + ); + + cliTest( + "preserves the parser diagnostic for an omitted endpoint", + async ({ cli, workspace }) => { + const result = await cli.run({ + arguments: ["list-browser-targets", "--json"], + cwd: workspace.root, + environment: { ...workspace.environment, REA_LOG_LEVEL: "silent" }, + }); + + expect(result.exitCode).toBe(1); + expect(result.json).toMatchObject({ + code: "VALIDATION_ERROR", + fieldErrors: [ + { + path: "endpoint", + code: "invalid_type", + missing: true, + expected: "string", + }, + ], + }); + }, + ); +});