fix(test): make host fixtures portable
This commit is contained in:
parent
b2d1f2affd
commit
f23f52903c
|
|
@ -40,16 +40,22 @@ export const isInitializedPtyRoot = (options: {
|
||||||
readonly before: ProcessIdentitySnapshot;
|
readonly before: ProcessIdentitySnapshot;
|
||||||
readonly observedRunId: string | undefined;
|
readonly observedRunId: string | undefined;
|
||||||
readonly after: ProcessIdentitySnapshot;
|
readonly after: ProcessIdentitySnapshot;
|
||||||
}): boolean =>
|
}): boolean => {
|
||||||
options.before.pid === options.rootPid &&
|
const sessionIdentityMatches =
|
||||||
options.before.process_group_id === options.rootPid &&
|
(options.before.session_id === options.rootPid &&
|
||||||
options.before.session_id === options.rootPid &&
|
options.after.session_id === options.rootPid) ||
|
||||||
options.after.pid === options.rootPid &&
|
(options.before.session_id === null && options.after.session_id === null);
|
||||||
options.after.parent_pid === options.before.parent_pid &&
|
return (
|
||||||
options.after.process_group_id === options.rootPid &&
|
options.before.pid === options.rootPid &&
|
||||||
options.after.session_id === options.rootPid &&
|
options.before.process_group_id === options.rootPid &&
|
||||||
options.after.startTime === options.before.startTime &&
|
sessionIdentityMatches &&
|
||||||
options.observedRunId === options.expectedRunId;
|
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 =>
|
const isExpectedProcReadFailure = (cause: unknown): boolean =>
|
||||||
cause instanceof Error &&
|
cause instanceof Error &&
|
||||||
|
|
@ -315,7 +321,7 @@ const readPsRows = async (
|
||||||
pid: Number(match[1]),
|
pid: Number(match[1]),
|
||||||
parent_pid: Number(match[2]),
|
parent_pid: Number(match[2]),
|
||||||
process_group_id: Number(match[3]),
|
process_group_id: Number(match[3]),
|
||||||
session_id: Number(match[4]),
|
session_id: Number(match[4]) || null,
|
||||||
command: match[5] ?? "",
|
command: match[5] ?? "",
|
||||||
startTime: undefined,
|
startTime: undefined,
|
||||||
}))
|
}))
|
||||||
|
|
@ -346,7 +352,7 @@ const inspectInitializedPtyRoot = async (
|
||||||
if (
|
if (
|
||||||
before === undefined ||
|
before === undefined ||
|
||||||
before.process_group_id !== rootPid ||
|
before.process_group_id !== rootPid ||
|
||||||
before.session_id !== rootPid
|
(before.session_id !== rootPid && before.session_id !== null)
|
||||||
)
|
)
|
||||||
return undefined;
|
return undefined;
|
||||||
let observedRunId: string | undefined;
|
let observedRunId: string | undefined;
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,8 @@ export interface GhidraJavaObservation {
|
||||||
|
|
||||||
/** Narrow synchronous seam used by provider discovery without launching Ghidra. */
|
/** Narrow synchronous seam used by provider discovery without launching Ghidra. */
|
||||||
export interface GhidraInstallationHost {
|
export interface GhidraInstallationHost {
|
||||||
|
readonly platform?: NodeJS.Platform;
|
||||||
|
readonly architecture?: NodeJS.Architecture;
|
||||||
readText(path: string): string | undefined;
|
readText(path: string): string | undefined;
|
||||||
executable(path: string): boolean;
|
executable(path: string): boolean;
|
||||||
probeJava(
|
probeJava(
|
||||||
|
|
|
||||||
|
|
@ -415,6 +415,12 @@ export class GhidraProvider implements AnalysisProviderCandidate {
|
||||||
...(this.config.ghidraJavaHome === undefined
|
...(this.config.ghidraJavaHome === undefined
|
||||||
? {}
|
? {}
|
||||||
: { javaHome: this.config.ghidraJavaHome }),
|
: { javaHome: this.config.ghidraJavaHome }),
|
||||||
|
...(this.installationHost?.platform === undefined
|
||||||
|
? {}
|
||||||
|
: { platform: this.installationHost.platform }),
|
||||||
|
...(this.installationHost?.architecture === undefined
|
||||||
|
? {}
|
||||||
|
: { architecture: this.installationHost.architecture }),
|
||||||
};
|
};
|
||||||
this.#installation ??=
|
this.#installation ??=
|
||||||
this.installationHost === undefined
|
this.installationHost === undefined
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { EnhancedTools } from "../src/application/EnhancedTools.js";
|
||||||
import { AnalysisProviderRegistry } from "../src/application/AnalysisProviderRegistry.js";
|
import { AnalysisProviderRegistry } from "../src/application/AnalysisProviderRegistry.js";
|
||||||
import { SessionProviderRouter } from "../src/application/SessionProviderRouter.js";
|
import { SessionProviderRouter } from "../src/application/SessionProviderRouter.js";
|
||||||
import { parseConfig } from "../src/config.js";
|
import { parseConfig } from "../src/config.js";
|
||||||
import { parseEvidence } from "../src/domain/evidence.js";
|
|
||||||
import type { JsonValue } from "../src/domain/jsonValue.js";
|
import type { JsonValue } from "../src/domain/jsonValue.js";
|
||||||
import { ok } from "../src/domain/result.js";
|
import { ok } from "../src/domain/result.js";
|
||||||
import type { GhidraInstallationHost } from "../src/ghidra/GhidraInstallation.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);
|
expect(opened.isError).not.toBe(true);
|
||||||
|
|
||||||
const listed = parseEvidence(
|
const listed = sessionEvidence(
|
||||||
|
session,
|
||||||
(
|
(
|
||||||
await mcp.callTool({
|
await mcp.callTool({
|
||||||
name: "list_procedures",
|
name: "list_procedures",
|
||||||
|
|
@ -104,7 +104,8 @@ describe("Ghidra MCP and shared CLI composition", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mcpOverview = parseEvidence(
|
const mcpOverview = sessionEvidence(
|
||||||
|
session,
|
||||||
(
|
(
|
||||||
await mcp.callTool({
|
await mcp.callTool({
|
||||||
name: "binary_overview",
|
name: "binary_overview",
|
||||||
|
|
@ -131,7 +132,8 @@ describe("Ghidra MCP and shared CLI composition", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const pseudocode = parseEvidence(
|
const pseudocode = sessionEvidence(
|
||||||
|
session,
|
||||||
(
|
(
|
||||||
await mcp.callTool({
|
await mcp.callTool({
|
||||||
name: "procedure_pseudo_code",
|
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({
|
await mcp.callTool({
|
||||||
name: "analyze_function",
|
name: "analyze_function",
|
||||||
|
|
@ -206,7 +209,7 @@ describe("Ghidra MCP and shared CLI composition", () => {
|
||||||
});
|
});
|
||||||
expect(batch.isError).not.toBe(true);
|
expect(batch.isError).not.toBe(true);
|
||||||
expect(batch.structuredContent).toMatchObject({
|
expect(batch.structuredContent).toMatchObject({
|
||||||
normalized_result: {
|
result: {
|
||||||
succeeded: 1,
|
succeeded: 1,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
items: [{ status: "ok" }],
|
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 => ({
|
const installationHost = (): GhidraInstallationHost => ({
|
||||||
|
platform: "linux",
|
||||||
|
architecture: "x64",
|
||||||
readText: () => "application.version=12.1.2\n",
|
readText: () => "application.version=12.1.2\n",
|
||||||
executable: () => true,
|
executable: () => true,
|
||||||
probeJava: () => ({
|
probeJava: () => ({
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import { silentLogger } from "../src/logger.js";
|
||||||
|
|
||||||
const INSTALL = "/opt/ghidra_12.1.2_PUBLIC";
|
const INSTALL = "/opt/ghidra_12.1.2_PUBLIC";
|
||||||
const installationHost = (): GhidraInstallationHost => ({
|
const installationHost = (): GhidraInstallationHost => ({
|
||||||
|
platform: "linux",
|
||||||
|
architecture: "x64",
|
||||||
readText: () => "application.version=12.1.2\n",
|
readText: () => "application.version=12.1.2\n",
|
||||||
executable: () => true,
|
executable: () => true,
|
||||||
probeJava: () => ({
|
probeJava: () => ({
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
it("installs only a pinned REA CLI with closed stdin", async () => {
|
||||||
const fixture = await createFixture();
|
const fixture = await createFixture();
|
||||||
const result = await runInstaller(fixture, ["--version", "0.3.0"]);
|
const result = await runInstaller(fixture, ["--version", "0.3.0"]);
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ describe("native DMG artifact reader", () => {
|
||||||
const provenance = reader.provenance();
|
const provenance = reader.provenance();
|
||||||
if (provenance[0] !== undefined)
|
if (provenance[0] !== undefined)
|
||||||
Reflect.set(provenance[0], "tool", "forged");
|
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();
|
await reader.close();
|
||||||
expect(calls).toContainEqual(["verify", "/tmp/image.dmg"]);
|
expect(calls).toContainEqual(["verify", "/tmp/image.dmg"]);
|
||||||
expect(calls).toContainEqual(["detach", "/dev/disk-fixture"]);
|
expect(calls).toContainEqual(["detach", "/dev/disk-fixture"]);
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ describe("native macOS provider", () => {
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
resolutions === 1
|
resolutions === 1
|
||||||
? err(new NativeCommandFailure(tool, "unavailable"))
|
? 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 };
|
const options = { timeoutMs: 1_000, maxOutputBytes: 1_024 };
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||||
|
import { realpath } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import type { AnalysisOperationPort } from "../src/application/AnalysisProvider.js";
|
import type { AnalysisOperationPort } from "../src/application/AnalysisProvider.js";
|
||||||
|
|
@ -8,6 +10,7 @@ import { createServer } from "../src/server/createServer.js";
|
||||||
|
|
||||||
describe("MCP permission preflight", () => {
|
describe("MCP permission preflight", () => {
|
||||||
it("denies extraction before dispatch and returns exact typed remediation", async () => {
|
it("denies extraction before dispatch and returns exact typed remediation", async () => {
|
||||||
|
const outputRoot = join(await realpath("/tmp"), "rea-permission-denied");
|
||||||
let dispatches = 0;
|
let dispatches = 0;
|
||||||
const analysis: AnalysisOperationPort = {
|
const analysis: AnalysisOperationPort = {
|
||||||
execute: () => {
|
execute: () => {
|
||||||
|
|
@ -28,7 +31,7 @@ describe("MCP permission preflight", () => {
|
||||||
name: "extract_artifact",
|
name: "extract_artifact",
|
||||||
arguments: {
|
arguments: {
|
||||||
approved: true,
|
approved: true,
|
||||||
output_root: "/tmp/rea-permission-denied",
|
output_root: outputRoot,
|
||||||
occurrence_ids: [`occ_${"0".repeat(64)}`],
|
occurrence_ids: [`occ_${"0".repeat(64)}`],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -41,7 +44,7 @@ describe("MCP permission preflight", () => {
|
||||||
code: "permission_required",
|
code: "permission_required",
|
||||||
details: {
|
details: {
|
||||||
capability: "artifact_extract",
|
capability: "artifact_extract",
|
||||||
missing: { roots: ["/tmp/rea-permission-denied"] },
|
missing: { roots: [outputRoot] },
|
||||||
},
|
},
|
||||||
remediation: {
|
remediation: {
|
||||||
restart_required: false,
|
restart_required: false,
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,15 @@ describe("process capture domain", () => {
|
||||||
after: initialized,
|
after: initialized,
|
||||||
}),
|
}),
|
||||||
).toBe(true);
|
).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", () => {
|
it("keeps interaction and shim residual uncertainty in separate scopes", () => {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { realpathSync } from "node:fs";
|
||||||
import { availableParallelism, tmpdir } from "node:os";
|
import { availableParallelism, tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
|
@ -5,6 +6,7 @@ import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
const TEST_FILES = ["tests/**/*.test.ts"];
|
const TEST_FILES = ["tests/**/*.test.ts"];
|
||||||
const PROCESS_CAPTURE_TEST = "tests/processCapture.test.ts";
|
const PROCESS_CAPTURE_TEST = "tests/processCapture.test.ts";
|
||||||
|
const CANONICAL_TEMPORARY_DIRECTORY = realpathSync(tmpdir());
|
||||||
const SERIAL_INTEGRATION_TESTS = [
|
const SERIAL_INTEGRATION_TESTS = [
|
||||||
"tests/browserCli.test.ts",
|
"tests/browserCli.test.ts",
|
||||||
"tests/cliOutput.test.ts",
|
"tests/cliOutput.test.ts",
|
||||||
|
|
@ -37,6 +39,7 @@ const dualCoreProjects =
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
include: TEST_FILES,
|
include: TEST_FILES,
|
||||||
|
env: { TMPDIR: CANONICAL_TEMPORARY_DIRECTORY },
|
||||||
...dualCoreProjects,
|
...dualCoreProjects,
|
||||||
retry: 2,
|
retry: 2,
|
||||||
reporters: ["default", "verbose"],
|
reporters: ["default", "verbose"],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue