From f23f52903cb6df4eed6fec0e09f79c82ad9825dc Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 16 Jul 2026 18:51:14 +0800 Subject: [PATCH] fix(test): make host fixtures portable --- src/application/ProcessSampling.ts | 30 +++++++++++++++---------- src/ghidra/GhidraInstallation.ts | 2 ++ src/ghidra/GhidraProvider.ts | 6 +++++ tests/ghidraMcp.test.ts | 32 ++++++++++++++++++++++----- tests/ghidraProvider.test.ts | 2 ++ tests/installScript.test.ts | 2 +- tests/nativeDmgArtifactReader.test.ts | 2 +- tests/nativeProvider.test.ts | 2 +- tests/permissionMcp.test.ts | 7 ++++-- tests/processCapture.test.ts | 9 ++++++++ vitest.config.ts | 3 +++ 11 files changed, 74 insertions(+), 23 deletions(-) diff --git a/src/application/ProcessSampling.ts b/src/application/ProcessSampling.ts index 5f2a07289..fe5f51a21 100644 --- a/src/application/ProcessSampling.ts +++ b/src/application/ProcessSampling.ts @@ -40,16 +40,22 @@ export const isInitializedPtyRoot = (options: { readonly before: ProcessIdentitySnapshot; readonly observedRunId: string | undefined; readonly after: ProcessIdentitySnapshot; -}): boolean => - options.before.pid === options.rootPid && - options.before.process_group_id === options.rootPid && - options.before.session_id === options.rootPid && - 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; +}): 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 && + sessionIdentityMatches && + options.after.pid === options.rootPid && + options.after.parent_pid === options.before.parent_pid && + options.after.process_group_id === options.rootPid && + options.after.startTime === options.before.startTime && + 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; diff --git a/src/ghidra/GhidraInstallation.ts b/src/ghidra/GhidraInstallation.ts index e10d74a69..f71c465f8 100644 --- a/src/ghidra/GhidraInstallation.ts +++ b/src/ghidra/GhidraInstallation.ts @@ -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( diff --git a/src/ghidra/GhidraProvider.ts b/src/ghidra/GhidraProvider.ts index c9efc666b..5bc699391 100644 --- a/src/ghidra/GhidraProvider.ts +++ b/src/ghidra/GhidraProvider.ts @@ -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 diff --git a/tests/ghidraMcp.test.ts b/tests/ghidraMcp.test.ts index a825640ea..30d665f44 100644 --- a/tests/ghidraMcp.test.ts +++ b/tests/ghidraMcp.test.ts @@ -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: () => ({ diff --git a/tests/ghidraProvider.test.ts b/tests/ghidraProvider.test.ts index 4e81fcb9f..1b32487bf 100644 --- a/tests/ghidraProvider.test.ts +++ b/tests/ghidraProvider.test.ts @@ -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: () => ({ diff --git a/tests/installScript.test.ts b/tests/installScript.test.ts index b1b182ec9..db7aa4b01 100644 --- a/tests/installScript.test.ts +++ b/tests/installScript.test.ts @@ -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"]); diff --git a/tests/nativeDmgArtifactReader.test.ts b/tests/nativeDmgArtifactReader.test.ts index 8959f4fc3..73cc1e50f 100644 --- a/tests/nativeDmgArtifactReader.test.ts +++ b/tests/nativeDmgArtifactReader.test.ts @@ -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"]); diff --git a/tests/nativeProvider.test.ts b/tests/nativeProvider.test.ts index c54916727..fecb0ae6d 100644 --- a/tests/nativeProvider.test.ts +++ b/tests/nativeProvider.test.ts @@ -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 }; diff --git a/tests/permissionMcp.test.ts b/tests/permissionMcp.test.ts index 012de05d1..698404acb 100644 --- a/tests/permissionMcp.test.ts +++ b/tests/permissionMcp.test.ts @@ -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, diff --git a/tests/processCapture.test.ts b/tests/processCapture.test.ts index 5d64a20e6..73808b0b8 100644 --- a/tests/processCapture.test.ts +++ b/tests/processCapture.test.ts @@ -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", () => { diff --git a/vitest.config.ts b/vitest.config.ts index 10acb3978..a71b18fdc 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -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"],