diff --git a/docs/testing.md b/docs/testing.md index f684673e8a..7adcf3123c 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -307,7 +307,7 @@ prove behavior on the verified Ghidra 12.1.4 and JDK 21 build. | Ghidra lane | Supported runner/target | Additional local tools | | ------------------------------------------ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------- | -| `npm run verify:ghidra` | Linux x64/arm64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 | +| `npm run verify:ghidra` | Linux x64/arm64 ELF or macOS x64/arm64 Mach-O | Host C compiler, Ghidra 12.1.4, and full JDK 21 | | `npm run verify:ghidra:switch` | Linux x64 ELF; GCC/Clang optimized and stripped switch fixtures | GCC, Clang, GNU nm/objdump/strip, Ghidra 12.1.4, and full JDK 21 | | `npm run verify:ghidra:aarch64-jump-table` | Any supported Ghidra host; AArch64 ELF; byte/halfword tables; host ARM64 Mach-O | Clang with AArch64 target support, Ghidra 12.1.4, and full JDK 21 | | `npm run verify:ghidra:cross-format` | Any supported Ghidra host; also analyzes AArch64 ELF, x86-64 PE, and x86-64 Mach-O | `clang`, LLD, and `lld-link` in addition to host-lane prerequisites | diff --git a/src/android/JadxConfiguration.ts b/src/android/JadxConfiguration.ts index 500571546c..f423bf2280 100644 --- a/src/android/JadxConfiguration.ts +++ b/src/android/JadxConfiguration.ts @@ -247,8 +247,7 @@ const inspectJavaRuntime = async ( java_executable: java, phase: "module-inspection", exit_code: typeof output.code === "number" ? output.code : null, - process_error_code: - typeof output.code === "string" ? output.code : null, + process_error_code: typeof output.code === "string" ? output.code : null, signal: output.signal, killed: output.killed, stdout: output.stdout, diff --git a/tests/acceptance/nonBinaryComposition.test.ts b/tests/acceptance/nonBinaryComposition.test.ts index f7c28fa410..0cb337de7f 100644 --- a/tests/acceptance/nonBinaryComposition.test.ts +++ b/tests/acceptance/nonBinaryComposition.test.ts @@ -34,33 +34,43 @@ const connect = async (environment: NodeJS.ProcessEnv) => { return client; }; -cliTest.skipIf(process.platform === "win32")( - "preserves the selected APK configuration through the compiled CLI and MCP factories", - async ({ cli }) => { - const root = await createTestTempDirectory("rea-android-composition-"); - const apk = join(root, "fixture.apk"); - await writeOrderedZip(apk, ["AndroidManifest.xml", "classes.dex"]); - const environment = { - REA_LOG_LEVEL: "silent", - JAVA_HOME: "relative-fixture-jdk", - }; - const cliResult = await cli.run({ - arguments: ["inspect-android-package", apk, "--json"], - environment, - timeoutMs: 10_000, - }); - expect(cliResult.exitCode).toBe(1); - expect(cliResult.json).toMatchObject({ code: "capability_unavailable" }); - const client = await connect(environment); - const response = await client.callTool({ - name: "inspect_android_package", - arguments: { path: apk }, - }); - expect(response.isError).toBe(true); - expect(response.structuredContent).toEqual({ error: cliResult.json }); - expect(JSON.stringify(response.structuredContent)).toContain("JAVA_HOME"); - }, -); +for (const { jarConfigured, setting } of [ + { jarConfigured: false, setting: "REA_JADX_MCP_JAR" }, + { jarConfigured: true, setting: "JAVA_HOME" }, +]) + cliTest.skipIf(process.platform === "win32")( + `preserves the APK ${setting} failure through the compiled CLI and MCP factories`, + async ({ cli }) => { + const root = await createTestTempDirectory("rea-android-composition-"); + const apk = join(root, "fixture.apk"); + await writeOrderedZip(apk, ["AndroidManifest.xml", "classes.dex"]); + const jar = join(root, "jadx-headless-mcp.jar"); + await writeFile( + jar, + "fixture jar; validation must fail before execution", + ); + const environment = { + REA_LOG_LEVEL: "silent", + JAVA_HOME: "relative-fixture-jdk", + ...(jarConfigured ? { REA_JADX_MCP_JAR: jar } : {}), + }; + const cliResult = await cli.run({ + arguments: ["inspect-android-package", apk, "--json"], + environment, + timeoutMs: 10_000, + }); + expect(cliResult.exitCode).toBe(1); + expect(cliResult.json).toMatchObject({ code: "capability_unavailable" }); + const client = await connect(environment); + const response = await client.callTool({ + name: "inspect_android_package", + arguments: { path: apk }, + }); + expect(response.isError).toBe(true); + expect(response.structuredContent).toEqual({ error: cliResult.json }); + expect(JSON.stringify(response.structuredContent)).toContain(setting); + }, + ); cliTest.skipIf(process.platform !== "linux")( "preserves the missing firmware command reason through both caller paths", diff --git a/tests/boundary/android/jadxConfiguration.test.ts b/tests/boundary/android/jadxConfiguration.test.ts index e6194ca249..0f6a8654a7 100644 --- a/tests/boundary/android/jadxConfiguration.test.ts +++ b/tests/boundary/android/jadxConfiguration.test.ts @@ -195,11 +195,16 @@ it("distinguishes an unreported Java version from an unsupported old version", a ); await chmod(join(unresolvedHome, "bin", "java"), 0o700); await expect( - inspectJadxAvailability({ REA_JADX_MCP_JAR: jar, JAVA_HOME: unresolvedHome }), + inspectJadxAvailability({ + REA_JADX_MCP_JAR: jar, + JAVA_HOME: unresolvedHome, + }), ).resolves.toMatchObject({ status: "unavailable", code: "version_unresolved", - reason: expect.stringContaining("did not report a parseable java.base version"), + reason: expect.stringContaining( + "did not report a parseable java.base version", + ), diagnostics: { stdout: "jdk.compiler@21\n", java_major: null }, }); diff --git a/tests/boundary/cli/mcpDoctorHelp.test.ts b/tests/boundary/cli/mcpDoctorHelp.test.ts index 8f91efeca7..e619d819d2 100644 --- a/tests/boundary/cli/mcpDoctorHelp.test.ts +++ b/tests/boundary/cli/mcpDoctorHelp.test.ts @@ -7,11 +7,15 @@ const execute = promisify(execFile); const runCli = async (arguments_: readonly string[]) => { try { - const result = await execute(process.execPath, ["scripts/rea.mjs", ...arguments_], { - cwd: process.cwd(), - env: { ...process.env, REA_LOG_LEVEL: "silent" }, - timeout: 15_000, - }); + const result = await execute( + process.execPath, + ["scripts/rea.mjs", ...arguments_], + { + cwd: process.cwd(), + env: { ...process.env, REA_LOG_LEVEL: "silent" }, + timeout: 15_000, + }, + ); return { exitCode: 0, ...result }; } catch (cause: unknown) { if ( @@ -46,5 +50,7 @@ it("shows MCP doctor help without starting a server and keeps option errors acti const invalid = await runCli(["mcp", "doctor", "--not-an-option"]); expect(invalid.exitCode).toBe(1); - expect(invalid.stdout).toContain("Unknown mcp doctor option: --not-an-option"); + expect(invalid.stdout).toContain( + "Unknown mcp doctor option: --not-an-option", + ); }); diff --git a/tests/boundary/providers/ghidra/ghidraLauncher.test.ts b/tests/boundary/providers/ghidra/ghidraLauncher.test.ts index 66672324a7..1e009c4604 100644 --- a/tests/boundary/providers/ghidra/ghidraLauncher.test.ts +++ b/tests/boundary/providers/ghidra/ghidraLauncher.test.ts @@ -201,61 +201,63 @@ describe("Ghidra headless launcher", () => { it.skipIf(process.platform !== "win32")( "keeps authority in a private descriptor and isolates Ghidra state through the batch launcher", async () => { - vi.stubEnv("GHIDRA_JAVA_OPTIONS", "-javaagent:/unapproved/agent.jar"); - vi.stubEnv("JAVA_TOOL_OPTIONS", "-Duser.home=/unapproved/home"); - vi.stubEnv("JDK_JAVA_OPTIONS", "-XX:MaxRAMPercentage=99"); - vi.stubEnv("_JAVA_OPTIONS", "-Xmx99G"); - const parent = await createTestTempDirectory("rea-launcher-test-"); - const runtime = await createGhidraTestRuntime(parent); - runtimes.push(runtime); - const runtimeRoot = runtime.path; - const token = "secret-token-that-must-not-leak"; - const javaHome = "C:\\Java\\jdk-21"; - const launcher = new GhidraHeadlessLauncher({ - analyzeHeadlessPath: fixturePath, - // POSIX follows the real inspected-JVM route; Windows uses the official - // batch script because its launcher contract differs. - platform: "win32", - javaHome, - bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java", - }); - const launched = await launcher.launch({ - runtimeRoot, - transport: "unix-socket", - endpointPath: join(runtimeRoot, "bridge.sock"), - token, - runId: "d6fcbb66-e829-4ff6-a535-0035aec63139", - targetPath: "/tmp/fixture", - targetSha256: "b".repeat(64), - providerVersion: "12.1.4", - profileDigest: "a".repeat(64), - }); - if (!launched.ok) throw launched.error; - const capturePath = join(runtimeRoot, "launch-capture.json"); - await vi.waitFor(() => access(`${capturePath}.ready`), { timeout: 10_000 }); - const capture = launchCaptureSchema.parse( - JSON.parse(await readFile(capturePath, "utf8")), - ); - const encodedArguments = JSON.stringify(capture.arguments); - const encodedEnvironment = JSON.stringify(capture.environment); - expect(encodedArguments).not.toContain(token); - expect(encodedEnvironment).not.toContain(token); - expect(capture).toMatchObject({ - ...(process.platform === "win32" ? {} : { descriptor_mode: 0o600 }), - descriptor_has_token: true, - }); - expectIsolatedEnvironment(capture.environment, runtimeRoot, javaHome); - expect(capture.environment.GHIDRA_HEADLESS_JAVA_OPTIONS).toBe( - "", - ); - if (process.platform !== "win32") - expect( - (await stat(join(runtimeRoot, "ownership.json"))).mode & 0o777, - ).toBe(0o600); + vi.stubEnv("GHIDRA_JAVA_OPTIONS", "-javaagent:/unapproved/agent.jar"); + vi.stubEnv("JAVA_TOOL_OPTIONS", "-Duser.home=/unapproved/home"); + vi.stubEnv("JDK_JAVA_OPTIONS", "-XX:MaxRAMPercentage=99"); + vi.stubEnv("_JAVA_OPTIONS", "-Xmx99G"); + const parent = await createTestTempDirectory("rea-launcher-test-"); + const runtime = await createGhidraTestRuntime(parent); + runtimes.push(runtime); + const runtimeRoot = runtime.path; + const token = "secret-token-that-must-not-leak"; + const javaHome = "C:\\Java\\jdk-21"; + const launcher = new GhidraHeadlessLauncher({ + analyzeHeadlessPath: fixturePath, + // POSIX follows the real inspected-JVM route; Windows uses the official + // batch script because its launcher contract differs. + platform: "win32", + javaHome, + bridgeScriptPath: "/package/bridge/ReaGhidraBridge.java", + }); + const launched = await launcher.launch({ + runtimeRoot, + transport: "unix-socket", + endpointPath: join(runtimeRoot, "bridge.sock"), + token, + runId: "d6fcbb66-e829-4ff6-a535-0035aec63139", + targetPath: "/tmp/fixture", + targetSha256: "b".repeat(64), + providerVersion: "12.1.4", + profileDigest: "a".repeat(64), + }); + if (!launched.ok) throw launched.error; + const capturePath = join(runtimeRoot, "launch-capture.json"); + await vi.waitFor(() => access(`${capturePath}.ready`), { + timeout: 10_000, + }); + const capture = launchCaptureSchema.parse( + JSON.parse(await readFile(capturePath, "utf8")), + ); + const encodedArguments = JSON.stringify(capture.arguments); + const encodedEnvironment = JSON.stringify(capture.environment); + expect(encodedArguments).not.toContain(token); + expect(encodedEnvironment).not.toContain(token); + expect(capture).toMatchObject({ + ...(process.platform === "win32" ? {} : { descriptor_mode: 0o600 }), + descriptor_has_token: true, + }); + expectIsolatedEnvironment(capture.environment, runtimeRoot, javaHome); + expect(capture.environment.GHIDRA_HEADLESS_JAVA_OPTIONS).toBe(""); + if (process.platform !== "win32") + expect( + (await stat(join(runtimeRoot, "ownership.json"))).mode & 0o777, + ).toBe(0o600); - const cleaned = await launched.value.cleanup?.(); - expect(cleaned).toMatchObject({ cleaned: true }); - await expect(access(join(runtimeRoot, "project"))).resolves.toBeUndefined(); + const cleaned = await launched.value.cleanup?.(); + expect(cleaned).toMatchObject({ cleaned: true }); + await expect( + access(join(runtimeRoot, "project")), + ).resolves.toBeUndefined(); }, ); }); diff --git a/tests/conformance/contracts/analysisErrorProjection.test.ts b/tests/conformance/contracts/analysisErrorProjection.test.ts index 8eb58e5a31..e98804bc0c 100644 --- a/tests/conformance/contracts/analysisErrorProjection.test.ts +++ b/tests/conformance/contracts/analysisErrorProjection.test.ts @@ -10,7 +10,7 @@ import { projectAnalysisError } from "../../../src/domain/analysisErrorProjectio import { type AnalysisError } from "../../../src/domain/analysisErrorBase.js"; describe("analysis error projection contract", () => { - it("accepts every closed error-reason variant without exposing diagnostics", () => { + it("accepts every closed error-reason variant", () => { const variants: AnalysisError[] = [ ...( [ @@ -40,7 +40,9 @@ describe("analysis error projection contract", () => { ).map((reason) => new UnknownRegistryError(reason)), ...(["capture_failed", "cleanup_incomplete", "cancelled"] as const).map( (reason) => - new ProcessCaptureError("SECRET capture diagnostic", { reason }), + new ProcessCaptureError("API_TOKEN=selected capture diagnostic", { + reason, + }), ), ...( [ @@ -62,7 +64,11 @@ describe("analysis error projection contract", () => { JSON.stringify({ variant, projected }), ).toMatchObject({ success: true }); expect(projected.code).toMatch(/^[a-z][a-z0-9_]*$/u); - expect(JSON.stringify(projected)).not.toContain("SECRET"); + if (variant instanceof ProcessCaptureError) + expect(projected).toMatchObject({ + message: variant.message, + details: { execution_failure: variant.message }, + }); } }); });