mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
d124809950
A supplied non-absolute REA_JADX_MCP_JAR was folded into the unset case, emitting not_configured with jar_configured: false and dropping the caller-selected value. The JAR parser now keeps the two outcomes distinct: unset stays not_configured, while a supplied non-absolute value (including an empty string) fails with open_options_invalid, jar_configured: true, the exact rejected jar_path and the absolute-path requirement. CLI, MCP and doctor derive their output from that one boundary result. Fixes #5844 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
481 lines
16 KiB
TypeScript
481 lines
16 KiB
TypeScript
import { createInspectionResources } from "../../fixtures/inspectionResources.js";
|
|
import { chmod, mkdir, realpath, symlink, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
|
|
import { expect, it as test, onTestFinished } from "vitest";
|
|
|
|
import { inspectJadxProvider as inspectProvider } 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 inspectionOwner = () => {
|
|
const resources = createInspectionResources();
|
|
onTestFinished(() => resources.close());
|
|
return resources.owner;
|
|
};
|
|
const inspectJadxProvider = (
|
|
environment: Parameters<typeof inspectProvider>[0],
|
|
) => inspectProvider(environment, inspectionOwner());
|
|
|
|
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("reports a relative REA_JADX_MCP_JAR as a rejected selection, not an unset one", async () => {
|
|
const inspection = await inspectJadxProvider({
|
|
REA_JADX_MCP_JAR: "tools/jadx-headless-mcp.jar",
|
|
});
|
|
|
|
expect(inspection).toMatchObject({
|
|
id: "jadx",
|
|
configured: true,
|
|
available: false,
|
|
registrationEnvironment: {},
|
|
checks: [
|
|
{
|
|
name: "configuration",
|
|
ok: false,
|
|
code: "open_options_invalid",
|
|
classification: "config_drift",
|
|
},
|
|
],
|
|
});
|
|
expect(inspection.checks[0]?.detail).toContain(
|
|
'REA_JADX_MCP_JAR must be an absolute path; received "tools/jadx-headless-mcp.jar".',
|
|
);
|
|
expect(inspection.checks[0]?.remediation).toContain("REA_JADX_MCP_JAR");
|
|
});
|
|
|
|
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),
|
|
});
|
|
});
|