mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
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:
1 parent
eab8abe948
commit
5d4e9b8532
18 files changed
+220
-147
No files matched your search
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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-" });
|
||||
@@ -396,6 +396,7 @@ export const createSystemDoctorHost = (
|
||||
(await inspectJadxProvider(
|
||||
inspectionEnvironment,
|
||||
resourceScope,
|
||||
host,
|
||||
inspectionSignal,
|
||||
)),
|
||||
unresolvedProvider("kuna", selected, inspectionEnvironment, platform) ??
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user