fix: preserve native UI failure reasons and dedupe version literals (#854)
Keep adapter/OS helper failures in diagnostics instead of replacing them with a canned remediation sentence. Share browserVersionSchema, Node support prose, and JADX heap/remediation constants so release pins stay authoritative. Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
parent
166dcd21d0
commit
2d4a910fcd
|
|
@ -16,6 +16,7 @@ import {
|
|||
jadxXrefsSchema,
|
||||
normalizeJadxText,
|
||||
} from "./JadxProtocol.js";
|
||||
import { JADX_RELEASE } from "./JadxRelease.js";
|
||||
|
||||
/** The adapter consumes tool payloads, never provider RPC details outside this layer. */
|
||||
export interface JadxToolPort {
|
||||
|
|
@ -246,7 +247,7 @@ const inspectMethod = async (
|
|||
throw new AnalysisCapabilityUnavailableError(
|
||||
"jadx",
|
||||
request.operation,
|
||||
`JADX 0.7.1 smali fallback joins all same-name overloads for ${summary.full_name}.${request.input.method_name}; it cannot establish source for only overload ${index}.`,
|
||||
`JADX ${JADX_RELEASE.version} smali fallback joins all same-name overloads for ${summary.full_name}.${request.input.method_name}; it cannot establish source for only overload ${index}.`,
|
||||
);
|
||||
return androidResultSchemas.inspect_android_method.parse({
|
||||
engine,
|
||||
|
|
@ -306,7 +307,7 @@ const traceReferences = async (
|
|||
throw new AnalysisCapabilityUnavailableError(
|
||||
"jadx",
|
||||
request.operation,
|
||||
`JADX 0.7.1 references select the first same-name method; ${summary.full_name}.${methodName} has ${candidates.length} overloads. No unambiguous reference result is available.`,
|
||||
`JADX ${JADX_RELEASE.version} references select the first same-name method; ${summary.full_name}.${methodName} has ${candidates.length} overloads. No unambiguous reference result is available.`,
|
||||
);
|
||||
const expectedTarget =
|
||||
methodName === undefined
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { constants } from "node:fs";
|
|||
import { isAbsolute, join } from "node:path";
|
||||
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
|
||||
import type { AndroidOperation } from "../domain/androidAnalysis.js";
|
||||
import { JADX_JAR_CONFIGURATION_REMEDIATION } from "./JadxRelease.js";
|
||||
|
||||
/** Caller-supplied tools, resolved only when an Android operation is selected. */
|
||||
export interface JadxConfiguration {
|
||||
|
|
@ -27,9 +28,7 @@ export const resolveJadxConfiguration = async (
|
|||
"JAVA_HOME must select an existing JDK by absolute path; omit it to use java on PATH.",
|
||||
);
|
||||
if (jar === undefined || !isAbsolute(jar))
|
||||
throw unavailable(
|
||||
"Set REA_JADX_MCP_JAR to the absolute path of a caller-supplied jadx-headless-mcp 0.7.1 JAR. REA does not download or install it.",
|
||||
);
|
||||
throw unavailable(JADX_JAR_CONFIGURATION_REMEDIATION);
|
||||
const java =
|
||||
environment.JAVA_HOME === undefined
|
||||
? "java"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from "./AndroidTargetSnapshot.js";
|
||||
import { resolveJadxConfiguration } from "./JadxConfiguration.js";
|
||||
import type { JadxLauncher } from "./JadxMcpTransport.js";
|
||||
import { JADX_HEAP_XMX_ARGUMENT } from "./JadxRelease.js";
|
||||
import { JadxSession } from "./JadxSession.js";
|
||||
|
||||
const OPERATION_TIMEOUT_MS = 120_000;
|
||||
|
|
@ -203,7 +204,7 @@ export class JadxProvider implements AndroidAnalysisPort {
|
|||
{
|
||||
command: configuration.java,
|
||||
arguments: [
|
||||
"-Xmx512m",
|
||||
JADX_HEAP_XMX_ARGUMENT,
|
||||
"-XX:ActiveProcessorCount=1",
|
||||
"-jar",
|
||||
engine.path,
|
||||
|
|
@ -216,7 +217,9 @@ export class JadxProvider implements AndroidAnalysisPort {
|
|||
],
|
||||
cwd: root.path,
|
||||
hostEnvironment: this.environment,
|
||||
env: { _JAVA_OPTIONS: "-Xmx512m -XX:ActiveProcessorCount=1" },
|
||||
env: {
|
||||
_JAVA_OPTIONS: `${JADX_HEAP_XMX_ARGUMENT} -XX:ActiveProcessorCount=1`,
|
||||
},
|
||||
},
|
||||
this.launcher,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,15 @@ export const JADX_RELEASE = Object.freeze({
|
|||
url: "https://github.com/1013503897/jadx-headless-mcp/releases/download/v0.7.1/jadx-headless-mcp-0.7.1-all.jar",
|
||||
});
|
||||
|
||||
/** JVM heap ceiling applied to every owned JADX worker process. */
|
||||
export const JADX_HEAP_LIMIT_MIB = 512;
|
||||
|
||||
/** JVM `-Xmx` argument matching {@link JADX_HEAP_LIMIT_MIB}. */
|
||||
export const JADX_HEAP_XMX_ARGUMENT = `-Xmx${JADX_HEAP_LIMIT_MIB}m`;
|
||||
|
||||
/** Remediation when `REA_JADX_MCP_JAR` is missing or not an absolute path. */
|
||||
export const JADX_JAR_CONFIGURATION_REMEDIATION = `Set REA_JADX_MCP_JAR to the absolute path of a caller-supplied jadx-headless-mcp ${JADX_RELEASE.version} JAR. REA does not download or install it.`;
|
||||
|
||||
/** Producing engine identity used by Evidence and the generated product catalog. */
|
||||
export const JADX_PROVIDER_IDENTITY = Object.freeze({
|
||||
id: "jadx",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
parseJadxJson,
|
||||
} from "./JadxProtocol.js";
|
||||
import {
|
||||
JADX_HEAP_LIMIT_MIB,
|
||||
JADX_RELEASE,
|
||||
JADX_PROVIDER_IDENTITY,
|
||||
JADX_LIMITATIONS,
|
||||
|
|
@ -84,7 +85,7 @@ export class JadxSession {
|
|||
source_revision:
|
||||
jarHash === JADX_RELEASE.sha256 ? JADX_RELEASE.revision : null,
|
||||
worker_count: 1,
|
||||
heap_limit_mib: 512,
|
||||
heap_limit_mib: JADX_HEAP_LIMIT_MIB,
|
||||
} as const;
|
||||
const result = await analyzeJadxRequest(tools, request, loaded, engine);
|
||||
return createAnalysisExecution(result, JADX_PROVIDER_IDENTITY, {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { SUPPORTED_NODE_VERSION_PROSE } from "../domain/runtimeVersion.js";
|
||||
import type { ClientRegistrationStatus } from "./ClientRegistrationStatus.js";
|
||||
import { createDoctorHostFixture as host } from "./Doctor.fixture.js";
|
||||
import { runDoctor } from "./Doctor.js";
|
||||
|
|
@ -19,7 +21,7 @@ describe("doctor", () => {
|
|||
result.checks.map(({ name, remediation }) => [name, remediation]),
|
||||
),
|
||||
).toEqual({
|
||||
node: "Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",
|
||||
node: `Install ${SUPPORTED_NODE_VERSION_PROSE}.`,
|
||||
host: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and the experimental Windows x64 Ghidra P0 boundary.",
|
||||
hopper: "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",
|
||||
target: "Supply a readable local app or program path.",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { homedir } from "node:os";
|
||||
|
||||
import { supportsNodeVersion } from "../domain/runtimeVersion.js";
|
||||
import {
|
||||
SUPPORTED_NODE_VERSION_PROSE,
|
||||
supportsNodeVersion,
|
||||
} from "../domain/runtimeVersion.js";
|
||||
import { linuxHopperLauncherPath } from "./LinuxHopper.js";
|
||||
import type {
|
||||
DoctorCheck,
|
||||
|
|
@ -92,7 +95,7 @@ export const doctorHealthy = (
|
|||
|
||||
const nodeCheck = (host: DoctorHost): DoctorCheck =>
|
||||
check("node", supportsNodeVersion(host.nodeVersion), host.nodeVersion, {
|
||||
remediation: "Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",
|
||||
remediation: `Install ${SUPPORTED_NODE_VERSION_PROSE}.`,
|
||||
classification: "missing_dependency",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { SUPPORTED_NODE_VERSION_PROSE } from "../domain/runtimeVersion.js";
|
||||
import { FakeSetupHost, options } from "./Setup.fixture.js";
|
||||
import { runSetup } from "./Setup.js";
|
||||
|
||||
|
|
@ -122,7 +123,7 @@ describe("setup scoped readiness", () => {
|
|||
const nodeHost = new FakeSetupHost();
|
||||
nodeHost.nodeVersion = "20.0.0";
|
||||
expect((await runSetup(options(true), nodeHost)).remediation).toBe(
|
||||
"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup.",
|
||||
`Install ${SUPPORTED_NODE_VERSION_PROSE} and rerun setup.`,
|
||||
);
|
||||
|
||||
const macHost = new FakeSetupHost();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import { homedir } from "node:os";
|
|||
import { resolve } from "node:path";
|
||||
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import { supportsNodeVersion } from "../domain/runtimeVersion.js";
|
||||
import {
|
||||
SUPPORTED_NODE_VERSION_PROSE,
|
||||
supportsNodeVersion,
|
||||
} from "../domain/runtimeVersion.js";
|
||||
import { runDoctor, systemDoctorHost, type DoctorHost } from "./Doctor.js";
|
||||
import { installLinuxHopper, readLinuxDistribution } from "./LinuxHopper.js";
|
||||
import { installMacHopper } from "./MacHopper.js";
|
||||
|
|
@ -63,7 +66,7 @@ export const hostRemediation = async (
|
|||
installHopper: boolean,
|
||||
): Promise<string | undefined> => {
|
||||
if (!supportsNodeVersion(host.nodeVersion))
|
||||
return "Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup.";
|
||||
return `Install ${SUPPORTED_NODE_VERSION_PROSE} and rerun setup.`;
|
||||
if (!installHopper) return undefined;
|
||||
if (host.platform !== "darwin" && host.platform !== "linux")
|
||||
return "REA supports Hopper on macOS and selected 64-bit Linux distributions.";
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import { webTextArtifactSchema } from "./webContentArtifact.js";
|
|||
import { jsonShapeSchema } from "./jsonShape.js";
|
||||
import { prefixedDigestSchema } from "./../domain/digests.js";
|
||||
|
||||
const browserVersionSchema = z.object({
|
||||
/** Shared CDP/browser version identity carried on observation Evidence. */
|
||||
export const browserVersionSchema = z.object({
|
||||
product: z.string(),
|
||||
protocol_version: z.string(),
|
||||
revision: z.string(),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
browserEndpointSchema,
|
||||
} from "./browserObservation.js";
|
||||
import { browserCompletenessSchema } from "./browserCompleteness.js";
|
||||
import { browserVersionSchema } from "./browserObservationSchemas.js";
|
||||
|
||||
/** Input for a navigation-aware browser observation window. */
|
||||
export const observeWebSessionInputSchema = z.strictObject({
|
||||
|
|
@ -42,13 +43,7 @@ const timelineEventSchema = z.object({
|
|||
|
||||
/** Navigation-aware session result for external user actions. */
|
||||
export const webObservationSessionSchema = z.object({
|
||||
browser: z.object({
|
||||
product: z.string(),
|
||||
protocol_version: z.string(),
|
||||
revision: z.string(),
|
||||
user_agent: z.string(),
|
||||
js_version: z.string(),
|
||||
}),
|
||||
browser: browserVersionSchema,
|
||||
target: z.object({
|
||||
target_id: z.string(),
|
||||
initial_url: z.string(),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { z } from "zod";
|
|||
|
||||
import { browserCompletenessSchema } from "./browserCompleteness.js";
|
||||
import { browserEndpointSchema } from "./browserObservation.js";
|
||||
import { browserVersionSchema } from "./browserObservationSchemas.js";
|
||||
import { webTextArtifactSchema } from "./webContentArtifact.js";
|
||||
import { prefixedDigestSchema } from "./../domain/digests.js";
|
||||
|
||||
|
|
@ -37,13 +38,6 @@ export type InspectElectronPageInput = z.infer<
|
|||
typeof inspectElectronPageInputSchema
|
||||
>;
|
||||
|
||||
const browserVersionSchema = z.object({
|
||||
product: z.string(),
|
||||
protocol_version: z.string(),
|
||||
revision: z.string(),
|
||||
user_agent: z.string(),
|
||||
js_version: z.string(),
|
||||
});
|
||||
const electronTargetSchema = z.object({
|
||||
target_id: z.string(),
|
||||
type: z.string(),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import { satisfies } from "semver";
|
|||
/** Supported runtime range, kept in sync with the published package engines. */
|
||||
export const SUPPORTED_NODE_VERSION_RANGE = "^22.19.0 || ^24.11.0 || >=26.0.0";
|
||||
|
||||
/** Human-readable form of {@link SUPPORTED_NODE_VERSION_RANGE} for remediation. */
|
||||
export const SUPPORTED_NODE_VERSION_PROSE =
|
||||
"Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+";
|
||||
|
||||
/** Return whether a Node.js version satisfies REA's tested runtime families. */
|
||||
export const supportsNodeVersion = (version: string): boolean =>
|
||||
satisfies(version, SUPPORTED_NODE_VERSION_RANGE);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
browserAllowedOriginsSchema,
|
||||
browserEndpointSchema,
|
||||
} from "./browserObservation.js";
|
||||
import { browserVersionSchema } from "./browserObservationSchemas.js";
|
||||
import { jsonShapeSchema } from "./jsonShape.js";
|
||||
import { prefixedDigestSchema } from "./../domain/digests.js";
|
||||
|
||||
|
|
@ -19,14 +20,6 @@ export type DiscoverWebMcpToolsInput = z.infer<
|
|||
typeof discoverWebMcpToolsInputSchema
|
||||
>;
|
||||
|
||||
const browserVersionSchema = z.object({
|
||||
product: z.string(),
|
||||
protocol_version: z.string(),
|
||||
revision: z.string(),
|
||||
user_agent: z.string(),
|
||||
js_version: z.string(),
|
||||
});
|
||||
|
||||
const webMcpToolSchema = z.object({
|
||||
tool_key: prefixedDigestSchema("webmcp"),
|
||||
name: z.string(),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
browserAllowedOriginsSchema,
|
||||
browserEndpointSchema,
|
||||
} from "./browserObservation.js";
|
||||
import { browserVersionSchema } from "./browserObservationSchemas.js";
|
||||
|
||||
/** Self-verifying inline PNG artifact for CLI/MCP parity. */
|
||||
export const webScreenshotArtifactSchema = z
|
||||
|
|
@ -41,14 +42,6 @@ export type CaptureWebScreenshotInput = z.infer<
|
|||
typeof captureWebScreenshotInputSchema
|
||||
>;
|
||||
|
||||
const browserVersionSchema = z.object({
|
||||
product: z.string(),
|
||||
protocol_version: z.string(),
|
||||
revision: z.string(),
|
||||
user_agent: z.string(),
|
||||
js_version: z.string(),
|
||||
});
|
||||
|
||||
/** Screenshot observation with embedded immutable artifact bytes. */
|
||||
export const webScreenshotSchema = z.object({
|
||||
browser: browserVersionSchema,
|
||||
|
|
|
|||
|
|
@ -223,6 +223,24 @@ describe("native UI target and cancellation failures", () => {
|
|||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error.message).toContain(code);
|
||||
});
|
||||
it("preserves the underlying helper failure reason in diagnostics", async () => {
|
||||
const result = await observeNativeUi(target, "observe_native_ui", scope, {
|
||||
invoke: async () => {
|
||||
throw new Error(
|
||||
"Native helper returned invalid JSON: Unexpected token",
|
||||
);
|
||||
},
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error).toMatchObject({
|
||||
diagnostics: {
|
||||
reason: "Native helper returned invalid JSON: Unexpected token",
|
||||
remediation:
|
||||
"Native helper failed, timed out, or returned malformed capture data; install compatible Xcode command-line tools and inspect local OS permissions",
|
||||
},
|
||||
});
|
||||
});
|
||||
it("stops on a failed action and returns ordered before/capture-gap evidence", async () => {
|
||||
const calls: Readonly<Record<string, unknown>>[] = [];
|
||||
const result = await observeNativeUi(
|
||||
|
|
|
|||
|
|
@ -153,7 +153,8 @@ const observeWithHelper = async (
|
|||
cause,
|
||||
diagnostics: {
|
||||
helper_path: helper,
|
||||
reason:
|
||||
reason: cause instanceof Error ? cause.message : String(cause),
|
||||
remediation:
|
||||
"Native helper failed, timed out, or returned malformed capture data; install compatible Xcode command-line tools and inspect local OS permissions",
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
Loading…
Reference in New Issue