rea/tests/compositeProvider.test.ts

224 lines
7.0 KiB
TypeScript

import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { BinarySession } from "../src/application/BinarySession.js";
import { CompositeProvider } from "../src/application/CompositeProvider.js";
import type {
AnalysisOperation,
AnalysisProvider,
CapabilityDescriptor,
ProviderIdentity,
} from "../src/application/AnalysisProvider.js";
import { createAnalysisExecution } from "../src/application/AnalysisProvider.js";
import type { BinaryTarget } from "../src/domain/binaryTarget.js";
import { ok } from "../src/domain/result.js";
const target: BinaryTarget = {
path: "/fixture",
kind: "database",
format: "analysis-database",
sha256: "0".repeat(64),
};
let directory: string | undefined;
afterEach(async () => {
if (directory !== undefined)
await rm(directory, { recursive: true, force: true });
directory = undefined;
});
describe("composite analysis provider", () => {
it("routes disjoint operations and does not start children during health", async () => {
const hopperCalls: string[] = [];
const nativeCalls: string[] = [];
const hopper = provider("hopper", "address_name", hopperCalls);
const native = provider("native", "analyze_function", nativeCalls);
const composite = new CompositeProvider([hopper, native]);
const client = composite.createClient(target);
expect(await client.execute("health", {})).toEqual({
ok: true,
value: {
result: null,
rawResult: null,
provider: {
id: "composite:hopper+native",
name: "REA composite analysis provider",
version: null,
},
limitations: [],
locations: [],
subject: null,
},
});
expect(hopperCalls).toEqual([]);
expect(nativeCalls).toEqual([]);
expect(await client.execute("address_name", {})).toEqual({
ok: true,
value: createAnalysisExecution("hopper:address_name", hopper.identity()),
});
expect(await client.execute("analyze_function", {})).toEqual({
ok: true,
value: createAnalysisExecution(
"native:analyze_function",
native.identity(),
),
});
expect(hopperCalls).toEqual(["address_name"]);
expect(nativeCalls).toEqual(["analyze_function"]);
expect(await client.execute("list_names", {})).toMatchObject({
ok: false,
error: { _tag: "AnalysisCapabilityUnavailableError" },
});
});
it("publishes exact operation provenance and rejects ambiguous routes", () => {
const hopper = provider("hopper", "address_name", []);
const native = provider("native", "analyze_function", []);
const session = new BinarySession(new CompositeProvider([hopper, native]));
expect(session.providerIdentity("address_name").id).toBe("hopper");
expect(session.providerIdentity("analyze_function").id).toBe("native");
expect(session.providerIdentity("binary_overview").id).toBe("hopper");
expect(session.providerIdentity().id).toBe("composite:hopper+native");
expect(
() =>
new CompositeProvider([
hopper,
provider("duplicate", "address_name", []),
]),
).toThrow(/Multiple providers declare operation address_name/u);
});
it("retains lineage snapshots from two lazily started dynamic providers", async () => {
directory = await mkdtemp(join(tmpdir(), "rea-composite-lineage-"));
const targetPath = join(directory, "fixture.hop");
await writeFile(targetPath, "fixture");
const first = dynamicProvider("first", "address_name", 100);
const second = dynamicProvider("second", "analyze_function", 200);
const session = new BinarySession(new CompositeProvider([second, first]));
expect((await session.open(targetPath)).ok).toBe(true);
expect(await session.execute("address_name", {})).toMatchObject({
ok: true,
});
expect(await session.execute("analyze_function", {})).toMatchObject({
ok: true,
});
expect(session.status()).toMatchObject({
analysis_run: {
process_lineage: {
status: "snapshots",
snapshots: [
{
provider: { id: "first" },
observation: {
status: "verified",
observed_at: "2026-07-22T10:00:00.000Z",
launcher_pid: 100,
},
},
{
provider: { id: "second" },
observation: {
status: "verified",
observed_at: "2026-07-22T10:00:00.000Z",
launcher_pid: 200,
},
},
],
},
},
});
await session.close();
});
});
const dynamicProvider = (
id: string,
operation: Exclude<AnalysisOperation, "health">,
launcherPid: number,
): AnalysisProvider => {
const identity: ProviderIdentity = { id, name: id, version: "1" };
return {
identity: () => identity,
capabilities: () => [capability(identity, operation)],
createClient: (_target, _profile, context) => {
if (context === undefined)
throw new Error("missing analysis run context");
return {
execute: (called) =>
Promise.resolve(
ok(createAnalysisExecution(`${id}:${called}`, identity)),
),
runtimeLineageSnapshots: () => [
{
provider: identity,
observation: {
status: "verified",
observedAt: "2026-07-22T10:00:00.000Z",
lineage: {
schemaVersion: 1,
runId: context.runId,
launcherPid,
launcherParentPid: 1,
processGroupId: launcherPid,
descendants: [],
},
},
},
],
close: () => Promise.resolve(),
};
},
};
};
const provider = (
id: string,
operation: Exclude<AnalysisOperation, "health">,
calls: string[],
): AnalysisProvider => {
const identity: ProviderIdentity = { id, name: id, version: "1" };
return {
identity: () => identity,
capabilities: () => [capability(identity, operation)],
createClient: () => ({
execute: (called) => {
calls.push(called);
return Promise.resolve(
ok(createAnalysisExecution(`${id}:${called}`, identity)),
);
},
close: () => Promise.resolve(),
}),
};
};
const capability = (
providerIdentity: ProviderIdentity,
operation: Exclude<AnalysisOperation, "health">,
): CapabilityDescriptor => ({
provider: providerIdentity,
operation,
inputContractVersion: 1,
outputContractVersion: 1,
available: true,
reason: null,
pagination: "none",
exhaustive: true,
effects: {
mutatesArtifact: false,
launchesProcess: true,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
},
limits: { maxResults: null, maxPayloadBytes: 1_000_000, timeoutMs: 5_000 },
limitations: [],
});