refactor(android): take the host in JADX configuration, doctor and workspace roots (#11086)

* refactor(android): take the host in JADX configuration, doctor and workspace roots

* fix(android): pass the host in verifiers, follow it for JAVA_HOME, and test the native cache split

* test(platform): count tests that redefine process.platform or process.arch (#11102)

Tests select a host by injection. A redefined process.platform stops exercising its branch once the module under test reads HOST_PLATFORM, and still passes. A counted baseline (105 sites in 20 files) may only shrink; each module migration switches its tests to injection.
This commit is contained in:
morluto authored and GitHub committed 2026-11-15 20:52:58 +08:00
1 parent eab8abe948
commit 5d4e9b8532
18 files changed
+220 -147

No files matched your search

+2
View File
@@ -16,6 +16,7 @@ import { verifyAndroidMetadata } from "./metadata.mjs";
import { verifyAndroidClassSearch } from "./class-search.mjs";
import { verifyAndroidFailureRecovery } from "./failure-recovery.mjs";
import { resolveJadxConfigurationInputs } from "../../../dist/android/JadxConfiguration.js";
import { HOST_PLATFORM } from "../../../dist/platform/HostPlatform.js";
import { validateVerifierJadxConfiguration } from "../../lib/jadx-configuration-inspection.mjs";
import { verifyWindowsAndroidLifecycle } from "./windows-lifecycle.mjs";
@@ -41,6 +42,7 @@ assert.equal(
const configuration = await resolveJadxConfigurationInputs(
process.env,
"inspect_android_package",
HOST_PLATFORM,
);
await validateVerifierJadxConfiguration(
configuration,
@@ -6,6 +6,7 @@ import { createInterface } from "node:readline";
import { setTimeout as delay } from "node:timers/promises";
import { fileURLToPath } from "node:url";
import { createJadxWorkspaceRoot } from "../../../dist/android/JadxWorkspaceRoot.js";
import { HOST_PLATFORM } from "../../../dist/platform/HostPlatform.js";
const source = fileURLToPath(import.meta.url);
const marker = "REA synthetic guardian isolation test\n";
@@ -19,7 +20,7 @@ const exists = (path) =>
);
if (process.argv[1] === source && process.argv[2] === "owner") {
const root = await createJadxWorkspaceRoot();
const root = await createJadxWorkspaceRoot(HOST_PLATFORM.platform);
await writeFile(join(root.path, "synthetic.txt"), marker);
process.stdout.write(JSON.stringify({ path: root.path }) + "\n");
await new Promise(() => {});
@@ -82,8 +83,10 @@ const stop = async ({ child, exit, path }) => {
};
/** Two independent owners must never delete one another's private workspace. */
export async function verifyWindowsGuardianIsolation() {
assert.equal(process.platform, "win32");
export async function verifyWindowsGuardianIsolation(
platform = HOST_PLATFORM.platform,
) {
assert.equal(platform, "win32");
const first = await launch();
let second;
try {
@@ -7,6 +7,7 @@ import { setTimeout as delay } from "node:timers/promises";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { resolveJadxConfigurationInputs } from "../../../dist/android/JadxConfiguration.js";
import { HOST_PLATFORM } from "../../../dist/platform/HostPlatform.js";
import { validateVerifierJadxConfiguration } from "../../lib/jadx-configuration-inspection.mjs";
import { verifyWindowsGuardianIsolation } from "./windows-guardian-isolation.mjs";
import {
@@ -39,6 +40,7 @@ async function verifyJavaSelection(environment) {
const selected = await resolveJadxConfigurationInputs(
environment,
"inspect_android_package",
HOST_PLATFORM,
);
await validateVerifierJadxConfiguration(
selected,
@@ -70,6 +72,7 @@ async function verifyJavaSelection(environment) {
const onPath = await resolveJadxConfigurationInputs(
pathEnvironment,
"inspect_android_package",
HOST_PLATFORM,
);
await validateVerifierJadxConfiguration(
onPath,
@@ -280,8 +280,6 @@
"src/adb/AdbDeviceOutput.ts": 1,
"src/adb/AdbHostFileName.ts": 1,
"src/adb/AdbProvider.ts": 1,
"src/android/JadxConfiguration.ts": 10,
"src/android/JadxWorkspaceRoot.ts": 1,
"src/apktool/ApktoolConfiguration.ts": 4,
"src/apktool/ApktoolProvider.ts": 2,
"src/application/BinaryTargetResolver.ts": 1,
@@ -358,11 +356,9 @@
"src/server/registerSessionTools.ts": 3,
"src/serverIdentity.ts": 1,
"src/signals/SigrokRecordingProvider.ts": 1,
"src/wasm/WabtArtifactProvider.ts": 1,
"src/windows/WindowsNativeLoader.ts": 1
"src/wasm/WabtArtifactProvider.ts": 1
},
"ambient-host-architecture": {
"src/android/JadxConfiguration.ts": 4,
"src/application/BinaryTargetResolver.ts": 1,
"src/application/Doctor.ts": 1,
"src/application/LinuxHopper.ts": 1,
@@ -370,8 +366,7 @@
"src/evm/EvmoleInterfaceProvider.ts": 2,
"src/javascript/recovery/WakaruCommand.ts": 1,
"src/native/pwntools/PwntoolsDecoder.ts": 2,
"src/server/createServer.ts": 1,
"src/windows/WindowsNativeLoader.ts": 1
"src/server/createServer.ts": 1
},
"hand-caught-configuration-error": {
"src/application/binary/BinarySession.ts": 3,
@@ -459,5 +454,26 @@
"lifecycle-plain-throw": {
"src/android/JadxWorkspaceRoot.ts": 4,
"src/native/pwntools/PwntoolsDecoder.ts": 1
},
"test-host-platform-stub": {
"src/application/binaryDiagnostics/ByteEntropyService.test.ts": 1,
"src/browser/ElectronCaptureCleanup.test.ts": 2,
"src/browser/PlaywrightElectronActiveProvider.test.ts": 2,
"src/browser/PlaywrightScenarioBrowser.test.ts": 3,
"src/browser/modules/NativeModuleResolver.test.ts": 3,
"src/pcap/PcapNativeBound.test.ts": 2,
"tests/boundary/browser/historicalCaptureDecoder.test.ts": 2,
"tests/boundary/cli/debuggerOneShotExit.test.ts": 1,
"tests/boundary/evm/evmCleanupRetry.test.ts": 52,
"tests/boundary/filesystem/webScriptExport.test.ts": 2,
"tests/boundary/filesystem/webScriptManifestPaths.test.ts": 2,
"tests/boundary/native/pwntoolsArtifactCleanup.test.ts": 4,
"tests/boundary/process/artifactSelectedCommand.test.ts": 6,
"tests/boundary/process/nativeUiVerifierActions.test.ts": 1,
"tests/process-global/hostRestrictedAvailabilityMcp.test.ts": 4,
"tests/process-global/pwntoolsCleanup.test.ts": 4,
"tests/process-global/pwntoolsCliFinalization.test.ts": 4,
"tests/process-global/pwntoolsCliRemediation.test.ts": 4,
"tests/process-global/pwntoolsStageRetirement.test.ts": 4
}
}
+33
View File
@@ -567,6 +567,39 @@ checkCounted(
["src/platform/HostPlatform.ts"],
);
// Tests select a platform or architecture by injecting it (a parameter, a
// `host` option, or a mock of src/platform/HostPlatform). Redefining
// process.platform or process.arch stops exercising its branch once the module
// under test takes the host from HostPlatform, and still passes. Each module
// migration switches its tests to injection and lowers this baseline.
const listTestFiles = async (dir) => {
const output = [];
for (const entry of await readdir(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) output.push(...(await listTestFiles(path)));
else if (/\.(?:test|spec|fixture)\.ts$/u.test(entry.name))
output.push(path);
}
return output;
};
const testFiles = await Promise.all(
[
...(await listTestFiles(join(root, "src"))),
...(await listFiles(join(root, "tests"))),
...(await listTestFiles(join(root, "tests"))),
].map(async (path) => ({
path: relative(root, path).split(sep).join("/"),
content: await readFile(path, "utf8"),
})),
);
checkCounted(
"test redefines process.platform or process.arch (inject the platform: pass it as a parameter or host option, or mock src/platform/HostPlatform)",
"test-host-platform-stub",
testFiles,
/\b(?:defineProperty|spyOn)\(\s*(?:globalThis\.)?process\s*,\s*["'`](?:platform|arch)["'`]/gu,
[],
);
// `find` on a parsed section, program-header or load-command table silently
// keeps the first of repeated declarations. Select keyed entries through
// src/domain/keyedCollection (`uniqueKeyed`, `allKeyed`, or
+32 -19
View File
@@ -1,3 +1,4 @@
import type { HostPlatform } from "../platform/HostPlatform.js";
import {
absoluteCallerPath,
appendCallerPath,
@@ -17,7 +18,7 @@ import {
} from "./JadxInterruption.js";
import { constants } from "node:fs";
import { access, realpath, stat } from "node:fs/promises";
import { isAbsolute } from "node:path";
import { isAbsolute, posix, win32 } from "node:path";
import {
posixPathCandidates,
windowsPathCandidates,
@@ -175,6 +176,8 @@ class JadxConfigurationFailure extends Error {
export const inspectJadxAvailability = async (
environment: Readonly<Record<string, string | undefined>>,
options: JadxJavaProbeOptions & {
/** The host whose process-ownership controls and Java layout apply. */
readonly host: HostPlatform;
/** Admitted runtime override; omission retains launch configuration. */
readonly operationTimeoutMs?: number | undefined;
},
@@ -186,6 +189,7 @@ export const inspectJadxAvailability = async (
);
const configuration = await readJadxConfigurationInputs(
environment,
options.host,
options.signal,
);
await validateJadxConfigurationContents(
@@ -246,10 +250,11 @@ export const resolveJadxOperationTimeout = (
export const resolveJadxConfigurationInputs = async (
environment: Readonly<Record<string, string | undefined>>,
operation: AndroidOperation,
host: HostPlatform,
signal?: AbortSignal,
): Promise<JadxConfiguration> => {
try {
return await readJadxConfigurationInputs(environment, signal);
return await readJadxConfigurationInputs(environment, host, signal);
} catch (cause) {
if (!(cause instanceof JadxConfigurationFailure)) throw cause;
throw projectConfigurationFailure(operation, cause);
@@ -276,12 +281,13 @@ export const validateJadxConfiguration = async (
const readJadxConfigurationInputs = async (
environment: Readonly<Record<string, string | undefined>>,
host: HostPlatform,
signal?: AbortSignal,
): Promise<JadxConfiguration> => {
throwIfJadxInterrupted(signal);
requireSupportedHost();
requireSupportedHost(host);
const jar = await resolveJar(environment);
const java = await resolveJava(environment, signal);
const java = await resolveJava(environment, host.platform, signal);
await requireJavaHomeExecutable(environment.JAVA_HOME, java);
throwIfJadxInterrupted(signal);
return { jar, java, jvmArguments: resolveJvmArguments(environment) };
@@ -491,33 +497,34 @@ const archiveCancellation = (
: cancelledBy(operation, carriedReason(reason), { diagnostics });
};
const requireSupportedHost = (): void => {
if (process.platform === "linux" || process.platform === "darwin") return;
if (process.platform === "win32" && process.arch === "x64") {
const loaded = systemWindowsNativeAuthority();
const requireSupportedHost = (host: HostPlatform): void => {
const { platform, architecture } = host;
if (platform === "linux" || platform === "darwin") return;
if (platform === "win32" && architecture === "x64") {
const loaded = systemWindowsNativeAuthority(host);
if (loaded.available) {
if (loaded.authority.inspection.protocolStdin === true) return;
throw configurationFailure(
"runtime_missing",
"The bundled Windows native artifact lacks owned protocol stdin; use a matching current Windows x64 REA package.",
{
platform: process.platform,
architecture: process.arch,
platform: platform,
architecture: architecture,
phase: "process-ownership",
owned_protocol_stdin: false,
},
"windows_native_artifact",
);
}
const readiness = inspectWindowsNativeReadiness(process.platform, loaded);
const readiness = inspectWindowsNativeReadiness(platform, loaded);
throw configurationFailure(
readiness.available ? "runtime_unavailable" : readiness.code,
readiness.available
? "Windows native readiness disagreed with the native loader result."
: readiness.reason,
{
platform: process.platform,
architecture: process.arch,
platform: platform,
architecture: architecture,
phase: "process-ownership",
owned_protocol_stdin: false,
...(readiness.available
@@ -538,8 +545,8 @@ const requireSupportedHost = (): void => {
}
throw configurationFailure(
"unsupported_host",
`Android JADX subprocess ownership is unsupported on ${process.platform}/${process.arch}; use Linux, macOS, or Windows x64 with its bundled native controls.`,
{ platform: process.platform },
`Android JADX subprocess ownership is unsupported on ${platform}/${architecture}; use Linux, macOS, or Windows x64 with its bundled native controls.`,
{ platform: platform },
"unsupported_host",
);
};
@@ -588,11 +595,12 @@ const resolveJar = async (
const resolveJava = async (
environment: Readonly<Record<string, string | undefined>>,
platform: NodeJS.Platform,
signal?: AbortSignal,
): Promise<string> => {
const executable = process.platform === "win32" ? "java.exe" : "java";
const windows = platform === "win32";
const executable = windows ? "java.exe" : "java";
if (environment.JAVA_HOME === undefined) {
const windows = process.platform === "win32";
// Resolve in the caller's directory once. The module probe and the engine
// run in different directories, and the selected executable also owns the
// retained session's cache identity. Empty POSIX PATH components select
@@ -643,8 +651,13 @@ const resolveJava = async (
"java_runtime",
);
}
if (isAbsolute(environment.JAVA_HOME))
return appendCallerPath(environment.JAVA_HOME, `bin/${executable}`);
// JAVA_HOME follows the same host path rules as the PATH search above.
if ((windows ? win32 : posix).isAbsolute(environment.JAVA_HOME))
return appendCallerPath(
environment.JAVA_HOME,
`bin/${executable}`,
platform,
);
throw configurationFailure(
"open_options_invalid",
"JAVA_HOME must select an existing JDK by absolute path; omit it to use java on PATH.",
+3
View File
@@ -1,3 +1,4 @@
import type { HostPlatform } from "../platform/HostPlatform.js";
import type {
RetainedResourceScope,
ProviderAvailability,
@@ -29,10 +30,12 @@ export const JADX_DOCTOR_SETTINGS = [
export const inspectJadxProvider = async (
environment: Readonly<Record<string, string | undefined>>,
resourceOwner: RetainedResourceScope,
host: HostPlatform,
signal?: AbortSignal,
): Promise<DoctorProviderInspection> => {
const configured = environment.REA_JADX_MCP_JAR !== undefined;
const availability = await inspectJadxAvailability(environment, {
host,
resourceOwner,
signal,
});
+8 -4
View File
@@ -1,5 +1,5 @@
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import { HOST_PLATFORM } from "../platform/HostPlatform.js";
import { HOST_PLATFORM, type HostPlatform } from "../platform/HostPlatform.js";
import type { AnalysisSettingsReader } from "../domain/analysisSettings.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
@@ -311,15 +311,18 @@ export class JadxProvider implements AndroidAnalysisPort {
readonly environment: Readonly<Record<string, string | undefined>>,
readonly launcher?: JadxLauncher,
readonly settings?: AnalysisSettingsReader,
/** The host JADX runs on; tests select another host's admission rules. */
readonly host: HostPlatform = HOST_PLATFORM,
) {}
/** Report actual local prerequisites without starting JADX or opening a target. */
inspectAvailability(signal?: AbortSignal) {
const environment = snapshotEnvironment(
this.environment,
HOST_PLATFORM.platform,
this.host.platform,
);
const pending = inspectJadxAvailability(environment, {
host: this.host,
operationTimeoutMs:
this.settings?.().overrides["jadx.operation_timeout_ms"],
resourceOwner: this.#prerequisiteOwner,
@@ -513,7 +516,7 @@ export class JadxProvider implements AndroidAnalysisPort {
): Promise<Outcome> {
const environment = snapshotEnvironment(
this.environment,
HOST_PLATFORM.platform,
this.host.platform,
);
// Resolve the accepted operation's override before acquiring any resource.
// An unusable launch value is refused without starting the engine.
@@ -554,6 +557,7 @@ export class JadxProvider implements AndroidAnalysisPort {
const configuration = await resolveJadxConfigurationInputs(
environment,
request.operation,
this.host,
signal,
);
throwIfJadxInterrupted(signal);
@@ -627,7 +631,7 @@ export class JadxProvider implements AndroidAnalysisPort {
operationDeadline: { signal: timeout, timeoutMs: timeoutMs.value },
},
);
root = await createJadxWorkspaceRoot();
root = await createJadxWorkspaceRoot(this.host.platform);
const workspaceSignal =
root.failureSignal === undefined
? signal
+10 -22
View File
@@ -31,11 +31,6 @@ afterEach(() => {
});
it("decodes a Windows guardian path across split UTF-8 stdout writes", async () => {
const previousPlatform = process.platform;
Object.defineProperty(process, "platform", {
configurable: true,
value: "win32",
});
const parent = "C:\\Users\\研究者\\AppData\\Local\\Temp";
const path = win32.join(parent, `rea-android-${"a".repeat(32)}`);
const child = Object.assign(new EventEmitter(), {
@@ -65,25 +60,18 @@ it("decodes a Windows guardian path across split UTF-8 stdout writes", async ()
);
const split = ready.indexOf(Buffer.from("研")) + 1;
try {
const start = createJadxWorkspaceRoot();
setImmediate(() => {
child.stdout.write(ready.subarray(0, split), () => {
setImmediate(() => child.stdout.write(ready.subarray(split)));
});
const start = createJadxWorkspaceRoot("win32");
setImmediate(() => {
child.stdout.write(ready.subarray(0, split), () => {
setImmediate(() => child.stdout.write(ready.subarray(split)));
});
});
const root = await start;
try {
expect(root.path).toBe(path);
} finally {
await expect(root.close()).resolves.toBeUndefined();
}
expect(lstatMock).toHaveBeenCalledWith(path);
const root = await start;
try {
expect(root.path).toBe(path);
} finally {
Object.defineProperty(process, "platform", {
configurable: true,
value: previousPlatform,
});
await expect(root.close()).resolves.toBeUndefined();
}
expect(lstatMock).toHaveBeenCalledWith(path);
});
+4 -2
View File
@@ -220,7 +220,9 @@ class WindowsJadxWorkspaceRoot implements JadxWorkspaceRoot {
}
/** POSIX retains its in-process root; Windows keeps a detached native owner. */
export const createJadxWorkspaceRoot = (): Promise<JadxWorkspaceRoot> =>
process.platform === "win32"
export const createJadxWorkspaceRoot = (
platform: NodeJS.Platform,
): Promise<JadxWorkspaceRoot> =>
platform === "win32"
? WindowsJadxWorkspaceRoot.create()
: PrivateRuntimeRoot.create({ prefix: "rea-android-" });
+1
View File
@@ -396,6 +396,7 @@ export const createSystemDoctorHost = (
(await inspectJadxProvider(
inspectionEnvironment,
resourceScope,
host,
inspectionSignal,
)),
unresolvedProvider("kuna", selected, inspectionEnvironment, platform) ??
+31 -1
View File
@@ -1,9 +1,10 @@
import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { windowsNativeArtifactFixture } from "../../tests/fixtures/windowsNativeArtifact.js";
import { inspectWindowsNativeReadiness } from "../process/WindowsAuthority.js";
import { HOST_PLATFORM } from "../platform/HostPlatform.js";
import {
loadWindowsNativeAuthority,
windowsNativeFailureRemediation,
@@ -282,3 +283,32 @@ describe("packaged Windows native artifact validation", () => {
});
});
});
describe("system authority cache", () => {
// Each case starts from an empty module-level cache.
beforeEach(() => {
vi.resetModules();
});
const systemAuthority = async () =>
(await import("./WindowsNativeLoader.js")).systemWindowsNativeAuthority;
const other = {
platform: HOST_PLATFORM.platform === "win32" ? "linux" : "win32",
architecture: "x64",
} as const;
it("keeps the host result after another host is injected", async () => {
const authority = await systemAuthority();
const host = authority();
const injected = authority(other);
expect(injected).not.toEqual(host);
expect(authority()).toEqual(host);
});
it("never caches an injected host's result for the host", async () => {
const authority = await systemAuthority();
const injected = authority(other);
const host = authority();
expect(host).not.toEqual(injected);
expect(authority()).toEqual(host);
});
});
+21 -7
View File
@@ -1,3 +1,4 @@
import { HOST_PLATFORM, type HostPlatform } from "../platform/HostPlatform.js";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
@@ -216,13 +217,13 @@ export const loadWindowsNativeAuthority = (
let cached: WindowsNativeLoadResult | undefined;
/** Cache stable admission; let a later query retry failed owned inspection. */
export const systemWindowsNativeAuthority = (): WindowsNativeLoadResult => {
if (cached?.available === false && cached.retryable === true)
cached = undefined;
cached ??= loadWindowsNativeAuthority({
platform: process.platform,
architecture: process.arch,
const loadPackagedWindowsNativeAuthority = ({
platform,
architecture,
}: HostPlatform): WindowsNativeLoadResult =>
loadWindowsNativeAuthority({
platform,
architecture,
nodeApiVersion: process.versions.napi,
packageVersion: PACKAGE_METADATA.version,
readManifest: () => {
@@ -252,6 +253,19 @@ export const systemWindowsNativeAuthority = (): WindowsNativeLoadResult => {
return value;
},
});
/**
* Cache stable admission; let a later query retry failed owned inspection.
* Only this host's result is cached: another host's admission rules are a
* test seam and load the packaged artifact uncached.
*/
export const systemWindowsNativeAuthority = (
host: HostPlatform = HOST_PLATFORM,
): WindowsNativeLoadResult => {
if (host !== HOST_PLATFORM) return loadPackagedWindowsNativeAuthority(host);
if (cached?.available === false && cached.retryable === true)
cached = undefined;
cached ??= loadPackagedWindowsNativeAuthority(host);
return cached;
};
@@ -1,3 +1,4 @@
import { HOST_PLATFORM } from "../../../src/platform/HostPlatform.js";
import { inspectJadxProvider } from "../../../src/android/JadxDoctor.js";
import { createInspectionResources } from "../../fixtures/inspectionResources.js";
import { stat, writeFile } from "node:fs/promises";
@@ -275,9 +276,11 @@ it("joins independent doctor archive finalization and retries its exact failed c
}
await original.call(this);
});
const inspection = inspectJadxProvider(f.environment, resources.owner).catch(
(cause: unknown) => cause,
);
const inspection = inspectJadxProvider(
f.environment,
resources.owner,
HOST_PLATFORM,
).catch((cause: unknown) => cause);
let settled = false;
let shutdown: Promise<unknown> | undefined;
try {
@@ -1,3 +1,4 @@
import { HOST_PLATFORM } from "../../../src/platform/HostPlatform.js";
import { AnalysisSettings } from "../../../src/application/AnalysisSettings.js";
import { JadxProvider } from "../../../src/android/JadxProvider.js";
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
@@ -38,6 +39,7 @@ const inspectJadxAvailability = (
signal?: AbortSignal,
) =>
inspectAvailability(environment, {
host: HOST_PLATFORM,
resourceOwner: inspectionOwner(),
signal,
});
@@ -51,6 +53,7 @@ const resolveConfiguration = async (
const configuration = await resolveJadxConfigurationInputs(
environment,
operation,
HOST_PLATFORM,
);
await validateJadxConfiguration(configuration, environment, operation, {
resourceOwner: inspectionOwner(),
+27 -67
View File
@@ -1,3 +1,7 @@
import {
HOST_PLATFORM,
type HostPlatform,
} from "../../../src/platform/HostPlatform.js";
import { createInspectionResources } from "../../fixtures/inspectionResources.js";
import { chmod, mkdir, realpath, symlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -17,7 +21,8 @@ const inspectionOwner = () => {
};
const inspectJadxProvider = (
environment: Parameters<typeof inspectProvider>[0],
) => inspectProvider(environment, inspectionOwner());
host: HostPlatform = HOST_PLATFORM,
) => inspectProvider(environment, inspectionOwner(), host);
const it = test.skipIf(process.platform === "win32");
@@ -334,54 +339,26 @@ it("guides JAR replacement when the JAR misses bridge classes", async () => {
});
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",
});
const inspection = await inspectJadxProvider(
{ REA_JADX_MCP_JAR: "/opt/jadx-headless-mcp.jar" },
{ platform: "sunos", architecture: "x64" },
);
expect(inspection.checks[0]?.code).toBe("unsupported_host");
expect(inspection.checks[0]?.remediation).toContain("Linux");
} finally {
Object.defineProperty(process, "platform", {
value: originalPlatform,
configurable: true,
});
}
expect(inspection.checks[0]?.code).toBe("unsupported_host");
expect(inspection.checks[0]?.remediation).toContain("Linux");
});
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",
});
const inspection = await inspectJadxProvider(
{ REA_JADX_MCP_JAR: "/opt/jadx-headless-mcp.jar" },
{ platform: "win32", architecture: "x64" },
);
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,
});
}
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");
});
it("guides Java probe correction when the module listing is unparseable", async () => {
@@ -420,30 +397,13 @@ const createFakeJdk = async (root: string, major = "21"): Promise<string> => {
};
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",
});
const inspection = await inspectJadxProvider(
{ REA_JADX_MCP_JAR: "/opt/jadx-headless-mcp.jar" },
{ platform: "win32", architecture: "x64" },
);
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,
});
}
expect(inspection.checks[0]?.code).toBe("runtime_missing");
expect(inspection.checks[0]?.classification).toBe("missing_dependency");
});
it("classifies malformed JAVA_HOME as configuration drift", async () => {
@@ -44,7 +44,7 @@ vi.mock("node:child_process", async (original) => {
};
});
const { verifyWindowsGuardianIsolation } = await vi.importActual<{
verifyWindowsGuardianIsolation(): Promise<void>;
verifyWindowsGuardianIsolation(platform?: NodeJS.Platform): Promise<void>;
}>("../../../scripts/verify/android/windows-guardian-isolation.mjs");
it.each(["ENOENT", "ENOTDIR", "EACCES", "EIO"])(
@@ -53,16 +53,9 @@ it.each(["ENOENT", "ENOTDIR", "EACCES", "EIO"])(
observation.failure = Object.assign(new Error(`inert stat ${code}`), {
code,
});
const original = Object.getOwnPropertyDescriptor(process, "platform");
if (original === undefined) throw new Error("Platform descriptor missing");
Object.defineProperty(process, "platform", { ...original, value: "win32" });
try {
const result = verifyWindowsGuardianIsolation();
if (code === "ENOENT" || code === "ENOTDIR")
await expect(result).resolves.toBeUndefined();
else await expect(result).rejects.toBe(observation.failure);
} finally {
Object.defineProperty(process, "platform", original);
}
const result = verifyWindowsGuardianIsolation("win32");
if (code === "ENOENT" || code === "ENOTDIR")
await expect(result).resolves.toBeUndefined();
else await expect(result).rejects.toBe(observation.failure);
},
);
@@ -1,3 +1,4 @@
import { HOST_PLATFORM } from "../../../src/platform/HostPlatform.js";
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
import { beforeAll, expect, it as test, vi } from "vitest";
import { inspectJadxJava } from "../../../src/android/JadxJavaProbe.js";
@@ -348,6 +349,7 @@ setTimeout(() => process.exit(99), 10000);
const independent = inspectJadxProvider(
f.environment,
resources.owner,
HOST_PLATFORM,
doctor.signal,
).then(
(value) => {