mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
fix(cli): accept JSON files for web capture and screenshot comparisons (#1397)
compare-web-captures and compare-web-screenshots parsed their arguments as inline JSON only, unlike every other JSON-input command. Screenshot artifacts and scenario captures embed base64 PNG bytes, so a typical viewport screenshot exceeds the command-line length limit (128 KiB per argument on Linux, 1 MiB total on macOS) and could not be compared from the CLI at all, while MCP accepts it. A file path was reported as malformed JSON, and screenshot schema failures returned no issues. Each argument now accepts inline JSON or a JSON file path through parseCliJsonInput; file failures keep its path diagnostics, malformed inline JSON keeps the field issue, and screenshot validation reports projected schema issues. The tests share one CLI JSON process fixture. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2b6e9ec03f
commit
6875d4a64c
6 files changed
+238
-95
No files matched your search
@@ -15,13 +15,14 @@ import {
|
||||
type AnalysisInputIssue,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { browserCaptureComparisonInputSchema } from "../domain/browserCaptureComparison.js";
|
||||
import { projectInputIssues } from "../domain/inputIssueProjection.js";
|
||||
import { discoverWebMcpToolsInputSchema } from "../domain/webMcpDiscovery.js";
|
||||
import {
|
||||
captureWebScreenshotInputSchema,
|
||||
compareWebScreenshotsInputSchema,
|
||||
} from "../domain/webScreenshot.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { safeParseJson } from "../domain/safeJson.js";
|
||||
import { parseCliJsonInput } from "../cliJsonInput.js";
|
||||
import type { Logger } from "pino";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
|
||||
@@ -79,46 +80,39 @@ const registerCaptureDiff = (
|
||||
description:
|
||||
"Compare two normalized passive captures or recorded browser scenarios",
|
||||
args: z.object({
|
||||
beforeJson: z.string().describe("Earlier normalized web capture JSON"),
|
||||
afterJson: z.string().describe("Later normalized web capture JSON"),
|
||||
beforeJson: z
|
||||
.string()
|
||||
.describe("Earlier normalized web capture JSON or JSON file path"),
|
||||
afterJson: z
|
||||
.string()
|
||||
.describe("Later normalized web capture JSON or JSON file path"),
|
||||
}),
|
||||
options: z.object({
|
||||
normalizationJson: z
|
||||
.string()
|
||||
.default('{"rules":[]}')
|
||||
.describe(
|
||||
"Recorded literal normalization policy for browser scenarios",
|
||||
"Recorded literal normalization policy JSON or JSON file path for browser scenarios",
|
||||
),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, "compare-web-captures", async () => {
|
||||
const before = safeParseJson(args.beforeJson);
|
||||
const after = safeParseJson(args.afterJson);
|
||||
const normalization = safeParseJson(options.normalizationJson);
|
||||
if (!before.ok)
|
||||
return inputError("compare_web_captures", [
|
||||
invalidJsonIssue("before"),
|
||||
]);
|
||||
if (!after.ok)
|
||||
return inputError("compare_web_captures", [
|
||||
invalidJsonIssue("after"),
|
||||
]);
|
||||
if (!normalization.ok)
|
||||
return inputError("compare_web_captures", [
|
||||
invalidJsonIssue("normalization"),
|
||||
]);
|
||||
const inputs = await parseJsonArguments("compare_web_captures", {
|
||||
before: args.beforeJson,
|
||||
after: args.afterJson,
|
||||
normalization: options.normalizationJson,
|
||||
});
|
||||
if (!inputs.ok) return inputs.error;
|
||||
const { before, after, normalization } = inputs.value;
|
||||
const scenarioComparison =
|
||||
browserCaptureComparisonInputSchema.safeParse({
|
||||
before_scenario: before.value,
|
||||
after_scenario: after.value,
|
||||
normalization: normalization.value,
|
||||
before_scenario: before,
|
||||
after_scenario: after,
|
||||
normalization,
|
||||
});
|
||||
const parsed = scenarioComparison.success
|
||||
? scenarioComparison
|
||||
: browserCaptureComparisonInputSchema.safeParse({
|
||||
before: before.value,
|
||||
after: after.value,
|
||||
});
|
||||
: browserCaptureComparisonInputSchema.safeParse({ before, after });
|
||||
if (!parsed.success) return inputError("compare_web_captures");
|
||||
const result = await compareWebCaptureEvidence(
|
||||
new CdpBrowserProvider(),
|
||||
@@ -168,8 +162,12 @@ const registerScreenshotDiff = (
|
||||
cli.command(CLI_COMMANDS.compareWebScreenshots, {
|
||||
description: "Compare two self-verifying PNG artifact JSON values",
|
||||
args: z.object({
|
||||
beforeJson: z.string().describe("Earlier screenshot artifact JSON"),
|
||||
afterJson: z.string().describe("Later screenshot artifact JSON"),
|
||||
beforeJson: z
|
||||
.string()
|
||||
.describe("Earlier screenshot artifact JSON or JSON file path"),
|
||||
afterJson: z
|
||||
.string()
|
||||
.describe("Later screenshot artifact JSON or JSON file path"),
|
||||
}),
|
||||
options: z.object({
|
||||
channelThreshold: z
|
||||
@@ -182,12 +180,27 @@ const registerScreenshotDiff = (
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, "compare-web-screenshots", async () => {
|
||||
const parsed = compareWebScreenshotsInputSchema.safeParse({
|
||||
before: parseJson(args.beforeJson),
|
||||
after: parseJson(args.afterJson),
|
||||
channel_threshold: options.channelThreshold,
|
||||
const operation = "compare_web_screenshots";
|
||||
const inputs = await parseJsonArguments(operation, {
|
||||
before: args.beforeJson,
|
||||
after: args.afterJson,
|
||||
});
|
||||
if (!parsed.success) return inputError("compare_web_screenshots");
|
||||
if (!inputs.ok) return inputs.error;
|
||||
const { before, after } = inputs.value;
|
||||
const input = {
|
||||
before,
|
||||
after,
|
||||
channel_threshold: options.channelThreshold,
|
||||
};
|
||||
const parsed = compareWebScreenshotsInputSchema.safeParse(input);
|
||||
if (!parsed.success)
|
||||
return browserCliError(
|
||||
new AnalysisInputError(
|
||||
operation,
|
||||
{ cause: parsed.error },
|
||||
projectInputIssues(parsed.error.issues, input),
|
||||
),
|
||||
);
|
||||
const result = await compareWebScreenshotEvidence(
|
||||
new CdpBrowserProvider(),
|
||||
parsed.data,
|
||||
@@ -197,11 +210,40 @@ const registerScreenshotDiff = (
|
||||
});
|
||||
};
|
||||
|
||||
const parseJson = (value: string): unknown => {
|
||||
const parsed = safeParseJson(value);
|
||||
return parsed.ok ? parsed.value : undefined;
|
||||
/**
|
||||
* Parse each named inline JSON argument or local JSON file, like other
|
||||
* JSON-input commands. Screenshot artifacts and scenario captures embed PNG
|
||||
* bytes that can exceed the host's command-line length limit.
|
||||
*/
|
||||
const parseJsonArguments = async (
|
||||
operation: string,
|
||||
fields: Readonly<Record<string, string>>,
|
||||
): Promise<
|
||||
| { readonly ok: true; readonly value: Readonly<Record<string, unknown>> }
|
||||
| { readonly ok: false; readonly error: JsonValue }
|
||||
> => {
|
||||
const values: Record<string, unknown> = {};
|
||||
for (const [field, value] of Object.entries(fields)) {
|
||||
const parsed = await parseCliJsonInput(value, operation);
|
||||
if (!parsed.ok)
|
||||
return isFileInputFailure(parsed.error)
|
||||
? parsed
|
||||
: {
|
||||
ok: false,
|
||||
error: inputError(operation, [invalidJsonIssue(field)]),
|
||||
};
|
||||
values[field] = parsed.value;
|
||||
}
|
||||
return { ok: true, value: values };
|
||||
};
|
||||
|
||||
/** File read and decode failures name the selected path; inline JSON does not. */
|
||||
const isFileInputFailure = (error: JsonValue): boolean =>
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
!Array.isArray(error) &&
|
||||
"input_path" in error;
|
||||
|
||||
const invalidJsonIssue = (field: string): AnalysisInputIssue => ({
|
||||
path: [field],
|
||||
reason: "invalid_format",
|
||||
|
||||
Reference in new issue
Block a user