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.
This commit is contained in:
No BS AI 2026-10-12 11:29:07 -07:00 committed by GitHub
parent fd985dbc5b
commit 916ac97bf9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 247 additions and 23 deletions

View File

@ -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);
}),

View File

@ -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<typeof projectAnalysisError>[0],
): JsonValue => ({

View File

@ -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",
},
],
});
},
);
});