fix(ci): align diagnostic regressions and format merged changes

This commit is contained in:
morluto committed 2026-10-09 07:45:49 +08:00
1 parent 80c3d36dfa
commit 67756dacc0
7 files changed
+123 -95

No files matched your search

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