Compare commits

...
6 changed files with 170 additions and 162 deletions

No files matched your search

@@ -1,33 +1,13 @@
import { projectCliAnalysisResult } from "../../../src/cli/projectCliAnalysisResult.js";
import { chmod, writeFile } from "node:fs/promises";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { withArtifactResourceScope } from "../../fixtures/artifactInventory.js";
import { createInspectionResources } from "../../fixtures/inspectionResources.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { writeAnalysisSnapshot } from "../../../src/application/binary/AnalysisSnapshotFiles.js";
import { createDirectAnalysis } from "../../../src/composition/directAnalysis.js";
import type { AnalysisSnapshot } from "../../../src/domain/analysisSnapshot.js";
import {
snapshotBinding,
snapshotTarget,
} from "../../../src/domain/analysisSnapshot.js";
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { REA_WORKFLOW_PROVIDER } from "../../../src/domain/investigationProviders.js";
import { workflowAnalysisProfile } from "../../../src/application/WorkflowAnalysisProfile.js";
import { HOPPER_PROVIDER_IDENTITY } from "../../../src/hopper/HopperProviderMetadata.js";
import { resolveHopperAnalysisProfile } from "../../../src/hopper/HopperAnalysisProfile.js";
let inspectionResources = createInspectionResources();
afterEach(async () => {
await inspectionResources.close();
inspectionResources = createInspectionResources();
afterEach(() => {
vi.unstubAllEnvs();
});
@@ -60,55 +40,4 @@ describe("direct analysis snapshot files", () => {
},
});
});
it("does not replay unbound historical Evidence from a supplied snapshot", async () => {
const directory = await createTestTempDirectory("rea-direct-snapshot-");
const snapshotPath = join(directory, "analysis.json");
const launcherPath = join(directory, "hopper-launcher");
await writeFile(launcherPath, "fixture Hopper launcher");
await chmod(launcherPath, 0o755);
const target = await parseBinaryTarget(process.execPath);
if (!target.ok) throw target.error;
const resolved = await resolveHopperAnalysisProfile(target.value, {
launcherPath,
loaderArgsOverride: [],
provider: HOPPER_PROVIDER_IDENTITY,
resourceScope: inspectionResources.owner,
});
if (!resolved.ok || resolved.value.profile === null)
throw new Error("fixture Hopper profile did not resolve");
const profile = resolved.value.profile;
const workflowProfile = workflowAnalysisProfile(profile);
const evidence = createEvidence(target.value, REA_WORKFLOW_PROVIDER, {
operation: "binary_overview",
parameters: {},
result: { cached: true },
analysisProfile: workflowProfile,
});
const snapshot: AnalysisSnapshot = {
target: snapshotTarget(target.value),
binding: snapshotBinding(profile),
entries: [],
workflow_entries: [],
evidence_bundle: createEvidenceBundle([evidence]),
};
const written = await withArtifactResourceScope((resources) =>
writeAnalysisSnapshot(snapshot, snapshotPath, false, resources),
);
if (!written.ok) throw written.error;
vi.stubEnv("HOPPER_LAUNCHER_PATH", launcherPath);
await expect(
createDirectAnalysis(process.env)
.runDirectAnalysis(
process.execPath,
"binary_overview",
{},
{
snapshotPath,
},
)
.then(projectCliAnalysisResult),
).resolves.not.toEqual(evidence);
});
});
@@ -150,6 +150,7 @@ const expectSourceSnapshotRetained = (snapshot: AnalysisSnapshot) => {
interface SnapshotProviderTrace {
readonly starts: string[];
readonly sources: Map<string, Evidence>;
readonly queries: Array<{
readonly operation: string;
readonly parameters: Readonly<Record<string, unknown>>;
@@ -256,25 +257,24 @@ const makeProvider = (
`Unmodeled snapshot provider operation: ${operation}`,
);
const result = results[operation];
const source = options.sourceEvidence
? createEvidence(target, profile.provider, {
operation: "health",
parameters: {},
result: "fixture source",
...(options.sourceMissingLink
? { evidenceLinks: [`ev_${"0".repeat(64)}`] }
: {}),
})
: undefined;
if (source !== undefined)
trace.sources.set(source.evidence_id, source);
return ok(
createAnalysisExecution(result, profile.provider, {
analysisProfile: profile,
rawResult: result,
limitations: options.limitations ?? [],
...(options.sourceEvidence
? {
sourceEvidence: [
createEvidence(target, profile.provider, {
operation: "health",
parameters: {},
result: "fixture source",
...(options.sourceMissingLink
? { evidenceLinks: [`ev_${"0".repeat(64)}`] }
: {}),
}),
],
}
: {}),
...(source === undefined ? {} : { sourceEvidence: [source] }),
}),
);
},
@@ -293,6 +293,7 @@ const createSnapshotReplayFixture = async (
const snapshotPath = join(directory, snapshotRelativePath);
await writeFile(path, "fixture");
const starts: string[] = [];
const sources = new Map<string, Evidence>();
const queries: SnapshotProviderTrace["queries"] = [];
const resolutions: string[] = [];
@@ -300,13 +301,14 @@ const createSnapshotReplayFixture = async (
path,
snapshotPath,
starts,
sources,
get calls() {
return queries.map(({ operation }) => operation);
},
queries,
resolutions,
createProvider(options: SnapshotProviderOptions = {}): AnalysisProvider {
return makeProvider({ starts, queries, resolutions }, options);
return makeProvider({ starts, sources, queries, resolutions }, options);
},
dependencies(provider: AnalysisProvider): DirectAnalysisDependencies {
return {
@@ -1075,7 +1077,7 @@ describe("Ghidra composed workflow runtime-budget admission", () => {
});
describe("binary overview snapshot replay", () => {
it("replays the identical binary overview without provider startup or calls", async () => {
it("replays bound overviews without provider work and recomposes unbound history", async () => {
const fixture = await createSnapshotReplayFixture("rea-workflow-snapshot-");
const provider = fixture.createProvider();
const dependencies = fixture.dependencies(provider);
@@ -1159,6 +1161,47 @@ describe("binary overview snapshot replay", () => {
(record) => record.evidence_id === firstEvidence.evidence_id,
)?.caller_input_path,
).toBe(fixture.path);
// Historical observations remain importable data without a replay binding.
// Retain the primitive entries and one current-profile historical overview,
// but omit its workflow entry and the bound current overview record.
const historicalOnly = withEarlierHistoricalEvidence(
loaded.value,
fixture.path,
current,
);
const unboundPath = join(dirname(fixture.snapshotPath), "unbound.json");
const unboundWritten = await withArtifactResourceScope((resources) =>
writeAnalysisSnapshot(
{
...historicalOnly,
workflow_entries: [],
evidence_bundle: createEvidenceBundle(
historicalOnly.evidence_bundle.records.filter(
(record) => record.evidence_id !== current.evidence_id,
),
),
},
unboundPath,
false,
resources,
),
);
if (!unboundWritten.ok) throw unboundWritten.error;
const recomposed = parseEvidence(
await runDirectAnalysis(
dependencies,
fixture.path,
"binary_overview",
{},
{ snapshotPath: unboundPath },
).then(projectCliAnalysisResult),
);
expect(recomposed.operation).toBe("binary_overview");
expect(recomposed.normalized_result).toEqual(
firstEvidence.normalized_result,
);
expect(recomposed.caller_input_path).toBe(fixture.path);
});
});
@@ -2279,6 +2322,8 @@ it("publishes captured function sources when completed parent admission fails",
{ includeEvidenceSources: true },
);
expect(result.ok).toBe(false);
const sourceIds = [...fixture.sources.keys()];
expect(sourceIds).toHaveLength(1);
const outputPath = join(dirname(fixture.snapshotPath), "refused.json");
const output = await publishCliAnalysisEvidence(result, {
providerId: IDENTITY.id,
@@ -2299,8 +2344,9 @@ it("publishes captured function sources when completed parent admission fails",
(record) => record.operation === "analyze_function",
);
expect(parent).toBeDefined();
expect(parent?.evidence_links).toEqual(sourceIds);
expect(bundle.records.map((record) => record.evidence_id).sort()).toEqual(
[parent!.evidence_id, ...parent!.evidence_links].sort(),
[parent!.evidence_id, ...sourceIds].sort(),
);
});
@@ -107,7 +107,21 @@ mcpTest(
name: "inspect_native_initializers",
arguments: { path },
});
expect(missing.isError).toBe(true);
expect(parseMcpToolError(missing).error).toMatchObject({
code: "invalid_request",
details: {
operation: "inspect_native_initializers",
issues: [
{
path: ["architecture"],
reason: "invalid_value",
message: expect.stringMatching(
/arm64 \(subtype 0x0\).*select one architecture/u,
),
},
],
},
});
const selected = await client.callTool({
name: "inspect_native_initializers",
arguments: { path, architecture: "arm64" },
+35
View File
@@ -0,0 +1,35 @@
import spawn from "cross-spawn";
import { existsSync } from "node:fs";
import { registerHooks } from "node:module";
const supervisor = new URL(
"../../scripts/run-exclusive-command.mjs",
import.meta.url,
).href;
// Replace only the supervisor's dependency, leaving its actual source and the
// public wrapper intact. Other imports still use the real implementation.
registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === "cross-spawn" && context.parentURL === supervisor)
return { url: import.meta.url, shortCircuit: true };
return nextResolve(specifier, context);
},
});
export default function spawnWithPostSpawnError(command, args, options) {
const child = spawn(command, args, options);
if (args.at(-1) !== "rea-runner-post-spawn-error") return child;
let timer;
child.once("close", () => clearInterval(timer));
child.once("spawn", () => {
timer = setInterval(() => {
if (!existsSync(process.env.REA_TEST_RUNNER_READY_PATH)) return;
clearInterval(timer);
process.stdout.write("controlled-post-spawn-error\n");
child.emit("error", new Error("controlled post-spawn child error"));
child.emit("error", new Error("controlled secondary child error"));
}, 5);
});
return child;
}
@@ -32,10 +32,7 @@ import {
import { prepareProcessOwnershipInspection } from "../../src/process/ProcessOwnershipObservation.js";
import { spawnOwnedProviderProcess } from "../../src/process/ProviderProcess.js";
import { PrivateRuntimeRoot } from "../../src/process/PrivateRuntimeRoot.js";
import {
withArtifactFileHandle,
ArtifactFileCleanupFailure,
} from "../../src/artifacts/ArtifactFileOperation.js";
import { ArtifactFileCleanupFailure } from "../../src/artifacts/ArtifactFileOperation.js";
import * as artifacts from "../../src/artifacts/readStableArtifact.js";
import {
recordedCrashFixture,
@@ -406,13 +403,6 @@ it.runIf(supportedHost).each(["decoder", "debugger"])(
const resources = args[4];
if (resources === undefined)
throw new Error("Reply must have an artifact resource owner");
const file = await open(reply, "r");
const close = file.close.bind(file);
emergencyCloses.push(close);
file.close = async () => {
if (!releaseAllowed) throw closeFailure;
await close();
};
const retain = resources.retain.bind(resources);
const spy = vi
.spyOn(resources, "retain")
@@ -421,12 +411,25 @@ it.runIf(supportedHost).each(["decoder", "debugger"])(
controller.abort(reason);
});
try {
return await withArtifactFileHandle(
reply,
file,
return await read(
args[0],
args[1],
args[2],
{
lstat: (filePath) => lstat(filePath, { bigint: true }),
open: async (filePath, flags) => {
const file = await open(filePath, flags);
const close = file.close.bind(file);
emergencyCloses.push(close);
file.close = async () => {
if (!releaseAllowed) throw closeFailure;
await close();
};
return file;
},
},
resources,
controller.signal,
() => read(...args),
args[5],
);
} finally {
spy.mockRestore();
+36 -55
View File
@@ -7,19 +7,14 @@ import {
utimes,
writeFile,
} from "node:fs/promises";
import { createRequire } from "node:module";
import { join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { fileURLToPath } from "node:url";
import { setTimeout as delay } from "node:timers/promises";
import { expect, it, onTestFinished, vi } from "vitest";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
const supervisorPath = fileURLToPath(
new URL("../../scripts/run-exclusive-command.mjs", import.meta.url),
);
const runnerPath = fileURLToPath(
new URL("../../scripts/run-exclusive.mjs", import.meta.url),
);
@@ -654,39 +649,10 @@ it.runIf(process.platform !== "win32")(
const readyPath = join(directory, "command-ready");
const gatePath = join(directory, "allow-command-exit");
const closedPath = join(directory, "command-closed");
const source = await readFile(supervisorPath, "utf8");
const crossSpawn = pathToFileURL(
createRequire(import.meta.url).resolve("cross-spawn"),
const preload = new URL(
"../fixtures/runnerPostSpawnError.mjs",
import.meta.url,
).href;
const spawnCall =
/child = spawn\(command\[0\], command\.slice\(1\), \{\n\s+cwd: process\.cwd\(\),\n\s+env: process\.env,\n\s+stdio: "inherit",\n\s+\}\);/u;
const postSpawnInjection = `child.once("spawn", () => {
const waitForReady = setInterval(() => {
if (!existsSync(${JSON.stringify(readyPath)})) return;
clearInterval(waitForReady);
process.stdout.write("controlled-post-spawn-error\\n");
child.emit("error", new Error("controlled post-spawn child error"));
child.emit("error", new Error("controlled secondary child error"));
}, 5);
});`;
expect(source).toMatch(spawnCall);
const injectedSource = source
.replace(
'import spawn from "cross-spawn";',
`import spawn from ${JSON.stringify(crossSpawn)};\nimport { existsSync } from "node:fs";`,
)
.replace(spawnCall, (call) => `${call}\n ${postSpawnInjection}`);
expect(injectedSource).toContain(postSpawnInjection);
expect(injectedSource).toContain("controlled post-spawn child error");
const copiedSupervisor = join(directory, "run-exclusive-command.mjs");
const copiedHelperDirectory = join(directory, "lib");
await mkdir(copiedHelperDirectory, { recursive: true });
await copyFile(
join(process.cwd(), "scripts/lib/windows-owned-command.mjs"),
join(copiedHelperDirectory, "windows-owned-command.mjs"),
);
await writeFile(copiedSupervisor, injectedSource);
const gatedCommand = `
const fs = require("node:fs");
fs.writeFileSync(${JSON.stringify(readyPath)}, "ready");
@@ -698,17 +664,34 @@ it.runIf(process.platform !== "win32")(
process.stdout.write("command-closed\\n");
}, 5);
`;
const supervisor = spawn(
const runner = spawn(
process.execPath,
[copiedSupervisor, "test", process.execPath, "-e", gatedCommand],
{ cwd: directory, stdio: ["ignore", "pipe", "pipe", "ipc"] },
[
runnerPath,
"test",
process.execPath,
"-e",
gatedCommand,
"rea-runner-post-spawn-error",
],
{
cwd: directory,
stdio: ["ignore", "pipe", "pipe"],
env: {
...process.env,
NODE_OPTIONS: [process.env.NODE_OPTIONS, `--import=${preload}`]
.filter(Boolean)
.join(" "),
REA_TEST_RUNNER_READY_PATH: readyPath,
},
},
);
if (supervisor.stdout === null || supervisor.stderr === null) {
supervisor.kill("SIGKILL");
throw new Error("supervisor output pipes were not created");
if (runner.stdout === null || runner.stderr === null) {
runner.kill("SIGKILL");
throw new Error("runner output pipes were not created");
}
const stdoutStream = supervisor.stdout;
const stderrStream = supervisor.stderr;
const stdoutStream = runner.stdout;
const stderrStream = runner.stderr;
let stdout = "";
let stderr = "";
const outputWaiters = new Map<string, (() => void)[]>();
@@ -730,19 +713,17 @@ it.runIf(process.platform !== "win32")(
resolveOutput,
]);
}),
`supervisor did not write ${marker}`,
`runner did not write ${marker}`,
);
};
const exited = withTimeout(
const closed = withTimeout(
new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
(resolveExit, reject) => {
supervisor.once("error", reject);
supervisor.once("exit", (code, signal) =>
resolveExit({ code, signal }),
);
runner.once("error", reject);
runner.once("close", (code, signal) => resolveExit({ code, signal }));
},
),
"supervisor did not exit",
"runner did not exit",
);
try {
await waitForOutput("controlled-post-spawn-error");
@@ -759,7 +740,7 @@ it.runIf(process.platform !== "win32")(
await writeFile(gatePath, "open");
await waitForOutput("command-closed");
expect(await exited).toEqual({ code: 1, signal: null });
expect(await closed).toEqual({ code: 1, signal: null });
expect(stderr).toContain("controlled post-spawn child error");
expect(stderr).not.toContain("controlled secondary child error");
expect(
@@ -771,7 +752,7 @@ it.runIf(process.platform !== "win32")(
} finally {
await writeFile(gatePath, "open");
await waitForOutput("command-closed").catch(() => undefined);
await exited.catch(() => undefined);
await closed.catch(() => undefined);
}
},
);