feat(android): make the JADX operation deadline configurable (#4466)

* feat(android): make the JADX operation deadline configurable

JADX operations used a fixed 120 s provider deadline and a separate fixed
120 s per-call bound in the session, so an APK whose cold load exceeds
120 s could not complete, and raising only the MCP client timeout could
not extend either. Add REA_JADX_OPERATION_TIMEOUT_MS, parsed once by the
JADX configuration owner (decimal milliseconds from 1 to 2147483647,
default 120000). The provider resolves it before acquiring anything and
uses it for the operation deadline and its timeout errors, and the session
uses it for every upstream call. Doctor reports the effective value, and
setup forwards an explicit value like the other JADX settings.

Fixes #3918

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(android): isolate JADX fixtures from inherited shell settings

The shared JADX protocol fixture spread the developer's process.env, so an
exported REA_JADX_OPERATION_TIMEOUT_MS (or REA_JADX_HEAP_MIB) changed every
provider test that never selected one. Keep inheriting the host environment
for process launch, but take REA_JADX_* settings only from the fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Paritosh LahreandClaude Opus 5.5 authored and GitHub committed 2026-10-17 00:06:32 +08:00
1 parent 29b8769d2c
commit f433786463
12 files changed
+356 -14

No files matched your search

@@ -24,7 +24,9 @@ matching bundled native controls and uses an owned Job Object with bidirectional
stdio. A full JDK, including `jdk.compiler`, is required on every host; Windows
`JAVA_HOME` selects `bin\java.exe`. Consult the Android guide for actual host
verification coverage. Calls use one worker and provider deadlines; optional
`REA_JADX_HEAP_MIB` selects the JVM heap. The APK is never launched,
`REA_JADX_HEAP_MIB` selects the JVM heap and `REA_JADX_OPERATION_TIMEOUT_MS`
the per-operation deadline (default 120000 ms; the MCP client's tool timeout
must also allow it). The APK is never launched,
and no emulator is required.
Manifest entries and decompiled text are static observations, not execution or
+19 -2
View File
@@ -300,8 +300,25 @@ down to MiB and retains exact bytes, Java version and visible processors in its
raw runtime response. The observed allocatable heap can differ from configured
`-Xmx`, especially with Serial GC. Heap is not a total RSS cap: the JVM also
uses native memory. Independent REA instances have independent queues. Whole
operations have a **120-second** execution deadline after reaching the queue's
front; upstream decompilation is limited to **90 seconds**.
operations have a **120-second** execution deadline by default, starting after
they reach the queue's front. For a known slow cold load, such as a large
multidex APK, select another finite deadline:
```sh
export REA_JADX_OPERATION_TIMEOUT_MS=600000
```
The value must be a decimal millisecond count from 1 to 2147483647; anything
else is refused before Java starts. The same deadline bounds each upstream
call, a timeout reports the selected value, and a passing JADX configuration
check in `rea doctor` reports the effective `operation_timeout_ms`. Explicit
cancellation still ends the operation immediately, and partial observations
and owned-process cleanup are unchanged. Raising the deadline does not extend
the bridge's 30-second startup handshake or the upstream **90-second**
decompilation limit. MCP clients enforce their own tool-call timeouts, and
setup's derived [client tool timeout](client-configuration.md) does not include
this setting, so also raise the client timeout when selecting a longer
deadline.
Decoded manifest and method text have a **1 MiB** upstream byte budget and report
truncation. Class-name pages use indexed slices of up to 4,096 names without changing the
+7 -1
View File
@@ -151,7 +151,10 @@ An explicit `REA_JEB_MCP_TIMEOUT_MS` request budget also contributes to that
maximum. For `ida` or `auto` selection, a valid IDA registration's parsed
`timeoutMs` also contributes to that maximum; its default is 300000 ms.
Setup forwards these explicitly selected REA deadlines and the input schema
profile into the registration; unrelated process values are omitted.
profile into the registration; unrelated process values are omitted. An
explicit `REA_JADX_OPERATION_TIMEOUT_MS` is forwarded with the JADX
registration but does not contribute to the derived tool timeout; raise
`toolTimeoutMs` to cover a longer selected JADX deadline.
This headroom is a practical default, not a guarantee for every analysis
duration. Setup preserves positive timeout values already in the `rea` entry.
You can edit `startupTimeoutMs` and `toolTimeoutMs` there; doctor keeps positive
@@ -245,6 +248,9 @@ values in the REA entry, and doctor reports a non-blocking advisory when a tool
timeout is shorter than the known provider allowance. An explicitly selected
JEB request budget also contributes to the allowance. A configured IDA
registration's parsed `timeoutMs` contributes for `ida` or `auto` selection.
An explicit `REA_JADX_OPERATION_TIMEOUT_MS` is forwarded with the JADX
registration but does not contribute; raise `tool_timeout_sec` to cover a
longer selected JADX deadline.
These values are client-side deadlines, so increase them in `~/.codex/config.toml` if a
workload needs more time. Devin honors `XDG_CONFIG_HOME` for its
`devin/mcp_config.json` on non-Windows systems.
+44
View File
@@ -3,9 +3,11 @@ import { access, realpath, stat } from "node:fs/promises";
import { delimiter, isAbsolute, join, resolve } from "node:path";
import { inspectWindowsNativeReadiness } from "../process/WindowsAuthority.js";
import { systemWindowsNativeAuthority } from "../windows/WindowsNativeLoader.js";
import { providerStartupTimeoutSchema } from "../config/providerStartupTimeout.js";
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
import type { ProviderAvailability } from "../application/AnalysisProvider.js";
import { ZipArtifactReader } from "../artifacts/ZipArtifactReader.js";
import {
@@ -14,10 +16,12 @@ import {
} from "../process/ExecFileOutput.js";
import type { DoctorProviderCheck } from "../application/Doctor.js";
import {
DEFAULT_JADX_OPERATION_TIMEOUT_MS,
JADX_JAR_CONFIGURATION_REMEDIATION,
JADX_JAVA_PROBE_REMEDIATION,
JADX_JAVA_RUNTIME_REMEDIATION,
JADX_JVM_OPTIONS_REMEDIATION,
JADX_OPERATION_TIMEOUT_REMEDIATION,
JADX_UNSUPPORTED_HOST_REMEDIATION,
JADX_WINDOWS_NATIVE_REMEDIATION,
} from "./JadxRelease.js";
@@ -57,6 +61,7 @@ export type JadxFailureKind =
| "java_runtime"
| "java_home"
| "jvm_options"
| "operation_timeout"
| "jar"
| "java_probe"
| "unsupported_host";
@@ -68,6 +73,7 @@ export const JADX_FAILURE_KINDS: readonly JadxFailureKind[] = [
"java_runtime",
"java_home",
"jvm_options",
"operation_timeout",
"jar",
"java_probe",
"unsupported_host",
@@ -114,6 +120,11 @@ export const projectJadxFailureOutcome = (
remediation: JADX_JVM_OPTIONS_REMEDIATION,
classification: drift,
};
case "operation_timeout":
return {
remediation: JADX_OPERATION_TIMEOUT_REMEDIATION,
classification: drift,
};
case "jar":
return {
remediation: JADX_JAR_CONFIGURATION_REMEDIATION,
@@ -155,6 +166,7 @@ export const inspectJadxAvailability = async (
signal?: AbortSignal,
): Promise<ProviderAvailability> => {
try {
const operationTimeoutMs = readOperationTimeout(environment);
const configuration = await readJadxConfigurationInputs(
environment,
signal,
@@ -169,6 +181,7 @@ export const inspectJadxAvailability = async (
java_executable: configuration.java,
jdk_compiler: true,
jar_path: configuration.jar,
operation_timeout_ms: operationTimeoutMs,
},
};
} catch (cause) {
@@ -184,6 +197,22 @@ export const inspectJadxAvailability = async (
}
};
/**
* Resolve the whole-operation deadline before any admission work starts, so
* one value bounds the provider operation and each upstream call.
*/
export const resolveJadxOperationTimeout = (
environment: Readonly<Record<string, string | undefined>>,
operation: AndroidOperation,
): Result<number, AnalysisCapabilityUnavailableError> => {
try {
return ok(readOperationTimeout(environment));
} catch (cause) {
if (!(cause instanceof JadxConfigurationFailure)) throw cause;
return err(projectConfigurationFailure(operation, cause));
}
};
/** Resolve caller-selected paths and JVM options without probing their contents. */
export const resolveJadxConfigurationInputs = async (
environment: Readonly<Record<string, string | undefined>>,
@@ -508,6 +537,21 @@ const resolveJvmArguments = (
return result;
};
const readOperationTimeout = (
environment: Readonly<Record<string, string | undefined>>,
): number => {
const value = environment.REA_JADX_OPERATION_TIMEOUT_MS;
if (value === undefined) return DEFAULT_JADX_OPERATION_TIMEOUT_MS;
const parsed = providerStartupTimeoutSchema.safeParse(value);
if (parsed.success) return parsed.data;
throw configurationFailure(
"open_options_invalid",
`REA_JADX_OPERATION_TIMEOUT_MS must be a decimal millisecond count from 1 to 2147483647; received ${value}.`,
{ setting: "REA_JADX_OPERATION_TIMEOUT_MS", value },
"operation_timeout",
);
};
const inspectJavaRuntime = async (
java: string,
environment: Readonly<Record<string, string | undefined>>,
+8
View File
@@ -49,6 +49,10 @@ export const inspectJadxProvider = async (
"REA_JADX_ACTIVE_PROCESSOR_COUNT",
environment.REA_JADX_ACTIVE_PROCESSOR_COUNT,
),
...optionalEntry(
"REA_JADX_OPERATION_TIMEOUT_MS",
environment.REA_JADX_OPERATION_TIMEOUT_MS,
),
},
checks: [configurationCheck(configured, availability, jarPath)],
};
@@ -70,6 +74,7 @@ const configurationCheck = (
): DoctorProviderCheck => {
const code = availability.status === "unavailable" ? availability.code : null;
if (availability.status === "available" && jarPath !== undefined) {
const operationTimeout = availability.diagnostics["operation_timeout_ms"];
return {
name: "configuration",
ok: true,
@@ -77,6 +82,9 @@ const configurationCheck = (
detail: `JADX MCP bridge JAR validated at ${jarPath}; its path is recorded for agent registrations.`,
remediation: null,
classification: "config_drift",
...(typeof operationTimeout === "number"
? { details: { operation_timeout_ms: operationTimeout } }
: {}),
};
}
// The failure kind is tagged at the configuration boundary; project its
+15 -5
View File
@@ -34,13 +34,13 @@ import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
import {
inspectJadxAvailability,
resolveJadxConfigurationInputs,
resolveJadxOperationTimeout,
validateJadxConfiguration,
} from "./JadxConfiguration.js";
import type { JadxLauncher } from "./JadxMcpTransport.js";
import { JadxSession } from "./JadxSession.js";
const OPERATION_TIMEOUT_MS = 120_000;
const BRIDGE_SOURCE = fileURLToPath(
new URL("../../bridge/android/ReaJadxBridge.java", import.meta.url),
);
@@ -65,10 +65,11 @@ const executionError = (context: {
cause: unknown;
request: AndroidRequest;
timeout: AbortSignal;
timeoutMs: number;
signal: AbortSignal | undefined;
session: JadxSession | undefined;
}): AnalysisError => {
const { cause, request, timeout, signal, session } = context;
const { cause, request, timeout, timeoutMs, signal, session } = context;
if (cause instanceof RegularFileCleanupFailure) {
const primaryCause =
cause.outcome.kind === "failed"
@@ -86,7 +87,7 @@ const executionError = (context: {
: timeout.aborted
? new AnalysisTimeoutError(
request.operation,
OPERATION_TIMEOUT_MS,
timeoutMs,
primaryOptions,
)
: primaryCause instanceof AnalysisError
@@ -114,7 +115,7 @@ const executionError = (context: {
if (signal?.aborted === true)
return new AnalysisCancelledError(request.operation, { cause });
if (timeout.aborted)
return new AnalysisTimeoutError(request.operation, OPERATION_TIMEOUT_MS);
return new AnalysisTimeoutError(request.operation, timeoutMs);
if (cause instanceof AnalysisError) return cause;
const protocolFailure = session?.transport.failureReason();
if (protocolFailure !== undefined && protocolFailure !== null)
@@ -440,7 +441,14 @@ export class JadxProvider implements AndroidAnalysisPort {
request: AndroidRequest,
options?: ExecutionOptions,
): Promise<Outcome> {
const timeout = AbortSignal.timeout(OPERATION_TIMEOUT_MS);
// The deadline is fixed per provider environment; an unusable value is
// refused before any file handle, workspace, or engine is acquired.
const timeoutMs = resolveJadxOperationTimeout(
this.environment,
request.operation,
);
if (!timeoutMs.ok) return timeoutMs;
const timeout = AbortSignal.timeout(timeoutMs.value);
const signal =
options?.signal === undefined
? timeout
@@ -596,6 +604,7 @@ export class JadxProvider implements AndroidAnalysisPort {
jarHash: retained.jarHash,
bridgeHash: retained.bridgeHash,
signal: activeSignal,
timeoutMs: timeoutMs.value,
}),
);
activeSignal.throwIfAborted();
@@ -606,6 +615,7 @@ export class JadxProvider implements AndroidAnalysisPort {
cause,
request,
timeout,
timeoutMs: timeoutMs.value,
signal: options?.signal,
session,
});
+6
View File
@@ -23,6 +23,12 @@ export const JADX_JAVA_RUNTIME_REMEDIATION =
export const JADX_JVM_OPTIONS_REMEDIATION =
"Set REA_JADX_HEAP_MIB to a positive integer (heap size in MiB) and REA_JADX_ACTIVE_PROCESSOR_COUNT to a positive integer within the JVM's signed 32-bit range.";
/** Whole-operation deadline used when REA_JADX_OPERATION_TIMEOUT_MS is unset. */
export const DEFAULT_JADX_OPERATION_TIMEOUT_MS = 120_000;
/** Remediation when the JADX operation deadline override is malformed. */
export const JADX_OPERATION_TIMEOUT_REMEDIATION = `Set REA_JADX_OPERATION_TIMEOUT_MS to a decimal millisecond count from 1 to 2147483647, or unset it to keep the ${String(DEFAULT_JADX_OPERATION_TIMEOUT_MS)} ms default.`;
/** Remediation when the host cannot run the Android JADX subprocess. */
export const JADX_UNSUPPORTED_HOST_REMEDIATION =
"Use Linux, macOS, or Windows x64 with its bundled native controls for Android JADX analysis.";
+7 -3
View File
@@ -69,8 +69,10 @@ export class JadxSession {
readonly jarHash: string;
readonly bridgeHash: string;
readonly signal: AbortSignal;
/** Selected whole-operation deadline; also bounds each upstream call. */
readonly timeoutMs: number;
}): Promise<AnalysisExecution> {
const { request, target, snapshot, jarHash, signal } = context;
const { request, target, snapshot, jarHash, signal, timeoutMs } = context;
this.#partialContext = {
operation: request.operation,
selectedPath: request.input.path,
@@ -105,7 +107,7 @@ export class JadxSession {
request.operation,
`Expected ${JADX_BRIDGE_IDENTITY.name} ${JADX_BRIDGE_IDENTITY.version}; server reported ${server?.name ?? "unknown"} ${server?.version ?? "unknown"}.`,
);
const tools = this.#tools(request, target, signal);
const tools = this.#tools(request, target, signal, timeoutMs);
const runtime = jadxRuntimeSchema.parse(
await tools.json("rea_jvm_status", {}),
);
@@ -206,15 +208,17 @@ export class JadxSession {
request: AndroidRequest,
target: BinaryTarget,
signal: AbortSignal,
timeoutMs: number,
): JadxToolPort {
const call = async (
name: string,
input: Readonly<Record<string, JsonValue>>,
): Promise<string> => {
signal.throwIfAborted();
// Keep an explicit bound: omitting it falls back to the SDK's 60-second default.
const response = await this.#client.callTool(
{ name, arguments: input },
{ timeout: 120_000, signal },
{ timeout: timeoutMs, signal },
);
const observedResponse = jsonValueSchema.parse(response);
const call = { operation: name, input, response: observedResponse };
@@ -387,3 +387,45 @@ it("tags unavailable diagnostics with the boundary failure kind", async () => {
if (availability.status !== "unavailable") throw new Error("unreachable");
expect(availability.diagnostics["failure_kind"]).toBe("jar");
});
it("reports the effective operation deadline and rejects unusable values", async () => {
const root = await createTestTempDirectory("rea-jadx-deadline-");
const jar = join(root, "jadx-headless-mcp.jar");
const javaHome = await createFakeJdk(root);
await writeJadxJarInventory(jar);
const environment = { REA_JADX_MCP_JAR: jar, JAVA_HOME: javaHome };
for (const [value, expected] of [
[undefined, 120_000],
["1", 1],
["900000", 900_000],
["2147483647", 2_147_483_647],
] as const)
await expect(
inspectJadxAvailability({
...environment,
...(value === undefined
? {}
: { REA_JADX_OPERATION_TIMEOUT_MS: value }),
}),
).resolves.toMatchObject({
status: "available",
diagnostics: { operation_timeout_ms: expected },
});
// Node clamps timer delays above the signed 32-bit range to 1 ms.
for (const value of ["", "0", "-1", "1.5", "1e3", " 5", "abc", "2147483648"])
await expect(
inspectJadxAvailability({
...environment,
REA_JADX_OPERATION_TIMEOUT_MS: value,
}),
).resolves.toMatchObject({
status: "unavailable",
code: "open_options_invalid",
reason: expect.stringContaining("REA_JADX_OPERATION_TIMEOUT_MS"),
diagnostics: {
setting: "REA_JADX_OPERATION_TIMEOUT_MS",
value,
failure_kind: "operation_timeout",
},
});
});
+49
View File
@@ -33,6 +33,7 @@ it("publishes REA_JADX_MCP_JAR as registration environment when the configured J
ok: true,
code: null,
classification: "config_drift",
details: { operation_timeout_ms: 120_000 },
},
],
});
@@ -214,6 +215,54 @@ it("publishes validated JVM resource overrides in the registration environment",
});
});
it("publishes and reports a validated operation deadline", async () => {
const root = await createTestTempDirectory("rea-jadx-doctor-deadline-");
const jar = join(root, "jadx-headless-mcp.jar");
const javaHome = await createFakeJdk(root);
await writeJadxJarInventory(jar);
const inspection = await inspectJadxProvider({
REA_JADX_MCP_JAR: jar,
JAVA_HOME: javaHome,
REA_JADX_OPERATION_TIMEOUT_MS: "900000",
});
expect(inspection.registrationEnvironment).toEqual({
REA_JADX_MCP_JAR: jar,
JAVA_HOME: javaHome,
REA_JADX_OPERATION_TIMEOUT_MS: "900000",
});
expect(inspection.checks[0]).toMatchObject({
ok: true,
details: { operation_timeout_ms: 900_000 },
});
});
it("guides deadline correction when REA_JADX_OPERATION_TIMEOUT_MS is invalid", async () => {
const root = await createTestTempDirectory("rea-jadx-doctor-bad-deadline-");
const jar = join(root, "jadx-headless-mcp.jar");
const javaHome = await createFakeJdk(root);
await writeJadxJarInventory(jar);
const inspection = await inspectJadxProvider({
REA_JADX_MCP_JAR: jar,
JAVA_HOME: javaHome,
REA_JADX_OPERATION_TIMEOUT_MS: "2147483648",
});
expect(inspection.available).toBe(false);
expect(inspection.registrationEnvironment).toEqual({});
expect(inspection.checks[0]).toMatchObject({
ok: false,
code: "open_options_invalid",
classification: "config_drift",
});
expect(inspection.checks[0]?.remediation).toContain(
"REA_JADX_OPERATION_TIMEOUT_MS",
);
expect(inspection.checks[0]?.remediation).not.toContain("REA_JADX_MCP_JAR");
});
it("guides JDK selection when the Java runtime is older than 17", async () => {
const root = await createTestTempDirectory("rea-jadx-doctor-oldjdk-");
const jar = join(root, "jadx-headless-mcp.jar");
+147 -1
View File
@@ -1,20 +1,25 @@
import { expect, it as test, vi } from "vitest";
import { SdkErrorCode } from "@modelcontextprotocol/client";
import { expect, it as test, onTestFinished, vi } from "vitest";
import { access, readFile, truncate, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
import { AndroidAnalysisService } from "../../../src/application/android/AndroidAnalysisService.js";
import { JadxProvider } from "../../../src/android/JadxProvider.js";
import { JadxSession } from "../../../src/android/JadxSession.js";
import { androidResultSchemas } from "../../../src/domain/android/androidAnalysis.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { snapshotAndroidEngine } from "../../../src/android/AndroidTargetSnapshot.js";
import { hashStableFile } from "../../../src/filesystem/StableFileHash.js";
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
import { RegularFileCleanupFailure } from "../../../src/filesystem/OwnedRegularFile.js";
import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js";
import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
import {
createJadxProtocolFixture as setup,
verifyJadxFixtureCleanup as verifyCleanup,
} from "../../fixtures/android/jadx.js";
import { writeJadxJarInventory } from "../../fixtures/android/jadxJar.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const it = test.skipIf(process.platform === "win32");
@@ -668,6 +673,147 @@ it("cancels active and queued requests, cleans ownership and never launches a qu
await verifyCleanup(launches);
});
it("ends each queued stalled operation at the caller-selected deadline and cleans ownership", async () => {
const { service, apk, launches, provider } = await setup(
"stall",
undefined,
undefined,
{ REA_JADX_OPERATION_TIMEOUT_MS: "1500" },
);
const started = performance.now();
// The second request waits behind the first; its deadline starts at the
// queue front, so it gets its own engine launch and runtime reply.
const outcomes = await Promise.all([
service.execute("inspect_android_package", { path: apk }),
service.execute("inspect_android_package", { path: apk }),
]);
expect(performance.now() - started).toBeLessThan(20_000);
for (const outcome of outcomes) {
expect(outcome).toMatchObject({
ok: false,
error: {
_tag: "AnalysisTimeoutError",
timeoutMs: 1500,
partialObservation: {
calls: [expect.objectContaining({ operation: "rea_jvm_status" })],
},
},
});
if (outcome.ok) throw new Error("Expected the selected deadline");
expect(projectAnalysisError(outcome.error)).toMatchObject({
code: "provider_timeout",
details: { timeout_ms: 1500 },
});
}
expect(launches).toHaveLength(2);
// The deadline path itself releases ownership before provider shutdown.
await verifyCleanup(launches);
await provider.close();
});
it("completes an operation under the largest accepted deadline", async () => {
const { service, apk, provider, launches } = await setup(
"normal",
undefined,
undefined,
{ REA_JADX_OPERATION_TIMEOUT_MS: "2147483647" },
);
const outcome = await service.execute("inspect_android_package", {
path: apk,
});
expect(outcome.ok).toBe(true);
await provider.close();
await verifyCleanup(launches);
});
it.each(["", "2147483648"])(
"rejects REA_JADX_OPERATION_TIMEOUT_MS=%j before probing or launching Java",
async (value) => {
const { service, apk, launches, javaProbeCount } = await setup(
"normal",
undefined,
undefined,
{ REA_JADX_OPERATION_TIMEOUT_MS: value },
);
const outcome = await service.execute("inspect_android_package", {
path: apk,
});
expect(outcome).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCapabilityUnavailableError",
reason: expect.stringContaining("REA_JADX_OPERATION_TIMEOUT_MS"),
},
});
expect(launches).toHaveLength(0);
// The fake JDK records each module-listing probe; none may have run.
await expect(javaProbeCount()).rejects.toMatchObject({ code: "ENOENT" });
},
);
it("bounds each upstream call by the selected deadline instead of a fixed limit", async () => {
// Provider workflows cannot reach the former fixed 120-second call limit in
// a test, so exercise the session boundary that owns each upstream call.
const { apk } = await setup();
const target = await parseBinaryTarget(apk);
if (!target.ok) throw target.error;
const workspace = await createTestTempDirectory("rea-jadx-call-deadline-");
const pids: number[] = [];
const session = new JadxSession(
{
command: process.execPath,
arguments: [],
cwd: workspace,
hostEnvironment: process.env,
signal: new AbortController().signal,
},
async (options) => {
const spawned = await spawnOwnedProviderProcess({
...options,
command: process.execPath,
arguments: [
fileURLToPath(
new URL("../../fixtures/android/jadx-mcp.mjs", import.meta.url),
),
],
expectedCommand: process.execPath,
env: { ...options.env, REA_FAKE_JADX_MODE: "stall" },
});
if (spawned.process.pid !== undefined) pids.push(spawned.process.pid);
return spawned;
},
);
onTestFinished(() => session.close());
try {
const started = performance.now();
await expect(
session.execute({
request: { operation: "inspect_android_package", input: { path: apk } },
target: target.value,
snapshot: apk,
jarHash: "0".repeat(64),
bridgeHash: "0".repeat(64),
signal: new AbortController().signal,
timeoutMs: 300,
}),
).rejects.toMatchObject({
code: SdkErrorCode.RequestTimeout,
data: { timeout: 300 },
});
expect(performance.now() - started).toBeLessThan(10_000);
expect(session.partialObservation()?.calls).toEqual([
expect.objectContaining({ operation: "rea_jvm_status" }),
]);
} finally {
await session.close();
}
expect(pids).toHaveLength(1);
for (const pid of pids)
expect(() => process.kill(-pid, 0)).toThrowError(
expect.objectContaining({ code: "ESRCH" }),
);
});
it("reports missing explicit tools without trying to install or launch them", async () => {
const { apk } = await setup();
const provider = new JadxProvider({}, () => {
+9 -1
View File
@@ -28,6 +28,7 @@ export const createJadxProtocolFixture = async (
mode = "normal",
cleanupFailure?: () => boolean,
inventory?: { readonly classCount?: number },
settings: Readonly<Record<string, string>> = {},
) => {
const root = await createTestTempDirectory("rea-android-boundary-");
const apk = join(root, "fixture.apk");
@@ -52,10 +53,17 @@ export const createJadxProtocolFixture = async (
javaToolOptions: string | undefined;
legacyJavaOptions: string | undefined;
}[] = [];
// Inherit the host environment for process launch, but take JADX settings
// only from this fixture so a developer's shell cannot change the provider.
const environment: NodeJS.ProcessEnv = {
...process.env,
...Object.fromEntries(
Object.entries(process.env).filter(
([name]) => !name.startsWith("REA_JADX_"),
),
),
JAVA_HOME: javaHome,
REA_JADX_MCP_JAR: jar,
...settings,
};
const provider = createAndroidAnalysisProvider(
environment,