import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { createTestBinarySession } from "../../fixtures/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"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; const target: BinaryTarget = { path: "/fixture", kind: "database", format: "analysis-database", sha256: "0".repeat(64), }; 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 = createTestBinarySession( 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 () => { const directory = await createTestTempDirectory("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 = createTestBinarySession( 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, 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: { runId: context.runId, launcherPid, launcherParentPid: 1, processGroupId: launcherPid, descendants: [], }, }, }, ], close: () => Promise.resolve(), }; }, }; }; const provider = ( id: string, operation: Exclude, 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, ): CapabilityDescriptor => ({ provider: providerIdentity, operation, available: true, reason: null, effects: { mutatesArtifact: false, launchesProcess: true, mayShowUi: false, mayAccessNetwork: false, mayWriteFilesystem: false, changesPermissions: false, requiresRoot: false, }, limitations: [], });