From 2d4a910fcd30e8d5b96fcb957b931e9aa3e4ab0c Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:00:24 +0800 Subject: [PATCH] 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 --- src/android/JadxAnalysis.ts | 5 +++-- src/android/JadxConfiguration.ts | 5 ++--- src/android/JadxProvider.ts | 7 +++++-- src/android/JadxRelease.ts | 9 +++++++++ src/android/JadxSession.ts | 3 ++- src/application/Doctor.test.ts | 4 +++- src/application/DoctorDiagnostics.ts | 7 +++++-- src/application/Setup.recovery.test.ts | 3 ++- src/application/SetupHost.ts | 7 +++++-- src/domain/browserObservationSchemas.ts | 3 ++- src/domain/browserSession.ts | 9 ++------- src/domain/electronObservation.ts | 8 +------- src/domain/runtimeVersion.ts | 4 ++++ src/domain/webMcpDiscovery.ts | 9 +-------- src/domain/webScreenshot.ts | 9 +-------- src/native/NativeUiObservation.test.ts | 18 ++++++++++++++++++ src/native/NativeUiObservation.ts | 3 ++- 17 files changed, 67 insertions(+), 46 deletions(-) diff --git a/src/android/JadxAnalysis.ts b/src/android/JadxAnalysis.ts index 859537826..419dfc660 100644 --- a/src/android/JadxAnalysis.ts +++ b/src/android/JadxAnalysis.ts @@ -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 diff --git a/src/android/JadxConfiguration.ts b/src/android/JadxConfiguration.ts index dbdc5b2c6..c585769a5 100644 --- a/src/android/JadxConfiguration.ts +++ b/src/android/JadxConfiguration.ts @@ -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" diff --git a/src/android/JadxProvider.ts b/src/android/JadxProvider.ts index 97ab0a1ac..f008437d1 100644 --- a/src/android/JadxProvider.ts +++ b/src/android/JadxProvider.ts @@ -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, ); diff --git a/src/android/JadxRelease.ts b/src/android/JadxRelease.ts index 0f13d716e..aa48f7b84 100644 --- a/src/android/JadxRelease.ts +++ b/src/android/JadxRelease.ts @@ -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", diff --git a/src/android/JadxSession.ts b/src/android/JadxSession.ts index d806af782..27ecf28d1 100644 --- a/src/android/JadxSession.ts +++ b/src/android/JadxSession.ts @@ -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, { diff --git a/src/application/Doctor.test.ts b/src/application/Doctor.test.ts index 37975f21e..81e4009b4 100644 --- a/src/application/Doctor.test.ts +++ b/src/application/Doctor.test.ts @@ -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.", diff --git a/src/application/DoctorDiagnostics.ts b/src/application/DoctorDiagnostics.ts index aa6eae092..1fe411e2d 100644 --- a/src/application/DoctorDiagnostics.ts +++ b/src/application/DoctorDiagnostics.ts @@ -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", }); diff --git a/src/application/Setup.recovery.test.ts b/src/application/Setup.recovery.test.ts index 13638a0d0..686a3d903 100644 --- a/src/application/Setup.recovery.test.ts +++ b/src/application/Setup.recovery.test.ts @@ -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(); diff --git a/src/application/SetupHost.ts b/src/application/SetupHost.ts index 31075a7a7..d574c5bd0 100644 --- a/src/application/SetupHost.ts +++ b/src/application/SetupHost.ts @@ -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 => { 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."; diff --git a/src/domain/browserObservationSchemas.ts b/src/domain/browserObservationSchemas.ts index 2bd4d3ad0..84d3349b8 100644 --- a/src/domain/browserObservationSchemas.ts +++ b/src/domain/browserObservationSchemas.ts @@ -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(), diff --git a/src/domain/browserSession.ts b/src/domain/browserSession.ts index cfbb44078..17ecfa8db 100644 --- a/src/domain/browserSession.ts +++ b/src/domain/browserSession.ts @@ -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(), diff --git a/src/domain/electronObservation.ts b/src/domain/electronObservation.ts index fb7d398fc..538e2872f 100644 --- a/src/domain/electronObservation.ts +++ b/src/domain/electronObservation.ts @@ -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(), diff --git a/src/domain/runtimeVersion.ts b/src/domain/runtimeVersion.ts index dc6e08b64..09fbc54e3 100644 --- a/src/domain/runtimeVersion.ts +++ b/src/domain/runtimeVersion.ts @@ -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); diff --git a/src/domain/webMcpDiscovery.ts b/src/domain/webMcpDiscovery.ts index d48413e22..fa429e453 100644 --- a/src/domain/webMcpDiscovery.ts +++ b/src/domain/webMcpDiscovery.ts @@ -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(), diff --git a/src/domain/webScreenshot.ts b/src/domain/webScreenshot.ts index f319e6c61..57ee35161 100644 --- a/src/domain/webScreenshot.ts +++ b/src/domain/webScreenshot.ts @@ -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, diff --git a/src/native/NativeUiObservation.test.ts b/src/native/NativeUiObservation.test.ts index cea4f3264..0eda0258f 100644 --- a/src/native/NativeUiObservation.test.ts +++ b/src/native/NativeUiObservation.test.ts @@ -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>[] = []; const result = await observeNativeUi( diff --git a/src/native/NativeUiObservation.ts b/src/native/NativeUiObservation.ts index f788075c3..86eac590d 100644 --- a/src/native/NativeUiObservation.ts +++ b/src/native/NativeUiObservation.ts @@ -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", }, }),