From 5d4e9b85320ff5ab15b2740f2cb45b6b5bcd7d94 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 15 Nov 2026 20:52:58 +0800 Subject: [PATCH] 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. --- scripts/verify/android/analysis.mjs | 2 + .../android/windows-guardian-isolation.mjs | 9 +- scripts/verify/android/windows-lifecycle.mjs | 3 + .../verify/architecture-guard-baselines.json | 30 ++++-- scripts/verify/architecture-guards.mjs | 33 +++++++ src/android/JadxConfiguration.ts | 51 ++++++---- src/android/JadxDoctor.ts | 3 + src/android/JadxProvider.ts | 12 ++- src/android/JadxWorkspaceRoot.test.ts | 32 ++----- src/android/JadxWorkspaceRoot.ts | 6 +- src/doctorRuntime.ts | 1 + src/windows/WindowsNativeLoader.test.ts | 32 ++++++- src/windows/WindowsNativeLoader.ts | 28 ++++-- .../android/jadxArchiveLifecycle.test.ts | 9 +- .../android/jadxConfiguration.test.ts | 3 + tests/boundary/android/jadxDoctor.test.ts | 94 ++++++------------- .../androidGuardianObservation.test.ts | 17 +--- .../process/jadxJavaLifecycle.test.ts | 2 + 18 files changed, 220 insertions(+), 147 deletions(-) diff --git a/scripts/verify/android/analysis.mjs b/scripts/verify/android/analysis.mjs index 6191b4b8b1..69051d44fb 100644 --- a/scripts/verify/android/analysis.mjs +++ b/scripts/verify/android/analysis.mjs @@ -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, diff --git a/scripts/verify/android/windows-guardian-isolation.mjs b/scripts/verify/android/windows-guardian-isolation.mjs index 98fa9553fb..146c64663e 100644 --- a/scripts/verify/android/windows-guardian-isolation.mjs +++ b/scripts/verify/android/windows-guardian-isolation.mjs @@ -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 { diff --git a/scripts/verify/android/windows-lifecycle.mjs b/scripts/verify/android/windows-lifecycle.mjs index c7bdcfd36f..d16ffda705 100644 --- a/scripts/verify/android/windows-lifecycle.mjs +++ b/scripts/verify/android/windows-lifecycle.mjs @@ -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, diff --git a/scripts/verify/architecture-guard-baselines.json b/scripts/verify/architecture-guard-baselines.json index 7d74c35c39..b85a7b7a41 100644 --- a/scripts/verify/architecture-guard-baselines.json +++ b/scripts/verify/architecture-guard-baselines.json @@ -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 } } diff --git a/scripts/verify/architecture-guards.mjs b/scripts/verify/architecture-guards.mjs index cf4c546a58..a3567500cf 100644 --- a/scripts/verify/architecture-guards.mjs +++ b/scripts/verify/architecture-guards.mjs @@ -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 diff --git a/src/android/JadxConfiguration.ts b/src/android/JadxConfiguration.ts index a636b59b74..39e39a4602 100644 --- a/src/android/JadxConfiguration.ts +++ b/src/android/JadxConfiguration.ts @@ -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>, 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>, operation: AndroidOperation, + host: HostPlatform, signal?: AbortSignal, ): Promise => { 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>, + host: HostPlatform, signal?: AbortSignal, ): Promise => { 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>, + platform: NodeJS.Platform, signal?: AbortSignal, ): Promise => { - 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.", diff --git a/src/android/JadxDoctor.ts b/src/android/JadxDoctor.ts index adafa436fd..d79ce8317f 100644 --- a/src/android/JadxDoctor.ts +++ b/src/android/JadxDoctor.ts @@ -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>, resourceOwner: RetainedResourceScope, + host: HostPlatform, signal?: AbortSignal, ): Promise => { const configured = environment.REA_JADX_MCP_JAR !== undefined; const availability = await inspectJadxAvailability(environment, { + host, resourceOwner, signal, }); diff --git a/src/android/JadxProvider.ts b/src/android/JadxProvider.ts index 10f5cad18b..5f948e4139 100644 --- a/src/android/JadxProvider.ts +++ b/src/android/JadxProvider.ts @@ -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>, 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 { 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 diff --git a/src/android/JadxWorkspaceRoot.test.ts b/src/android/JadxWorkspaceRoot.test.ts index 5d86018ef3..e57cf1e405 100644 --- a/src/android/JadxWorkspaceRoot.test.ts +++ b/src/android/JadxWorkspaceRoot.test.ts @@ -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); }); diff --git a/src/android/JadxWorkspaceRoot.ts b/src/android/JadxWorkspaceRoot.ts index d92727e99a..e8f52f16d9 100644 --- a/src/android/JadxWorkspaceRoot.ts +++ b/src/android/JadxWorkspaceRoot.ts @@ -220,7 +220,9 @@ class WindowsJadxWorkspaceRoot implements JadxWorkspaceRoot { } /** POSIX retains its in-process root; Windows keeps a detached native owner. */ -export const createJadxWorkspaceRoot = (): Promise => - process.platform === "win32" +export const createJadxWorkspaceRoot = ( + platform: NodeJS.Platform, +): Promise => + platform === "win32" ? WindowsJadxWorkspaceRoot.create() : PrivateRuntimeRoot.create({ prefix: "rea-android-" }); diff --git a/src/doctorRuntime.ts b/src/doctorRuntime.ts index 1262e8437e..c55b7946c2 100644 --- a/src/doctorRuntime.ts +++ b/src/doctorRuntime.ts @@ -396,6 +396,7 @@ export const createSystemDoctorHost = ( (await inspectJadxProvider( inspectionEnvironment, resourceScope, + host, inspectionSignal, )), unresolvedProvider("kuna", selected, inspectionEnvironment, platform) ?? diff --git a/src/windows/WindowsNativeLoader.test.ts b/src/windows/WindowsNativeLoader.test.ts index 53b8ec2cf9..fea3ae3ee0 100644 --- a/src/windows/WindowsNativeLoader.test.ts +++ b/src/windows/WindowsNativeLoader.test.ts @@ -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); + }); +}); diff --git a/src/windows/WindowsNativeLoader.ts b/src/windows/WindowsNativeLoader.ts index e63902cadb..a56740acf3 100644 --- a/src/windows/WindowsNativeLoader.ts +++ b/src/windows/WindowsNativeLoader.ts @@ -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; }; diff --git a/tests/boundary/android/jadxArchiveLifecycle.test.ts b/tests/boundary/android/jadxArchiveLifecycle.test.ts index f7e4850f46..6974ab2d59 100644 --- a/tests/boundary/android/jadxArchiveLifecycle.test.ts +++ b/tests/boundary/android/jadxArchiveLifecycle.test.ts @@ -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 | undefined; try { diff --git a/tests/boundary/android/jadxConfiguration.test.ts b/tests/boundary/android/jadxConfiguration.test.ts index ae3bb5fb72..aec3c601ee 100644 --- a/tests/boundary/android/jadxConfiguration.test.ts +++ b/tests/boundary/android/jadxConfiguration.test.ts @@ -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(), diff --git a/tests/boundary/android/jadxDoctor.test.ts b/tests/boundary/android/jadxDoctor.test.ts index 2b21e1dccc..069120ce80 100644 --- a/tests/boundary/android/jadxDoctor.test.ts +++ b/tests/boundary/android/jadxDoctor.test.ts @@ -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[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 => { }; 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 () => { diff --git a/tests/boundary/process/androidGuardianObservation.test.ts b/tests/boundary/process/androidGuardianObservation.test.ts index d8f6ade9fe..211e73901b 100644 --- a/tests/boundary/process/androidGuardianObservation.test.ts +++ b/tests/boundary/process/androidGuardianObservation.test.ts @@ -44,7 +44,7 @@ vi.mock("node:child_process", async (original) => { }; }); const { verifyWindowsGuardianIsolation } = await vi.importActual<{ - verifyWindowsGuardianIsolation(): Promise; + verifyWindowsGuardianIsolation(platform?: NodeJS.Platform): Promise; }>("../../../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); }, ); diff --git a/tests/boundary/process/jadxJavaLifecycle.test.ts b/tests/boundary/process/jadxJavaLifecycle.test.ts index 52eb19e0d8..8bacc3ed68 100644 --- a/tests/boundary/process/jadxJavaLifecycle.test.ts +++ b/tests/boundary/process/jadxJavaLifecycle.test.ts @@ -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) => {