Files
reverse-engineering-anything/src/ghidra/GhidraJavaProbe.test.ts
T
morluto cc2e18a265 fix(ghidra): own Java prerequisite probes through final cleanup
Replace synchronous Java execution with bounded owned commands. Keep task-scoped readiness observations fresh across later tasks, defer target client prerequisites behind a closeable owner, and join doctor cancellation and cleanup.

Preserve completed physical failures across late cancellation, exact cleanup dependencies and partial observations. Admit target/profile inputs before probing and return typed factory failures instead of unavailable client wrappers.

Validation: 405 focused tests across 24 files, 2 explicit skips; 109 provider/doctor controls rerun after final admission refinement; check:fast and docs:check. Controlled macOS Java-shaped commands and public CLI/MCP workflows; no new JVM/Ghidra or Windows/Linux lifecycle verification.

Measured warm public doctor baseline/candidate: elapsed 0.447/1.223s, CPU 0.624/1.237s, sampled family RSS 379/529MB, reply tokens 3133/3133. Ownership adds cost; no performance improvement claimed. True cold, whole-agent and MCP costs unmeasured.

Refs #4561. Remaining installation file admission tracked in #4672.
2026-10-17 11:29:03 +08:00

169 lines
4.4 KiB
TypeScript

import { describe, expect, it, afterEach } from "vitest";
import { probeGhidraJava, parseGhidraJavaOutput } from "./GhidraJavaProbe.js";
import { createInspectionResources } from "../../tests/fixtures/inspectionResources.js";
let resources = createInspectionResources();
afterEach(async () => {
await resources.close();
resources = createInspectionResources();
});
const probe = (values: {
readonly stdout?: Buffer;
readonly stderr?: Buffer;
readonly status?: number | null;
readonly signal?: NodeJS.Signals | null;
}) => {
const stdout = values.stdout ?? Buffer.alloc(0),
stderr = values.stderr ?? Buffer.alloc(0);
return parseGhidraJavaOutput("/selected/bin/java", {
stdout: {
text: stdout.toString("utf8"),
bytes: stdout.length,
observedBytes: stdout.length,
},
stderr: {
text: stderr.toString("utf8"),
bytes: stderr.length,
observedBytes: stderr.length,
},
exitCode: values.status === undefined ? 0 : values.status,
signal: values.signal ?? null,
});
};
describe("Ghidra Java prerequisite process facts", () => {
it.each([
["ENOENT", "runtime_missing"],
["EACCES", "runtime_unavailable"],
["ETIMEDOUT", "runtime_unavailable"],
["ENOBUFS", "runtime_unavailable"],
])(
"keeps %s distinct from other runtime failures",
async (code, expected) => {
const error = Object.assign(new Error(`controlled ${code}`), { code });
const result = await probeGhidraJava(
"/selected/bin/java",
{},
{
resourceOwner: resources.owner,
launcher: async () => {
throw error;
},
},
);
expect(result).toMatchObject({
ok: false,
error: {
_tag: "GhidraJavaProbeFailure",
code: expected,
processErrorCode: code,
detail: expect.stringContaining(code),
},
});
expect(result.ok ? null : result.error).not.toHaveProperty("output");
},
);
it("keeps raw byte counts and both streams on a nonzero exit", () => {
const result = probe({
status: 7,
stdout: Buffer.from([0xff]),
stderr: Buffer.from("controlled runtime failure\n"),
});
expect(result).toMatchObject({
ok: false,
error: {
code: "runtime_unavailable",
detail: expect.stringContaining("Java exited 7"),
output: {
stdout: "\ufffd",
stdout_bytes: 1,
stderr: "controlled runtime failure\n",
stderr_bytes: 27,
exit_code: 7,
signal: null,
truncated: false,
},
},
});
});
it("retains partial parsed properties when a successful probe cannot establish its version", () => {
expect(
probe({
stdout: Buffer.from(
"java.home = /selected/jdk\nos.arch = aarch64\nsun.arch.data.model = 64\n",
),
}),
).toMatchObject({
ok: false,
error: {
code: "version_unresolved",
properties: {
home: "/selected/jdk",
version: null,
osArch: "aarch64",
bits: 64,
},
output: { exit_code: 0 },
},
});
});
it("bounds diagnostic text and preserves examined stream sizes", () => {
expect(probe({ status: 7, stdout: Buffer.alloc(12000, 65) })).toMatchObject(
{
ok: false,
error: {
output: {
stdout: "A".repeat(8192),
stdout_bytes: 12000,
stderr_bytes: 0,
truncated: true,
},
},
},
);
});
});
it.each([
"21garbage",
"garbage",
"21.foo",
"9007199254740993",
"21.9999999999999999",
"21+9999999999999999",
"1.21.0",
"21.0",
"021",
])("keeps malformed java.version %s unresolved", (version) => {
expect(
probe({
stdout: Buffer.from(
`java.home = /selected/jdk\njava.version = ${version}\nsun.arch.data.model = 64\n`,
),
}),
).toMatchObject({
ok: false,
error: {
code: "version_unresolved",
properties: { version, home: "/selected/jdk" },
},
});
});
it.each([
["21-ea", 21],
["21.0.11+9-LTS", 21],
["27-internal+0-adhoc.user.build", 27],
["1.8.0_452-b09", 8],
] as const)("recognizes the Java major in %s", (version, major) => {
expect(
probe({
stdout: Buffer.from(
`java.home = /selected/jdk\njava.version = ${version}\nsun.arch.data.model = 64\n`,
),
}),
).toMatchObject({ ok: true, value: { version, major } });
});