rea/tests/analysisSnapshot.test.ts

211 lines
6.7 KiB
TypeScript

import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { createAnalysisExecution } from "../src/application/AnalysisProvider.js";
import { AnalysisSnapshotCache } from "../src/application/AnalysisSnapshotCache.js";
import {
readAnalysisSnapshot,
writeAnalysisSnapshot,
} from "../src/application/AnalysisSnapshotFiles.js";
import { createAnalysisProfile } from "../src/domain/analysisProfile.js";
import {
createAnalysisSnapshotEntry,
parseAnalysisSnapshot,
snapshotBinding,
snapshotEvidenceForQuery,
snapshotTarget,
type AnalysisSnapshot,
} from "../src/domain/analysisSnapshot.js";
import type { BinaryTarget } from "../src/domain/binaryTarget.js";
import { createEvidence } from "../src/domain/evidence.js";
import { createEvidenceBundle } from "../src/domain/evidenceBundle.js";
let directory: string | undefined;
const TARGET: BinaryTarget = {
path: "/tmp/app",
sha256: "a".repeat(64),
kind: "executable",
format: "mach-o",
architecture: "arm64",
availableArchitectures: ["arm64"],
};
const PROVIDER = { id: "fixture", name: "Fixture", version: "1" } as const;
const PROFILE = createAnalysisProfile(PROVIDER, 1, {
loader: "mach-o-arm64",
});
afterEach(async () => {
if (directory !== undefined)
await rm(directory, { recursive: true, force: true });
directory = undefined;
});
describe("analysis snapshots", () => {
it("returns detached cache data only from the exact provider/profile partition", () => {
const cache = new AnalysisSnapshotCache();
const execution = createAnalysisExecution("main", PROVIDER, {
analysisProfile: PROFILE,
});
cache.record({
target: TARGET,
profile: PROFILE,
operation: "address_name",
parameters: { address: "0x1000", document: "fixture" },
execution,
});
const exported = cache.export(TARGET, PROFILE, createEvidenceBundle([]));
expect(exported.ok).toBe(true);
if (!exported.ok) return;
const entry = exported.value.entries[0];
if (entry !== undefined) {
Reflect.set(entry.execution, "provider", {
id: "forged",
name: "Forged",
version: "9",
});
entry.execution.limitations.push("forged");
}
expect(
cache.lookup(TARGET, PROFILE, "address_name", {
address: "0x1000",
document: "fixture",
}),
).toMatchObject({
provider: PROVIDER,
analysisProfile: PROFILE,
limitations: [expect.stringContaining("local REA analysis snapshot")],
});
const changedProfile = createAnalysisProfile(PROVIDER, 1, {
loader: "configured-override",
});
expect(
cache.lookup(TARGET, changedProfile, "address_name", {
address: "0x1000",
document: "fixture",
}),
).toBeUndefined();
const changedProviderProfile = createAnalysisProfile(
{ id: "other", name: "Other", version: "1" },
1,
{ loader: "mach-o-arm64" },
);
expect(
cache.lookup(TARGET, changedProviderProfile, "address_name", {
address: "0x1000",
document: "fixture",
}),
).toBeUndefined();
});
it("finds only Evidence committed to the exact binding and evidence profile", () => {
const evidence = createEvidence(TARGET, PROVIDER, {
operation: "analyze_function",
parameters: { procedure: "main" },
result: { summary: "cached" },
analysisProfile: PROFILE,
});
const legacy = createEvidence(TARGET, PROVIDER, {
operation: "legacy_query",
parameters: {},
result: { summary: "legacy" },
});
const snapshot: AnalysisSnapshot = {
snapshot_version: 2,
target: snapshotTarget(TARGET),
binding: snapshotBinding(PROFILE),
entries: [],
evidence_bundle: createEvidenceBundle([evidence, legacy]),
};
expect(
snapshotEvidenceForQuery(snapshot, {
target: TARGET,
bindingProfile: PROFILE,
operation: "analyze_function",
parameters: { procedure: "main" },
provider: PROVIDER,
evidenceProfile: PROFILE,
}),
).toEqual(evidence);
expect(
snapshotEvidenceForQuery(snapshot, {
target: TARGET,
bindingProfile: PROFILE,
operation: "legacy_query",
parameters: {},
provider: PROVIDER,
evidenceProfile: PROFILE,
}),
).toBeUndefined();
});
it("writes canonical private v2 JSON and rejects changed query identity", async () => {
directory = await mkdtemp(join(tmpdir(), "rea-snapshot-"));
const path = join(directory, "analysis.json");
const target = snapshotTarget(TARGET);
const binding = snapshotBinding(PROFILE);
const entry = createAnalysisSnapshotEntry({
target,
binding,
operation: "address_name",
parameters: { address: "0x1000" },
execution: createAnalysisExecution("main", PROVIDER, {
analysisProfile: PROFILE,
}),
});
const snapshot: AnalysisSnapshot = {
snapshot_version: 2,
target,
binding,
entries: [entry],
evidence_bundle: createEvidenceBundle([]),
};
const policy = {
roots: [directory],
maxBytes: 1024 * 1024,
maxDepth: 64,
maxStringLength: 1024,
maxNodes: 10_000,
};
const written = await writeAnalysisSnapshot(snapshot, path, false, policy);
expect(written.ok).toBe(true);
expect((await readFile(path, "utf8")).endsWith("\n")).toBe(true);
expect((await stat(path)).mode & 0o777).toBe(0o600);
expect((await readAnalysisSnapshot(path, policy)).ok).toBe(true);
const altered: unknown = JSON.parse(await readFile(path, "utf8"));
if (
typeof altered !== "object" ||
altered === null ||
!("entries" in altered) ||
!Array.isArray(altered.entries)
)
throw new TypeError("fixture snapshot is malformed");
const first: unknown = altered.entries[0];
if (
typeof first !== "object" ||
first === null ||
!("parameters" in first) ||
typeof first.parameters !== "object" ||
first.parameters === null
)
throw new TypeError("fixture entry is malformed");
Reflect.set(first.parameters, "address", "0x2000");
await writeFile(path, JSON.stringify(altered));
const loaded = await readAnalysisSnapshot(path, policy);
expect(loaded.ok).toBe(false);
if (!loaded.ok) expect(loaded.error._tag).toBe("EvidenceIntegrityError");
expect(() => parseAnalysisSnapshot(altered)).toThrow(/identifier/u);
});
it("rejects snapshot v1 with explicit recapture guidance", () => {
expect(() => parseAnalysisSnapshot({ snapshot_version: 1 })).toThrow(
/v1.*recapture.*v2/iu,
);
});
});