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:
parent
fd985dbc5b
commit
916ac97bf9
|
|
@ -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);
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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,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",
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
Loading…
Reference in New Issue