fix(test): make host fixtures portable

This commit is contained in:
morluto 2026-07-16 18:51:14 +08:00
parent b2d1f2affd
commit f23f52903c
11 changed files with 74 additions and 23 deletions

View File

@ -40,16 +40,22 @@ export const isInitializedPtyRoot = (options: {
readonly before: ProcessIdentitySnapshot;
readonly observedRunId: string | undefined;
readonly after: ProcessIdentitySnapshot;
}): boolean =>
}): boolean => {
const sessionIdentityMatches =
(options.before.session_id === options.rootPid &&
options.after.session_id === options.rootPid) ||
(options.before.session_id === null && options.after.session_id === null);
return (
options.before.pid === options.rootPid &&
options.before.process_group_id === options.rootPid &&
options.before.session_id === options.rootPid &&
sessionIdentityMatches &&
options.after.pid === options.rootPid &&
options.after.parent_pid === options.before.parent_pid &&
options.after.process_group_id === options.rootPid &&
options.after.session_id === options.rootPid &&
options.after.startTime === options.before.startTime &&
options.observedRunId === options.expectedRunId;
options.observedRunId === options.expectedRunId
);
};
const isExpectedProcReadFailure = (cause: unknown): boolean =>
cause instanceof Error &&
@ -315,7 +321,7 @@ const readPsRows = async (
pid: Number(match[1]),
parent_pid: Number(match[2]),
process_group_id: Number(match[3]),
session_id: Number(match[4]),
session_id: Number(match[4]) || null,
command: match[5] ?? "",
startTime: undefined,
}))
@ -346,7 +352,7 @@ const inspectInitializedPtyRoot = async (
if (
before === undefined ||
before.process_group_id !== rootPid ||
before.session_id !== rootPid
(before.session_id !== rootPid && before.session_id !== null)
)
return undefined;
let observedRunId: string | undefined;

View File

@ -63,6 +63,8 @@ export interface GhidraJavaObservation {
/** Narrow synchronous seam used by provider discovery without launching Ghidra. */
export interface GhidraInstallationHost {
readonly platform?: NodeJS.Platform;
readonly architecture?: NodeJS.Architecture;
readText(path: string): string | undefined;
executable(path: string): boolean;
probeJava(

View File

@ -415,6 +415,12 @@ export class GhidraProvider implements AnalysisProviderCandidate {
...(this.config.ghidraJavaHome === undefined
? {}
: { javaHome: this.config.ghidraJavaHome }),
...(this.installationHost?.platform === undefined
? {}
: { platform: this.installationHost.platform }),
...(this.installationHost?.architecture === undefined
? {}
: { architecture: this.installationHost.architecture }),
};
this.#installation ??=
this.installationHost === undefined

View File

@ -6,7 +6,6 @@ import { EnhancedTools } from "../src/application/EnhancedTools.js";
import { AnalysisProviderRegistry } from "../src/application/AnalysisProviderRegistry.js";
import { SessionProviderRouter } from "../src/application/SessionProviderRouter.js";
import { parseConfig } from "../src/config.js";
import { parseEvidence } from "../src/domain/evidence.js";
import type { JsonValue } from "../src/domain/jsonValue.js";
import { ok } from "../src/domain/result.js";
import type { GhidraInstallationHost } from "../src/ghidra/GhidraInstallation.js";
@ -67,7 +66,8 @@ describe("Ghidra MCP and shared CLI composition", () => {
});
expect(opened.isError).not.toBe(true);
const listed = parseEvidence(
const listed = sessionEvidence(
session,
(
await mcp.callTool({
name: "list_procedures",
@ -104,7 +104,8 @@ describe("Ghidra MCP and shared CLI composition", () => {
},
});
const mcpOverview = parseEvidence(
const mcpOverview = sessionEvidence(
session,
(
await mcp.callTool({
name: "binary_overview",
@ -131,7 +132,8 @@ describe("Ghidra MCP and shared CLI composition", () => {
},
});
const pseudocode = parseEvidence(
const pseudocode = sessionEvidence(
session,
(
await mcp.callTool({
name: "procedure_pseudo_code",
@ -149,7 +151,8 @@ describe("Ghidra MCP and shared CLI composition", () => {
]),
});
const analyzed = parseEvidence(
const analyzed = sessionEvidence(
session,
(
await mcp.callTool({
name: "analyze_function",
@ -206,7 +209,7 @@ describe("Ghidra MCP and shared CLI composition", () => {
});
expect(batch.isError).not.toBe(true);
expect(batch.structuredContent).toMatchObject({
normalized_result: {
result: {
succeeded: 1,
failed: 0,
items: [{ status: "ok" }],
@ -269,7 +272,24 @@ describe("Ghidra MCP and shared CLI composition", () => {
});
});
const sessionEvidence = (session: BinarySession, value: unknown) => {
if (
typeof value !== "object" ||
value === null ||
!("evidence_id" in value) ||
typeof value.evidence_id !== "string"
)
throw new TypeError(
`Missing compact Evidence ID: ${JSON.stringify(value)}`,
);
const evidence = session.evidenceById(value.evidence_id);
if (evidence === undefined) throw new TypeError("Missing session Evidence");
return evidence;
};
const installationHost = (): GhidraInstallationHost => ({
platform: "linux",
architecture: "x64",
readText: () => "application.version=12.1.2\n",
executable: () => true,
probeJava: () => ({

View File

@ -16,6 +16,8 @@ import { silentLogger } from "../src/logger.js";
const INSTALL = "/opt/ghidra_12.1.2_PUBLIC";
const installationHost = (): GhidraInstallationHost => ({
platform: "linux",
architecture: "x64",
readText: () => "application.version=12.1.2\n",
executable: () => true,
probeJava: () => ({

View File

@ -24,7 +24,7 @@ afterEach(async () => {
);
});
describe("curl installer scenarios", () => {
describe("curl installer scenarios", { timeout: 20_000 }, () => {
it("installs only a pinned REA CLI with closed stdin", async () => {
const fixture = await createFixture();
const result = await runInstaller(fixture, ["--version", "0.3.0"]);

View File

@ -44,7 +44,7 @@ describe("native DMG artifact reader", () => {
const provenance = reader.provenance();
if (provenance[0] !== undefined)
Reflect.set(provenance[0], "tool", "forged");
expect(reader.provenance()[0]?.tool).toBe("hdiutil");
expect(reader.provenance()[0]?.tool).toBe("/usr/bin/hdiutil");
await reader.close();
expect(calls).toContainEqual(["verify", "/tmp/image.dmg"]);
expect(calls).toContainEqual(["detach", "/dev/disk-fixture"]);

View File

@ -30,7 +30,7 @@ describe("native macOS provider", () => {
return Promise.resolve(
resolutions === 1
? err(new NativeCommandFailure(tool, "unavailable"))
: ok({ path: "/bin/true", sha256: "a".repeat(64) }),
: ok({ path: "/usr/bin/true", sha256: "a".repeat(64) }),
);
});
const options = { timeoutMs: 1_000, maxOutputBytes: 1_024 };

View File

@ -1,4 +1,6 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { realpath } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import type { AnalysisOperationPort } from "../src/application/AnalysisProvider.js";
@ -8,6 +10,7 @@ import { createServer } from "../src/server/createServer.js";
describe("MCP permission preflight", () => {
it("denies extraction before dispatch and returns exact typed remediation", async () => {
const outputRoot = join(await realpath("/tmp"), "rea-permission-denied");
let dispatches = 0;
const analysis: AnalysisOperationPort = {
execute: () => {
@ -28,7 +31,7 @@ describe("MCP permission preflight", () => {
name: "extract_artifact",
arguments: {
approved: true,
output_root: "/tmp/rea-permission-denied",
output_root: outputRoot,
occurrence_ids: [`occ_${"0".repeat(64)}`],
},
});
@ -41,7 +44,7 @@ describe("MCP permission preflight", () => {
code: "permission_required",
details: {
capability: "artifact_extract",
missing: { roots: ["/tmp/rea-permission-denied"] },
missing: { roots: [outputRoot] },
},
remediation: {
restart_required: false,

View File

@ -249,6 +249,15 @@ describe("process capture domain", () => {
after: initialized,
}),
).toBe(true);
expect(
isInitializedPtyRoot({
rootPid: 100,
expectedRunId: "run-token",
before: { ...initialized, session_id: null },
observedRunId: "run-token",
after: { ...initialized, session_id: null },
}),
).toBe(true);
});
it("keeps interaction and shim residual uncertainty in separate scopes", () => {

View File

@ -1,3 +1,4 @@
import { realpathSync } from "node:fs";
import { availableParallelism, tmpdir } from "node:os";
import { join } from "node:path";
@ -5,6 +6,7 @@ import { defineConfig } from "vitest/config";
const TEST_FILES = ["tests/**/*.test.ts"];
const PROCESS_CAPTURE_TEST = "tests/processCapture.test.ts";
const CANONICAL_TEMPORARY_DIRECTORY = realpathSync(tmpdir());
const SERIAL_INTEGRATION_TESTS = [
"tests/browserCli.test.ts",
"tests/cliOutput.test.ts",
@ -37,6 +39,7 @@ const dualCoreProjects =
export default defineConfig({
test: {
include: TEST_FILES,
env: { TMPDIR: CANONICAL_TEMPORARY_DIRECTORY },
...dualCoreProjects,
retry: 2,
reporters: ["default", "verbose"],