Files
reverse-engineering-anything/tests/fixtures/android/jadx.ts
T
akushonkamen 7a0140b4aa fix(android): bound inline search Evidence with retained inventory provenance (#1890)
Focused and zero-match search_android_classes responses embedded every
examined class name: JadxSession records each raw list_classes page into
observed calls, those calls ride into Evidence raw_result on the
registration path, and the complete inline Evidence is serialized into
every tool response (issue #1850: 382,435 bytes for one match).

When the server session retains Evidence, record the complete raw-call
record first, then deliver a summary Evidence that keeps the pagination
audit (totals, offsets, page sizes, elided item counts), the unchanged
normalized matches and truthful coverage metadata, and links the
retained complete inventory by Evidence ID. Sessions without retention
keep the complete inline delivery, so complete CLI JSON output is
unchanged and no provenance is discarded.

🤖 Generated with AI assistance (disclosure voluntary per repo policy)
2026-10-12 01:07:22 +08:00

169 lines
5.7 KiB
TypeScript

import {
access,
chmod,
mkdir,
readFile,
rm,
writeFile,
} from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { expect, onTestFinished } from "vitest";
import { AndroidAnalysisService } from "../../../src/application/android/AndroidAnalysisService.js";
import { createAndroidAnalysisProvider } from "../../../src/composition/android.js";
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
import {
ProviderProcessSupervisor,
spawnOwnedProviderProcess,
} from "../../../src/process/ProviderProcess.js";
import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js";
import { writeOrderedZip } from "../artifactEntryOrder.js";
import { createTestTempDirectory } from "../temporaryDirectory.js";
import { writeJadxJarInventory } from "./jadxJar.js";
const fixture = fileURLToPath(new URL("./jadx-mcp.mjs", import.meta.url));
/** Real owned subprocess running a synthetic MCP producer, with no module mocks. */
export const createJadxProtocolFixture = async (
mode = "normal",
cleanupFailure?: () => boolean,
inventory?: { readonly classCount?: number },
) => {
const root = await createTestTempDirectory("rea-android-boundary-");
const apk = join(root, "fixture.apk");
const jar = join(root, "fixture.jar");
const javaHome = join(root, "jdk");
const javaBin = join(javaHome, "bin");
const java = join(javaBin, "java");
const javaProbeLog = join(root, "java-probes.log");
await mkdir(javaBin, { recursive: true });
await writeFile(
java,
`#!/usr/bin/env node\nif (process.argv[2] === "--list-modules") { require("node:fs").appendFileSync(${JSON.stringify(javaProbeLog)}, "probe\\n"); console.log("java.base@21.0.0\\njdk.compiler@21.0.0"); }\n`,
);
await chmod(java, 0o755);
await writeOrderedZip(apk, ["AndroidManifest.xml", "classes.dex"]);
await writeJadxJarInventory(jar);
const launches: {
runId: string;
cwd: string | undefined;
pid: number | undefined;
arguments: readonly string[] | undefined;
javaToolOptions: string | undefined;
legacyJavaOptions: string | undefined;
}[] = [];
const environment: NodeJS.ProcessEnv = {
...process.env,
JAVA_HOME: javaHome,
REA_JADX_MCP_JAR: jar,
};
const provider = createAndroidAnalysisProvider(
environment,
async (options) => {
const spawned = await spawnOwnedProviderProcess({
...options,
command: process.execPath,
arguments: [fixture],
expectedCommand: process.execPath,
env: {
...options.env,
REA_FAKE_JADX_MODE: mode,
...(inventory?.classCount === undefined
? {}
: {
REA_FAKE_JADX_CLASS_COUNT: String(inventory.classCount),
}),
},
});
launches.push({
runId: options.runId,
cwd: options.cwd,
pid: spawned.process.pid,
arguments: options.arguments,
javaToolOptions: options.hostEnvironment?.JAVA_TOOL_OPTIONS,
legacyJavaOptions:
options.env?._JAVA_OPTIONS ?? options.hostEnvironment?._JAVA_OPTIONS,
});
if (mode === "cleanup-failure" || cleanupFailure !== undefined) {
onTestFinished(async () => {
const supervisor = new ProviderProcessSupervisor({
...spawned,
ownsProcessLifetime: true,
cleanup: () => cleanupOwnedProcessGroup(spawned.ownership),
});
// The SDK can close the child transport before process exit settles.
// Retry retained ownership without bypassing the supervisor's proof.
// Retain the entire stop result so an incomplete teardown failure reports its reason.
await expect
.poll(
async () => {
const stopped = await supervisor.stop();
if (stopped.status === "incomplete")
throw new Error(
`Owned process cleanup incomplete: ${stopped.reason}`,
);
return stopped.status;
},
{ timeout: 5_000 },
)
.not.toBe("incomplete");
if (options.cwd !== undefined)
await rm(options.cwd, { recursive: true, force: true });
});
return {
...spawned,
cleanup: async () =>
(cleanupFailure?.() ?? true)
? {
cleaned: false,
reason: "injected ownership verification failure",
}
: cleanupOwnedProcessGroup(spawned.ownership),
};
}
return spawned;
},
);
onTestFinished(async () => {
try {
await provider.close();
} catch (cause) {
if (
mode !== "cleanup-failure" ||
!(cause instanceof ProviderCleanupError) ||
cause.providerId !== "jadx"
)
throw cause;
}
});
return {
service: new AndroidAnalysisService(provider),
provider,
apk,
jar,
environment,
launches,
javaProbeCount: async () => {
const contents = await readFile(javaProbeLog, "utf8");
return contents.split("\n").filter(Boolean).length;
},
};
};
/** Verify only this fixture's acquired group and workspace, without scanning unrelated host environments. */
export const verifyJadxFixtureCleanup = async (
launches: Awaited<ReturnType<typeof createJadxProtocolFixture>>["launches"],
): Promise<void> => {
for (const launch of launches) {
const pid = launch.pid;
if (pid !== undefined)
expect(() => process.kill(-pid, 0)).toThrowError(
expect.objectContaining({ code: "ESRCH" }),
);
if (launch.cwd !== undefined)
await expect(access(launch.cwd)).rejects.toMatchObject({
code: "ENOENT",
});
}
};