mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
* 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>
446 lines
14 KiB
TypeScript
446 lines
14 KiB
TypeScript
import { chmod, mkdir, realpath, symlink, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { expect, it as test } from "vitest";
|
|
|
|
import { inspectJadxProvider } from "../../../src/android/JadxDoctor.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { writeJadxJarInventory } from "../../fixtures/android/jadxJar.js";
|
|
import { writeOrderedZip } from "../../fixtures/artifactEntryOrder.js";
|
|
|
|
const it = test.skipIf(process.platform === "win32");
|
|
|
|
it("publishes REA_JADX_MCP_JAR as registration environment when the configured JAR validates", async () => {
|
|
const root = await createTestTempDirectory("rea-jadx-doctor-");
|
|
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,
|
|
});
|
|
|
|
expect(inspection).toMatchObject({
|
|
id: "jadx",
|
|
configured: true,
|
|
available: true,
|
|
providerVersion: null,
|
|
registrationEnvironment: { REA_JADX_MCP_JAR: await realpath(jar) },
|
|
checks: [
|
|
{
|
|
name: "configuration",
|
|
ok: true,
|
|
code: null,
|
|
classification: "config_drift",
|
|
details: { operation_timeout_ms: 120_000 },
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("publishes JAVA_HOME alongside the JAR when the configured JAR validates", async () => {
|
|
const root = await createTestTempDirectory("rea-jadx-doctor-javahome-");
|
|
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,
|
|
});
|
|
|
|
expect(inspection.registrationEnvironment).toEqual({
|
|
REA_JADX_MCP_JAR: jar,
|
|
JAVA_HOME: javaHome,
|
|
});
|
|
});
|
|
|
|
it("preserves the caller-selected JAR path when it is a symlink", async () => {
|
|
const root = await createTestTempDirectory("rea-jadx-doctor-symlink-");
|
|
const jar = join(root, "jadx-headless-mcp.jar");
|
|
const link = join(root, "current.jar");
|
|
const javaHome = await createFakeJdk(root);
|
|
await writeJadxJarInventory(jar);
|
|
await symlink(jar, link);
|
|
|
|
const inspection = await inspectJadxProvider({
|
|
REA_JADX_MCP_JAR: link,
|
|
JAVA_HOME: javaHome,
|
|
});
|
|
|
|
expect(inspection.available).toBe(true);
|
|
expect(inspection.registrationEnvironment["REA_JADX_MCP_JAR"]).toBe(link);
|
|
});
|
|
|
|
it("guides Java selection when the Java runtime is missing", async () => {
|
|
const root = await createTestTempDirectory("rea-jadx-doctor-remediation-");
|
|
const jar = join(root, "jadx-headless-mcp.jar");
|
|
await writeJadxJarInventory(jar);
|
|
|
|
const inspection = await inspectJadxProvider({
|
|
REA_JADX_MCP_JAR: jar,
|
|
JAVA_HOME: join(root, "missing-jdk"),
|
|
});
|
|
|
|
expect(inspection.checks[0]?.code).toBe("runtime_missing");
|
|
expect(inspection.checks[0]?.remediation).toContain("JAVA_HOME");
|
|
expect(inspection.checks[0]?.remediation).not.toContain("REA_JADX_MCP_JAR");
|
|
});
|
|
|
|
it("guides Java selection when JAVA_HOME is not an absolute path", async () => {
|
|
const root = await createTestTempDirectory("rea-jadx-doctor-options-");
|
|
const jar = join(root, "jadx-headless-mcp.jar");
|
|
await writeJadxJarInventory(jar);
|
|
|
|
const inspection = await inspectJadxProvider({
|
|
REA_JADX_MCP_JAR: jar,
|
|
JAVA_HOME: "relative/path",
|
|
});
|
|
|
|
expect(inspection.checks[0]?.code).toBe("open_options_invalid");
|
|
expect(inspection.checks[0]?.remediation).toContain("JAVA_HOME");
|
|
expect(inspection.checks[0]?.remediation).not.toContain("REA_JADX_MCP_JAR");
|
|
});
|
|
|
|
it("publishes no registration environment when REA_JADX_MCP_JAR is unset", async () => {
|
|
const inspection = await inspectJadxProvider({});
|
|
|
|
expect(inspection).toMatchObject({
|
|
id: "jadx",
|
|
configured: false,
|
|
available: false,
|
|
providerVersion: null,
|
|
registrationEnvironment: {},
|
|
checks: [
|
|
{
|
|
name: "configuration",
|
|
ok: false,
|
|
code: "not_configured",
|
|
classification: "missing_analysis_engine",
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("publishes no registration environment when the configured JAR fails validation", async () => {
|
|
const root = await createTestTempDirectory("rea-jadx-doctor-invalid-");
|
|
const jar = join(root, "engine.jar");
|
|
const javaHome = await createFakeJdk(root);
|
|
await writeFile(jar, "not a Java archive");
|
|
|
|
const inspection = await inspectJadxProvider({
|
|
REA_JADX_MCP_JAR: jar,
|
|
JAVA_HOME: javaHome,
|
|
});
|
|
|
|
expect(inspection).toMatchObject({
|
|
id: "jadx",
|
|
configured: true,
|
|
available: false,
|
|
registrationEnvironment: {},
|
|
checks: [
|
|
{
|
|
name: "configuration",
|
|
ok: false,
|
|
code: "version_unresolved",
|
|
classification: "config_drift",
|
|
},
|
|
],
|
|
});
|
|
expect(inspection.checks[0]?.detail).toContain("REA_JADX_MCP_JAR");
|
|
});
|
|
|
|
it("classifies a missing Java runtime as a missing dependency", async () => {
|
|
const root = await createTestTempDirectory("rea-jadx-doctor-runtime-");
|
|
const jar = join(root, "jadx-headless-mcp.jar");
|
|
await writeJadxJarInventory(jar);
|
|
|
|
const inspection = await inspectJadxProvider({
|
|
REA_JADX_MCP_JAR: jar,
|
|
JAVA_HOME: join(root, "missing-jdk"),
|
|
});
|
|
|
|
expect(inspection).toMatchObject({
|
|
configured: true,
|
|
available: false,
|
|
registrationEnvironment: {},
|
|
checks: [
|
|
{
|
|
name: "configuration",
|
|
ok: false,
|
|
code: "runtime_missing",
|
|
classification: "missing_dependency",
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it("guides JVM option correction when REA_JADX_HEAP_MIB is invalid", async () => {
|
|
const root = await createTestTempDirectory("rea-jadx-doctor-heap-");
|
|
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_HEAP_MIB: "not-a-number",
|
|
});
|
|
|
|
expect(inspection.checks[0]?.code).toBe("open_options_invalid");
|
|
expect(inspection.checks[0]?.remediation).toContain("REA_JADX_HEAP_MIB");
|
|
expect(inspection.checks[0]?.remediation).not.toContain("JAVA_HOME");
|
|
});
|
|
|
|
it("publishes validated JVM resource overrides in the registration environment", async () => {
|
|
const root = await createTestTempDirectory("rea-jadx-doctor-overrides-");
|
|
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_HEAP_MIB: "4096",
|
|
REA_JADX_ACTIVE_PROCESSOR_COUNT: "8",
|
|
});
|
|
|
|
expect(inspection.available).toBe(true);
|
|
expect(inspection.registrationEnvironment).toEqual({
|
|
REA_JADX_MCP_JAR: jar,
|
|
JAVA_HOME: javaHome,
|
|
REA_JADX_HEAP_MIB: "4096",
|
|
REA_JADX_ACTIVE_PROCESSOR_COUNT: "8",
|
|
});
|
|
});
|
|
|
|
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");
|
|
const javaHome = await createFakeJdk(root, "11");
|
|
await writeJadxJarInventory(jar);
|
|
|
|
const inspection = await inspectJadxProvider({
|
|
REA_JADX_MCP_JAR: jar,
|
|
JAVA_HOME: javaHome,
|
|
});
|
|
|
|
expect(inspection.checks[0]?.code).toBe("unsupported_version");
|
|
expect(inspection.checks[0]?.remediation).toContain("JDK");
|
|
expect(inspection.checks[0]?.remediation).not.toContain("REA_JADX_MCP_JAR");
|
|
});
|
|
|
|
it("guides JAR replacement when the JAR misses bridge classes", async () => {
|
|
const root = await createTestTempDirectory("rea-jadx-doctor-classes-");
|
|
const jar = join(root, "jadx-headless-mcp.jar");
|
|
const javaHome = await createFakeJdk(root);
|
|
await writeOrderedZip(jar, ["com/atxx/jhmcp/JadxSession.class"]);
|
|
|
|
const inspection = await inspectJadxProvider({
|
|
REA_JADX_MCP_JAR: jar,
|
|
JAVA_HOME: javaHome,
|
|
});
|
|
|
|
expect(inspection.checks[0]?.code).toBe("unsupported_version");
|
|
expect(inspection.checks[0]?.remediation).toContain("REA_JADX_MCP_JAR");
|
|
});
|
|
|
|
it("remediates an unsupported host with platform guidance", async () => {
|
|
const originalPlatform = process.platform;
|
|
Object.defineProperty(process, "platform", {
|
|
value: "sunos",
|
|
configurable: true,
|
|
});
|
|
try {
|
|
const inspection = await inspectJadxProvider({
|
|
REA_JADX_MCP_JAR: "/opt/jadx-headless-mcp.jar",
|
|
});
|
|
|
|
expect(inspection.checks[0]?.code).toBe("unsupported_host");
|
|
expect(inspection.checks[0]?.remediation).toContain("Linux");
|
|
} finally {
|
|
Object.defineProperty(process, "platform", {
|
|
value: originalPlatform,
|
|
configurable: true,
|
|
});
|
|
}
|
|
});
|
|
|
|
it("guides Windows native package correction when process ownership is unavailable", async () => {
|
|
const originalPlatform = process.platform;
|
|
const originalArch = process.arch;
|
|
Object.defineProperty(process, "platform", {
|
|
value: "win32",
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(process, "arch", { value: "x64", configurable: true });
|
|
try {
|
|
const inspection = await inspectJadxProvider({
|
|
REA_JADX_MCP_JAR: "/opt/jadx-headless-mcp.jar",
|
|
});
|
|
|
|
expect(inspection.checks[0]?.code).toBe("runtime_missing");
|
|
expect(inspection.checks[0]?.remediation).toContain(
|
|
"Windows x64 REA package",
|
|
);
|
|
expect(inspection.checks[0]?.remediation).not.toContain("JAVA_HOME");
|
|
} finally {
|
|
Object.defineProperty(process, "platform", {
|
|
value: originalPlatform,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(process, "arch", {
|
|
value: originalArch,
|
|
configurable: true,
|
|
});
|
|
}
|
|
});
|
|
|
|
it("guides Java probe correction when the module listing is unparseable", async () => {
|
|
const root = await createTestTempDirectory("rea-jadx-doctor-unparseable-");
|
|
const jar = join(root, "jadx-headless-mcp.jar");
|
|
await writeJadxJarInventory(jar);
|
|
// Fake JDK whose module listing lacks a parseable java.base version.
|
|
const javaHome = join(root, "fake-jdk");
|
|
const bin = join(javaHome, "bin");
|
|
const java = join(bin, "java");
|
|
await mkdir(bin, { recursive: true });
|
|
await writeFile(java, "#!/bin/sh\nprintf 'jdk.compiler@21\\n'\n");
|
|
await chmod(java, 0o700);
|
|
|
|
const inspection = await inspectJadxProvider({
|
|
REA_JADX_MCP_JAR: jar,
|
|
JAVA_HOME: javaHome,
|
|
});
|
|
|
|
expect(inspection.checks[0]?.code).toBe("version_unresolved");
|
|
expect(inspection.checks[0]?.remediation).toContain("java --list-modules");
|
|
expect(inspection.checks[0]?.remediation).not.toContain("REA_JADX_MCP_JAR");
|
|
});
|
|
|
|
const createFakeJdk = async (root: string, major = "21"): Promise<string> => {
|
|
const javaHome = join(root, "fake-jdk");
|
|
const bin = join(javaHome, "bin");
|
|
const java = join(bin, "java");
|
|
await mkdir(bin, { recursive: true });
|
|
await writeFile(
|
|
java,
|
|
`#!/bin/sh\nprintf 'java.base@${major}\\njdk.compiler@${major}\\n'\n`,
|
|
);
|
|
await chmod(java, 0o700);
|
|
return javaHome;
|
|
};
|
|
|
|
it("classifies missing Windows native artifacts as missing dependencies", async () => {
|
|
const originalPlatform = process.platform;
|
|
const originalArch = process.arch;
|
|
Object.defineProperty(process, "platform", {
|
|
value: "win32",
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(process, "arch", { value: "x64", configurable: true });
|
|
try {
|
|
const inspection = await inspectJadxProvider({
|
|
REA_JADX_MCP_JAR: "/opt/jadx-headless-mcp.jar",
|
|
});
|
|
|
|
expect(inspection.checks[0]?.code).toBe("runtime_missing");
|
|
expect(inspection.checks[0]?.classification).toBe("missing_dependency");
|
|
} finally {
|
|
Object.defineProperty(process, "platform", {
|
|
value: originalPlatform,
|
|
configurable: true,
|
|
});
|
|
Object.defineProperty(process, "arch", {
|
|
value: originalArch,
|
|
configurable: true,
|
|
});
|
|
}
|
|
});
|
|
|
|
it("classifies malformed JAVA_HOME as configuration drift", async () => {
|
|
const root = await createTestTempDirectory("rea-jadx-doctor-javahome-");
|
|
const jar = join(root, "jadx-headless-mcp.jar");
|
|
await writeJadxJarInventory(jar);
|
|
|
|
const inspection = await inspectJadxProvider({
|
|
REA_JADX_MCP_JAR: jar,
|
|
JAVA_HOME: "relative/path",
|
|
});
|
|
|
|
expect(inspection.checks[0]?.code).toBe("open_options_invalid");
|
|
expect(inspection.checks[0]?.classification).toBe("config_drift");
|
|
});
|
|
|
|
it("preserves Java probe diagnostics in doctor output", async () => {
|
|
const root = await createTestTempDirectory("rea-jadx-doctor-probe-");
|
|
const jar = join(root, "jadx-headless-mcp.jar");
|
|
await writeJadxJarInventory(jar);
|
|
const javaHome = join(root, "fake-jdk-unparseable");
|
|
const bin = join(javaHome, "bin");
|
|
const java = join(bin, "java");
|
|
await mkdir(bin, { recursive: true });
|
|
// Emits jdk.compiler but no parseable java.base version.
|
|
await writeFile(java, `#!/bin/sh\nprintf 'jdk.compiler@21\\n'\n`);
|
|
await chmod(java, 0o700);
|
|
|
|
const inspection = await inspectJadxProvider({
|
|
REA_JADX_MCP_JAR: jar,
|
|
JAVA_HOME: javaHome,
|
|
});
|
|
|
|
expect(inspection.checks[0]?.code).toBe("version_unresolved");
|
|
expect(inspection.checks[0]?.details).toMatchObject({
|
|
java_executable: expect.any(String),
|
|
});
|
|
});
|