mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(cli): advertise domain constraints for selector and path options (#9181)
* fix(cli): advertise domain constraints for selector and path options Android class, method, and mapping fields, ADB setting keys, and browser endpoints used bare strings in --schema. They now reuse the domain fields, so empty selectors, relative mapping paths, and malformed setting keys fail in the advertised schema. * fix(cli): keep selector limits when argument parsing rejects them Empty browser and Electron selectors now fail in the CLI parser, so their diagnostics are VALIDATION_ERROR with the one-character minimum. ADB setting keys stay on the selector repair path, which restores maximum 256 and the identifier pattern. * test(cli): assert empty-selector codes without frozen prose Empty browser and Electron selectors still fail as VALIDATION_ERROR too_small on the named field. The tests no longer copy Zod's sentence. * test(cli): compare endpoint diagnostics with the domain schema Browser and Inspector tests take custom issue text from the schema that owns it, and empty capture comparison must name both missing inspection fields. * fix(cli): remove duplicate merged schema import --------- Co-authored-by: Cursor Agent <[email protected]>
This commit is contained in:
1 parent
562779bd71
commit
b31fc5d5fb
13 files changed
+418
-125
No files matched your search
@@ -237,10 +237,8 @@ export const registerAdbCommands = (
|
||||
description: "Read one settings value from system, secure, or global",
|
||||
args: z.object({
|
||||
serial,
|
||||
namespace: z
|
||||
.enum(["system", "secure", "global"])
|
||||
.describe("Settings namespace to read"),
|
||||
key: z.string().min(1).describe("Setting key to read"),
|
||||
namespace: adbInputSchemas.read_adb_setting.shape.namespace,
|
||||
key: adbInputSchemas.read_adb_setting.shape.key,
|
||||
}),
|
||||
run: ({ args }) =>
|
||||
execute(CLI_COMMANDS.readAdbSetting, "read_adb_setting", {
|
||||
|
||||
+12
-20
@@ -21,6 +21,7 @@ import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
||||
import { parseAnalysisInput } from "../application/AnalysisInput.js";
|
||||
import { androidJniSelectionSchema } from "../domain/android/androidJniBindings.js";
|
||||
import { androidJniDeliverySchema } from "../domain/android/androidJniQuery.js";
|
||||
import { androidInputSchemas } from "../domain/android/androidAnalysis.js";
|
||||
import { cliIntegerArgument } from "./options.js";
|
||||
|
||||
/** Expose the same explicit APK requests through one-shot CLI commands. */
|
||||
@@ -66,14 +67,10 @@ export const registerAndroidCommands = (
|
||||
).then(projectCliAnalysisResult);
|
||||
}),
|
||||
);
|
||||
const path = z.string().describe("Local standalone APK path");
|
||||
const className = z.string().describe("Exact fully qualified class name");
|
||||
const mappingPath = z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Absolute local R8/ProGuard mapping.txt; names are annotated, never replaced",
|
||||
);
|
||||
const path = androidInputSchemas.inspect_android_package.shape.path;
|
||||
const className = androidInputSchemas.inspect_android_class.shape.class_name;
|
||||
const mappingPath =
|
||||
androidInputSchemas.inspect_android_class.shape.mapping_path;
|
||||
const mappingInput = (path: string | undefined) =>
|
||||
path === undefined ? {} : { mapping_path: path };
|
||||
cli.command(CLI_COMMANDS.inspectAndroidJniBindings, {
|
||||
@@ -195,11 +192,7 @@ export const registerAndroidCommands = (
|
||||
description: "Search APK class names; empty query inventories all classes",
|
||||
args: z.object({
|
||||
path,
|
||||
query: z
|
||||
.string()
|
||||
.describe(
|
||||
"Case-sensitive class-name substring; empty lists all classes",
|
||||
),
|
||||
query: androidInputSchemas.search_android_classes.shape.query,
|
||||
}),
|
||||
run: ({ args }) =>
|
||||
execute(
|
||||
@@ -231,7 +224,10 @@ export const registerAndroidCommands = (
|
||||
args: z.object({
|
||||
path,
|
||||
className,
|
||||
methodName: z.string().describe("Exact declared method name"),
|
||||
methodName:
|
||||
androidInputSchemas.inspect_android_method.shape.method_name.describe(
|
||||
"Exact declared method name",
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
overloadIndex: cliIntegerArgument(0)
|
||||
@@ -264,12 +260,8 @@ export const registerAndroidCommands = (
|
||||
"Trace APK using nodes for a defined class/method or an externally invoked DEX method",
|
||||
args: z.object({ path, className }),
|
||||
options: z.object({
|
||||
methodName: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Defined or directly referenced external method; omit for APK class references",
|
||||
),
|
||||
methodName:
|
||||
androidInputSchemas.trace_android_references.shape.method_name,
|
||||
overloadIndex: cliIntegerArgument(0)
|
||||
.optional()
|
||||
.describe(
|
||||
|
||||
@@ -25,7 +25,8 @@ const isAdbSelector = (command: string, field: string): boolean =>
|
||||
(["pull-adb-file", "push-adb-file"].includes(command) &&
|
||||
field === "device-path") ||
|
||||
(command === "start-adb-activity" &&
|
||||
["action", "component", "data-uri"].includes(field));
|
||||
["action", "component", "data-uri"].includes(field)) ||
|
||||
(command === "read-adb-setting" && field === "key");
|
||||
|
||||
/** Preserve parser correction fields without embedding its serialized cause. */
|
||||
export const presentCliArgumentErrors = (cli: CliInstance): void => {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { parseCliJsonInput } from "../cliJsonInput.js";
|
||||
import type { Logger } from "pino";
|
||||
import { cliDomainInput } from "./domainInput.js";
|
||||
import { cliDecimalField } from "./options.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
@@ -57,10 +58,12 @@ const registerWebMcp = (
|
||||
cli.command(CLI_COMMANDS.discoverWebMcpTools, {
|
||||
description: "Passively discover page-declared WebMCP tools",
|
||||
args: z.object({
|
||||
endpoint: z
|
||||
.string()
|
||||
.describe("Loopback CDP HTTP endpoint or browser WebSocket URL"),
|
||||
targetId: z.string().describe("Target ID from list-browser-targets"),
|
||||
endpoint: cliDomainInput(
|
||||
discoverWebMcpToolsInputSchema.shape.cdp_endpoint,
|
||||
),
|
||||
targetId: discoverWebMcpToolsInputSchema.shape.target_id.describe(
|
||||
"Target ID from list-browser-targets",
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
...browserScopeOptions,
|
||||
@@ -172,10 +175,12 @@ const registerScreenshot = (
|
||||
description:
|
||||
"Capture one visible page viewport within the supplied origin scope",
|
||||
args: z.object({
|
||||
endpoint: z
|
||||
.string()
|
||||
.describe("Loopback CDP HTTP endpoint or browser WebSocket URL"),
|
||||
targetId: z.string().describe("Target ID from list-browser-targets"),
|
||||
endpoint: cliDomainInput(
|
||||
captureWebScreenshotInputSchema.shape.cdp_endpoint,
|
||||
),
|
||||
targetId: captureWebScreenshotInputSchema.shape.target_id.describe(
|
||||
"Target ID from list-browser-targets",
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
...browserScopeOptions,
|
||||
|
||||
+22
-15
@@ -25,6 +25,7 @@ import { analyzeWebBundleInputSchema } from "../domain/webBundleAnalysis.js";
|
||||
import { observeWebSessionInputSchema } from "../domain/browserSession.js";
|
||||
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
||||
import type { Logger } from "pino";
|
||||
import { cliDomainInput } from "./domainInput.js";
|
||||
import { cliDecimalField } from "./options.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import {
|
||||
@@ -55,9 +56,9 @@ const registerTargetList = (
|
||||
description:
|
||||
"List page targets from a selected loopback CDP browser, optionally filtered by origin",
|
||||
args: z.object({
|
||||
endpoint: z
|
||||
.string()
|
||||
.describe("Loopback CDP HTTP endpoint or browser WebSocket URL"),
|
||||
endpoint: cliDomainInput(
|
||||
listBrowserTargetsInputSchema.shape.cdp_endpoint,
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
allowedOrigins: browserScopeOptions.allowedOrigins,
|
||||
@@ -103,10 +104,12 @@ const registerPageInspection = (
|
||||
description:
|
||||
"Passively inspect one page through CDP, defaulting to its current origin",
|
||||
args: z.object({
|
||||
endpoint: z
|
||||
.string()
|
||||
.describe("Loopback CDP HTTP endpoint or browser WebSocket URL"),
|
||||
targetId: z.string().describe("Target ID from list-browser-targets"),
|
||||
endpoint: cliDomainInput(
|
||||
listBrowserTargetsInputSchema.shape.cdp_endpoint,
|
||||
),
|
||||
targetId: observeWebSessionInputSchema.shape.target_id.describe(
|
||||
"Target ID from list-browser-targets",
|
||||
),
|
||||
}),
|
||||
options: browserPageInspectionOptions,
|
||||
run: ({ args, options }) =>
|
||||
@@ -160,10 +163,12 @@ const registerBundleAnalysis = (
|
||||
description:
|
||||
"Capture and statically analyze a page bundle, defaulting to the selected target origin",
|
||||
args: z.object({
|
||||
endpoint: z
|
||||
.string()
|
||||
.describe("Loopback CDP HTTP endpoint or browser WebSocket URL"),
|
||||
targetId: z.string().describe("Target ID from list-browser-targets"),
|
||||
endpoint: cliDomainInput(
|
||||
listBrowserTargetsInputSchema.shape.cdp_endpoint,
|
||||
),
|
||||
targetId: observeWebSessionInputSchema.shape.target_id.describe(
|
||||
"Target ID from list-browser-targets",
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
...browserScopeOptions,
|
||||
@@ -250,10 +255,12 @@ const registerObservationSession = (
|
||||
cli.command(CLI_COMMANDS.observeWebSession, {
|
||||
description: "Observe user-driven page navigation during a chosen window",
|
||||
args: z.object({
|
||||
endpoint: z
|
||||
.string()
|
||||
.describe("Loopback CDP HTTP endpoint or browser WebSocket URL"),
|
||||
targetId: z.string().describe("Target ID from list-browser-targets"),
|
||||
endpoint: cliDomainInput(
|
||||
listBrowserTargetsInputSchema.shape.cdp_endpoint,
|
||||
),
|
||||
targetId: observeWebSessionInputSchema.shape.target_id.describe(
|
||||
"Target ID from list-browser-targets",
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
...browserScopeOptions,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { z } from "incur";
|
||||
|
||||
/**
|
||||
* CLI argv keeps the domain field's inbound text. A service transform still
|
||||
* runs when that service parses the same field, and `--schema` matches the
|
||||
* input view advertised by tools/list.
|
||||
*/
|
||||
export const cliDomainInput = (field: z.ZodType): z.ZodType =>
|
||||
field instanceof z.ZodPipe ? (field.def.in as z.ZodType) : field;
|
||||
@@ -16,6 +16,7 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { projectInputIssues } from "../domain/inputIssueProjection.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { Logger } from "pino";
|
||||
import { cliDomainInput } from "./domainInput.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { createElectronFuseService } from "../composition/electronFuses.js";
|
||||
import { parseCliJsonInput, resolveCliJsonPaths } from "../cliJsonInput.js";
|
||||
@@ -243,7 +244,9 @@ const registerElectronTargetList = (
|
||||
description:
|
||||
"List local file pages exposed by the selected Electron CDP endpoint",
|
||||
args: z.object({
|
||||
endpoint: z.string().describe("Literal-loopback Electron CDP endpoint"),
|
||||
endpoint: cliDomainInput(
|
||||
listElectronTargetsInputSchema.shape.cdp_endpoint,
|
||||
).describe("Literal-loopback Electron CDP endpoint"),
|
||||
}),
|
||||
run: ({ args }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
@@ -286,8 +289,13 @@ const registerElectronPageInspection = (
|
||||
cli.command(CLI_COMMANDS.inspectElectronPage, {
|
||||
description: "Passively inspect one Electron file page",
|
||||
args: z.object({
|
||||
endpoint: z.string().describe("Literal-loopback Electron CDP endpoint"),
|
||||
targetId: z.string().describe("Target ID from list-electron-targets"),
|
||||
endpoint: cliDomainInput(
|
||||
listElectronTargetsInputSchema.shape.cdp_endpoint,
|
||||
).describe("Literal-loopback Electron CDP endpoint"),
|
||||
targetId: z
|
||||
.string()
|
||||
.min(1)
|
||||
.describe("Target ID from list-electron-targets"),
|
||||
}),
|
||||
options: electronPageInspectionOptions,
|
||||
run: ({ args, options }) =>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "../application/javascript/JavaScriptRuntimeObservationService.js";
|
||||
import { createJavaScriptRuntimeObservationProvider } from "../composition/javascriptRuntimeObservation.js";
|
||||
import { V8InspectorAdmission } from "../inspector/V8InspectorProvider.js";
|
||||
import { cliDomainInput } from "./domainInput.js";
|
||||
import { cliDecimalField } from "./options.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
@@ -49,7 +50,9 @@ export const registerJavaScriptRuntimeObservationCommands = (
|
||||
cli.command(CLI_COMMANDS.listJavaScriptRuntimeTargets, {
|
||||
description: "List Node/Electron V8 Inspector targets",
|
||||
args: z.object({
|
||||
endpoint: z.string().describe("Literal-loopback Inspector endpoint"),
|
||||
endpoint: cliDomainInput(
|
||||
listJavaScriptRuntimeTargetsInputSchema.shape.inspector_endpoint,
|
||||
).describe("Literal-loopback Inspector endpoint"),
|
||||
}),
|
||||
run: ({ args }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
@@ -90,10 +93,12 @@ export const registerJavaScriptRuntimeObservationCommands = (
|
||||
cli.command(CLI_COMMANDS.observeJavaScriptRuntime, {
|
||||
description: "Passively observe one exact Node/Electron Inspector target",
|
||||
args: z.object({
|
||||
endpoint: z.string().describe("Literal-loopback Inspector endpoint"),
|
||||
targetId: z
|
||||
.string()
|
||||
.describe("Target from list-javascript-runtime-targets"),
|
||||
endpoint: cliDomainInput(
|
||||
listJavaScriptRuntimeTargetsInputSchema.shape.inspector_endpoint,
|
||||
).describe("Literal-loopback Inspector endpoint"),
|
||||
targetId: observeJavaScriptRuntimeInputSchema.shape.target_id.describe(
|
||||
"Target from list-javascript-runtime-targets",
|
||||
),
|
||||
}),
|
||||
options: observeOptionsSchema,
|
||||
run: ({ args, options }) =>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
|
||||
import { Cli, z } from "incur";
|
||||
import { createWebRuntimeService } from "../composition/webRuntime.js";
|
||||
import { cliDomainInput } from "./domainInput.js";
|
||||
import { observeWebExecutionInputSchema } from "../domain/webExecution.js";
|
||||
import { inspectWebEventListenersInputSchema } from "../domain/webEventListeners.js";
|
||||
import { browserCliError } from "./browserCliError.js";
|
||||
import {
|
||||
browserScopeOptions,
|
||||
@@ -9,7 +12,6 @@ import {
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { cliDecimalField } from "./options.js";
|
||||
import { observeWebExecutionInputSchema } from "../domain/webExecution.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { ProgressReporter } from "../application/ProgressReporter.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
@@ -31,10 +33,12 @@ export const registerWebRuntimeCommands = (
|
||||
description:
|
||||
"Observe precise page execution; resets counters and temporarily disables optimization",
|
||||
args: z.object({
|
||||
endpoint: z.string().describe("Existing loopback browser CDP endpoint"),
|
||||
targetId: z
|
||||
.string()
|
||||
.describe("Selected page target ID from list-browser-targets"),
|
||||
endpoint: cliDomainInput(
|
||||
observeWebExecutionInputSchema.shape.cdp_endpoint,
|
||||
).describe("Existing loopback browser CDP endpoint"),
|
||||
targetId: observeWebExecutionInputSchema.shape.target_id.describe(
|
||||
"Selected page target ID from list-browser-targets",
|
||||
),
|
||||
}),
|
||||
options: z.object({
|
||||
...browserScopeOptions,
|
||||
@@ -74,13 +78,15 @@ export const registerWebRuntimeCommands = (
|
||||
description:
|
||||
"Inspect native listener source locations on the first main-document CSS match",
|
||||
args: z.object({
|
||||
endpoint: z.string().describe("Existing loopback browser CDP endpoint"),
|
||||
targetId: z
|
||||
.string()
|
||||
.describe("Selected page target ID from list-browser-targets"),
|
||||
selector: z
|
||||
.string()
|
||||
.describe("Native CSS selector; inspect the first main-document match"),
|
||||
endpoint: cliDomainInput(
|
||||
inspectWebEventListenersInputSchema.shape.cdp_endpoint,
|
||||
).describe("Existing loopback browser CDP endpoint"),
|
||||
targetId: inspectWebEventListenersInputSchema.shape.target_id.describe(
|
||||
"Selected page target ID from list-browser-targets",
|
||||
),
|
||||
selector: inspectWebEventListenersInputSchema.shape.selector.describe(
|
||||
"Native CSS selector; inspect the first main-document match",
|
||||
),
|
||||
}),
|
||||
options: z.object({ ...browserScopeOptions }),
|
||||
run: ({ args, options }) =>
|
||||
|
||||
@@ -409,6 +409,16 @@ it("retains safe structured constraints for ADB selectors", async () => {
|
||||
path: "data-uri",
|
||||
maximum: 2048,
|
||||
},
|
||||
{
|
||||
argv: ["read-adb-setting", "S", "system", "k".repeat(257)],
|
||||
path: "key",
|
||||
maximum: 256,
|
||||
},
|
||||
{
|
||||
argv: ["read-adb-setting", "S", "system", "bad key"],
|
||||
path: "key",
|
||||
expected: "regex",
|
||||
},
|
||||
];
|
||||
for (const { argv, path, expected, maximum } of cases) {
|
||||
const result = await serve([...argv, "--json"], {
|
||||
|
||||
@@ -1,11 +1,49 @@
|
||||
import { describe, expect } from "vitest";
|
||||
import type { z } from "zod";
|
||||
|
||||
import {
|
||||
inspectWebPageInputSchema,
|
||||
listBrowserTargetsInputSchema,
|
||||
} from "../../../src/domain/browserObservation.js";
|
||||
import { listJavaScriptRuntimeTargetsInputSchema } from "../../../src/domain/javascript/javascriptRuntimeObservation.js";
|
||||
import { analyzeWebBundleInputSchema } from "../../../src/domain/webBundleAnalysis.js";
|
||||
import { discoverWebMcpToolsInputSchema } from "../../../src/domain/webMcpDiscovery.js";
|
||||
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";
|
||||
|
||||
/** The domain schema owns custom issue text. Tests compare the CLI to that issue. */
|
||||
const schemaIssue = (
|
||||
schema: z.ZodType,
|
||||
value: unknown,
|
||||
path: readonly (string | number)[],
|
||||
) => {
|
||||
const parsed = schema.safeParse(value);
|
||||
if (parsed.success)
|
||||
throw new Error(`domain schema accepted ${path.join(".")}`);
|
||||
const issue = parsed.error.issues.find(
|
||||
(item) => item.path.join(".") === path.join("."),
|
||||
);
|
||||
if (issue === undefined)
|
||||
throw new Error(
|
||||
`domain schema omitted ${path.join(".")}: ${JSON.stringify(parsed.error.issues)}`,
|
||||
);
|
||||
return issue;
|
||||
};
|
||||
|
||||
const malformedEndpointInput = (operation: string) =>
|
||||
operation === "list_browser_targets"
|
||||
? { cdp_endpoint: malformedEndpoint }
|
||||
: {
|
||||
cdp_endpoint: malformedEndpoint,
|
||||
target_id: "target",
|
||||
...(operation === "analyze_web_bundle"
|
||||
? { include_script_sources: true }
|
||||
: {}),
|
||||
};
|
||||
|
||||
const invalidInputs = [
|
||||
{
|
||||
command: "list-browser-targets",
|
||||
@@ -15,15 +53,8 @@ const invalidInputs = [
|
||||
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 },
|
||||
schema: listBrowserTargetsInputSchema,
|
||||
},
|
||||
{
|
||||
command: "analyze-web-bundle",
|
||||
@@ -33,33 +64,8 @@ const invalidInputs = [
|
||||
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 },
|
||||
schema: analyzeWebBundleInputSchema,
|
||||
},
|
||||
{
|
||||
command: "discover-webmcp-tools",
|
||||
@@ -69,15 +75,36 @@ const invalidInputs = [
|
||||
issue: {
|
||||
path: ["cdp_endpoint"],
|
||||
reason: "invalid_value",
|
||||
message: "Invalid CDP endpoint URL",
|
||||
},
|
||||
schema: discoverWebMcpToolsInputSchema,
|
||||
},
|
||||
];
|
||||
|
||||
const parserRejections = [
|
||||
{
|
||||
command: "inspect-web-page",
|
||||
arguments: [browserEndpoint, ""],
|
||||
path: "targetId",
|
||||
},
|
||||
{
|
||||
command: "observe-web-session",
|
||||
arguments: [browserEndpoint, ""],
|
||||
path: "targetId",
|
||||
},
|
||||
{
|
||||
command: "list-javascript-runtime-targets",
|
||||
arguments: [""],
|
||||
path: "endpoint",
|
||||
},
|
||||
{
|
||||
command: "observe-javascript-runtime",
|
||||
arguments: [inspectorEndpoint, ""],
|
||||
path: "targetId",
|
||||
},
|
||||
{
|
||||
command: "capture-web-screenshot",
|
||||
arguments: [browserEndpoint, ""],
|
||||
operation: "capture_web_screenshot",
|
||||
error: "Browser observation failed",
|
||||
issue: { path: ["target_id"], reason: "out_of_range", minimum: 1 },
|
||||
path: "targetId",
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -97,6 +124,15 @@ describe("inspection branch diagnostics", () => {
|
||||
environment: { ...workspace.environment, REA_LOG_LEVEL: "silent" },
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
const owner = schemaIssue(
|
||||
inspectWebPageInputSchema,
|
||||
{
|
||||
cdp_endpoint: malformedEndpoint,
|
||||
target_id: "target",
|
||||
include_script_sources: flags.includes("--include-script-sources"),
|
||||
},
|
||||
["cdp_endpoint"],
|
||||
);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
@@ -105,7 +141,7 @@ describe("inspection branch diagnostics", () => {
|
||||
{
|
||||
path: ["cdp_endpoint"],
|
||||
reason: "invalid_value",
|
||||
message: "Invalid CDP endpoint URL",
|
||||
message: owner.message,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -124,6 +160,11 @@ describe("browser and Inspector CLI input diagnostics", () => {
|
||||
environment: { ...workspace.environment, REA_LOG_LEVEL: "silent" },
|
||||
});
|
||||
|
||||
const owner = schemaIssue(
|
||||
input.schema,
|
||||
malformedEndpointInput(input.operation),
|
||||
input.issue.path,
|
||||
);
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
error: input.error,
|
||||
@@ -141,7 +182,9 @@ describe("browser and Inspector CLI input diagnostics", () => {
|
||||
},
|
||||
details: {
|
||||
operation: input.operation,
|
||||
issues: expect.arrayContaining([input.issue]),
|
||||
issues: expect.arrayContaining([
|
||||
{ ...input.issue, message: owner.message },
|
||||
]),
|
||||
},
|
||||
});
|
||||
expect(result.stdout).not.toContain(malformedEndpoint);
|
||||
@@ -163,6 +206,15 @@ describe("browser and Inspector CLI input diagnostics", () => {
|
||||
environment: { ...workspace.environment, REA_LOG_LEVEL: "debug" },
|
||||
});
|
||||
|
||||
const owner = schemaIssue(
|
||||
command === "list-browser-targets"
|
||||
? listBrowserTargetsInputSchema
|
||||
: listJavaScriptRuntimeTargetsInputSchema,
|
||||
command === "list-browser-targets"
|
||||
? { cdp_endpoint: endpoint }
|
||||
: { inspector_endpoint: endpoint },
|
||||
[field],
|
||||
);
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
@@ -171,8 +223,7 @@ describe("browser and Inspector CLI input diagnostics", () => {
|
||||
{
|
||||
path: [field],
|
||||
reason: "invalid_value",
|
||||
message:
|
||||
"CDP endpoint must be an explicit-port HTTP URL on 127.0.0.1 or ::1",
|
||||
message: owner.message,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -183,7 +234,9 @@ describe("browser and Inspector CLI input diagnostics", () => {
|
||||
expect(output).not.toContain("rea-diagnostic-secret");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("browser correction facts", () => {
|
||||
cliTest(
|
||||
"retains authored cross-field correction guidance",
|
||||
async ({ cli, workspace }) => {
|
||||
@@ -199,6 +252,16 @@ describe("browser and Inspector CLI input diagnostics", () => {
|
||||
environment: { ...workspace.environment, REA_LOG_LEVEL: "silent" },
|
||||
});
|
||||
|
||||
const owner = schemaIssue(
|
||||
inspectWebPageInputSchema,
|
||||
{
|
||||
cdp_endpoint: browserEndpoint,
|
||||
target_id: "target",
|
||||
include_script_sources: false,
|
||||
include_storage_fingerprints: true,
|
||||
},
|
||||
["include_storage_fingerprints"],
|
||||
);
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
@@ -208,7 +271,7 @@ describe("browser and Inspector CLI input diagnostics", () => {
|
||||
{
|
||||
path: ["include_storage_fingerprints"],
|
||||
reason: "invalid_value",
|
||||
message: "Storage fingerprints require storage key capture",
|
||||
message: owner.message,
|
||||
},
|
||||
]),
|
||||
},
|
||||
@@ -231,7 +294,14 @@ describe("browser and Inspector CLI input diagnostics", () => {
|
||||
details: {
|
||||
operation: "compare_web_captures",
|
||||
issues: expect.arrayContaining([
|
||||
expect.objectContaining({ reason: expect.any(String) }),
|
||||
expect.objectContaining({
|
||||
path: ["before", "inspection"],
|
||||
reason: "missing_argument",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
path: ["after", "inspection"],
|
||||
reason: "missing_argument",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
@@ -262,3 +332,26 @@ describe("browser and Inspector CLI input diagnostics", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("empty browser and Inspector selectors", () => {
|
||||
cliTest.for(parserRejections)(
|
||||
"$command rejects an empty selector in argument parsing",
|
||||
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({
|
||||
code: "VALIDATION_ERROR",
|
||||
fieldErrors: [
|
||||
{
|
||||
code: "too_small",
|
||||
path: input.path,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import type { AnySchema } from "ajv";
|
||||
import { Cli, z } from "incur";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { createCli } from "../../../src/cli.js";
|
||||
|
||||
type CliInstance = ReturnType<typeof createCli>;
|
||||
type CommandMap = NonNullable<ReturnType<typeof Cli.toCommands.get>>;
|
||||
|
||||
const registeredCommands: WeakMap<object, CommandMap> = Cli.toCommands;
|
||||
|
||||
const serve = async (cli: CliInstance, argv: readonly string[]) => {
|
||||
let stdout = "";
|
||||
let exitCode = 0;
|
||||
await cli.serve([...argv], {
|
||||
env: {},
|
||||
exit: (code) => {
|
||||
exitCode = code;
|
||||
},
|
||||
stdout: (text) => {
|
||||
stdout += text;
|
||||
},
|
||||
});
|
||||
expect(exitCode, argv.join(" ")).toBe(0);
|
||||
return z
|
||||
.object({
|
||||
args: z
|
||||
.object({ properties: z.record(z.string(), z.unknown()) })
|
||||
.optional(),
|
||||
options: z
|
||||
.object({ properties: z.record(z.string(), z.unknown()) })
|
||||
.optional(),
|
||||
})
|
||||
.parse(JSON.parse(stdout));
|
||||
};
|
||||
|
||||
const absoluteMapping =
|
||||
process.platform === "win32" ? "C:\\mapping.txt" : "/tmp/mapping.txt";
|
||||
|
||||
it("advertises the domain constraints for android, adb settings, and browser selectors", async () => {
|
||||
const cli = createCli({});
|
||||
const ajv = new Ajv2020({ strict: false, validateFormats: false });
|
||||
const validate = async (
|
||||
command: string,
|
||||
group: "args" | "options",
|
||||
value: Record<string, unknown>,
|
||||
) => {
|
||||
const advertised = await serve(cli, [command, "--schema", "--json"]);
|
||||
const schema = advertised[group];
|
||||
expect(schema, command).toBeDefined();
|
||||
return ajv.compile(schema as AnySchema)(value);
|
||||
};
|
||||
|
||||
expect(
|
||||
await validate("inspect-android-class", "args", {
|
||||
path: "",
|
||||
className: "com.example.Main",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await validate("inspect-android-class", "args", {
|
||||
path: "app.apk",
|
||||
className: "",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await validate("inspect-android-class", "args", {
|
||||
path: "app.apk",
|
||||
className: "com.example.Main",
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await validate("inspect-android-class", "options", {
|
||||
mappingPath: "mapping.txt",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await validate("inspect-android-class", "options", {
|
||||
mappingPath: absoluteMapping,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await validate("inspect-android-method", "args", {
|
||||
path: "app.apk",
|
||||
className: "com.example.Main",
|
||||
methodName: "",
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
const setting = {
|
||||
serial: "emulator-5554",
|
||||
namespace: "system",
|
||||
};
|
||||
expect(
|
||||
await validate("read-adb-setting", "args", {
|
||||
...setting,
|
||||
key: "key with space",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await validate("read-adb-setting", "args", {
|
||||
...setting,
|
||||
key: "k".repeat(257),
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await validate("read-adb-setting", "args", {
|
||||
...setting,
|
||||
key: "airplane_mode_on",
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
const listener = {
|
||||
targetId: "page",
|
||||
selector: "button",
|
||||
};
|
||||
expect(
|
||||
await validate("inspect-web-event-listeners", "args", {
|
||||
...listener,
|
||||
endpoint: "",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await validate("inspect-web-event-listeners", "args", {
|
||||
endpoint: "http://127.0.0.1:9222",
|
||||
targetId: "",
|
||||
selector: "button",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await validate("inspect-web-event-listeners", "args", {
|
||||
endpoint: "http://127.0.0.1:9222",
|
||||
...listener,
|
||||
selector: "",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await validate("inspect-web-event-listeners", "args", {
|
||||
endpoint: "http://127.0.0.1:9222",
|
||||
...listener,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await validate("capture-web-screenshot", "args", {
|
||||
endpoint: "",
|
||||
targetId: "page",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await validate("capture-web-screenshot", "args", {
|
||||
endpoint: "http://127.0.0.1:9222",
|
||||
targetId: "page",
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
const parser = registeredCommands.get(cli)?.get("inspect-android-class");
|
||||
expect(parser && "args" in parser && parser.args instanceof z.ZodObject).toBe(
|
||||
true,
|
||||
);
|
||||
if (parser && "args" in parser && parser.args instanceof z.ZodObject)
|
||||
expect(
|
||||
parser.args.safeParse({ path: "app.apk", className: "" }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
@@ -148,19 +148,13 @@ it(
|
||||
expect(invalidPageInspect.signal).toBeNull();
|
||||
expect(invalidPageInspect.exitCode).toBe(1);
|
||||
expect(invalidPageInspect.json).toMatchObject({
|
||||
error: "Electron analysis failed",
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
details: {
|
||||
operation: "inspect_electron_page",
|
||||
issues: [
|
||||
{
|
||||
path: ["target_id"],
|
||||
reason: "out_of_range",
|
||||
minimum: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
code: "VALIDATION_ERROR",
|
||||
fieldErrors: [
|
||||
{
|
||||
code: "too_small",
|
||||
path: "targetId",
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
INTEGRATION_TEST_TIMEOUT_MS,
|
||||
|
||||
Reference in new issue
Block a user