mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 00:36:35 +00:00
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.
169 lines
4.4 KiB
TypeScript
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 } });
|
|
});
|