Compare commits

...
Author SHA1 Message Date
morlutoandLouisChen c9320992c5 fix(cli): retain correction details across browser commands
Fold the complementary implementation from #2247 into the earlier #2230 proposal, retaining the original contribution history.

Co-authored-by: LouisChen <3361008+chqcjq@users.noreply.github.com>
2026-10-13 02:11:17 +08:00
Mukharjee Vemulapalli 05ba40e3cb fix(cli): retain browser and Inspector input correction details
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.
2026-10-12 08:31:22 -07:00
4 changed files with 308 additions and 30 deletions
+21 -7
View File
@@ -59,13 +59,18 @@ const registerWebMcp = (
run: ({ args, options }) =>
logCliCommand(logger, "discover-webmcp-tools", async () => {
const context = browserContext();
const parsed = discoverWebMcpToolsInputSchema.safeParse({
const input = {
cdp_endpoint: args.endpoint,
allowed_origins: options.allowedOrigins,
target_id: args.targetId,
observation_ms: options.observationMs,
});
if (!parsed.success) return inputError("discover_webmcp_tools");
};
const parsed = discoverWebMcpToolsInputSchema.safeParse(input);
if (!parsed.success)
return inputError(
"discover_webmcp_tools",
projectInputIssues(parsed.error.issues, input),
);
const result = await discoverWebMcpTools(context.provider, parsed.data);
return result.ok ? result.value : browserCliError(result.error);
}),
@@ -113,7 +118,11 @@ const registerCaptureDiff = (
const parsed = scenarioComparison.success
? scenarioComparison
: browserCaptureComparisonInputSchema.safeParse({ before, after });
if (!parsed.success) return inputError("compare_web_captures");
if (!parsed.success)
return inputError(
"compare_web_captures",
projectInputIssues(parsed.error.issues, { before, after }),
);
const result = await compareWebCaptureEvidence(
new CdpBrowserProvider(),
parsed.data,
@@ -140,12 +149,17 @@ const registerScreenshot = (
run: ({ args, options }) =>
logCliCommand(logger, "capture-web-screenshot", async () => {
const context = browserContext();
const parsed = captureWebScreenshotInputSchema.safeParse({
const input = {
cdp_endpoint: args.endpoint,
allowed_origins: options.allowedOrigins,
target_id: args.targetId,
});
if (!parsed.success) return inputError("capture_web_screenshot");
};
const parsed = captureWebScreenshotInputSchema.safeParse(input);
if (!parsed.success)
return inputError(
"capture_web_screenshot",
projectInputIssues(parsed.error.issues, input),
);
const result = await captureWebScreenshot(
context.provider,
parsed.data,
+39 -13
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);
}),
+23 -10
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 => ({
@@ -0,0 +1,225 @@
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 },
},
{
command: "discover-webmcp-tools",
arguments: [malformedEndpoint, "target"],
operation: "discover_webmcp_tools",
error: "Browser observation failed",
issue: {
path: ["cdp_endpoint"],
reason: "invalid_value",
message: "Invalid CDP endpoint URL",
},
},
{
command: "capture-web-screenshot",
arguments: [browserEndpoint, ""],
operation: "capture_web_screenshot",
error: "Browser 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(
"retains correction details for invalid capture comparison inputs",
async ({ cli, workspace }) => {
const result = await cli.run({
arguments: ["compare-web-captures", "{}", "{}", "--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: "compare_web_captures",
issues: expect.arrayContaining([
expect.objectContaining({ reason: expect.any(String) }),
]),
},
});
},
);
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",
},
],
});
},
);
});