Files
rea/tests/evidence.test.ts
T
N0zoM1z0 21da71ec6d refactor(domain): remove provider-specific target and snapshot state
Move Hopper loader derivation and compatibility projection into the Hopper adapter so BinaryTarget carries only provider-neutral identity and format metadata.

Introduce bounded canonical analysis-profile commitments, exact provider/profile snapshot v2 bindings, legacy snapshot rejection guidance, and backward-compatible profiled Evidence v2 records. Partition cache and replay behavior by target, concrete provider build, and profile while preserving custom Hopper loader overrides.

Propagate profiles consistently through CLI, MCP, composed workflows, and session lifecycle; preserve the 78-tool contract and legacy loaderArgs response projection. Add migration documentation, generated API updates, property regressions, package verification, and real Hopper coverage.
2026-07-15 20:31:56 +09:00

388 lines
13 KiB
TypeScript

import { fc, it } from "@fast-check/vitest";
import { describe, expect } from "vitest";
import { EvidenceLedger } from "../src/application/EvidenceLedger.js";
import type { BinaryTarget } from "../src/domain/binaryTarget.js";
import {
createEvidence,
evidenceSchema,
parseEvidence,
} from "../src/domain/evidence.js";
import { createEvidenceBundle } from "../src/domain/evidenceBundle.js";
import { recordUnknownInputSchema } from "../src/domain/residualUnknown.js";
import { createAnalysisProfile } from "../src/domain/analysisProfile.js";
const TARGET: BinaryTarget = {
path: "/tmp/fixture",
sha256: "a".repeat(64),
kind: "executable",
format: "mach-o",
architecture: "arm64",
availableArchitectures: ["arm64"],
};
const PROVIDER = { id: "fixture", name: "Fixture provider", version: "1" };
const PROFILE = createAnalysisProfile(PROVIDER, 1, { loader: "default" });
describe("analysis evidence", () => {
it("keeps the public object schema extensible for typed consumers", () => {
expect(evidenceSchema.shape.analysis_profile).toBeDefined();
expect(evidenceSchema.shape.provider).toBeDefined();
});
it("derives identity after normalizing prototype-named parameter keys", () => {
const evidence = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: Object.fromEntries([["__proto__", false]]),
result: true,
});
expect(parseEvidence(evidence)).toEqual(evidence);
});
it.prop([
fc.dictionary(
fc.string({ minLength: 1, maxLength: 12 }),
fc.oneof(fc.string(), fc.integer(), fc.boolean(), fc.constant(null)),
),
])("canonicalizes parameter key order", (parameters) => {
const reversed = Object.fromEntries(Object.entries(parameters).reverse());
const first = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters,
result: true,
});
const second = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: reversed,
result: true,
});
expect(second.evidence_id).toBe(first.evidence_id);
});
it.prop([fc.string({ minLength: 1 }), fc.string({ minLength: 1 })])(
"keeps identity path-independent and provider-sensitive",
(firstPath, secondPath) => {
const first = createEvidence(
{ ...TARGET, path: `/first/${firstPath}/artifact` },
PROVIDER,
{ operation: "health", parameters: {}, result: true },
);
const moved = createEvidence(
{ ...TARGET, path: `/second/${secondPath}/artifact` },
PROVIDER,
{ operation: "health", parameters: {}, result: true },
);
const changedProvider = createEvidence(
{ ...TARGET, path: `/second/${secondPath}/artifact` },
{ ...PROVIDER, id: `${PROVIDER.id}-other` },
{ operation: "health", parameters: {}, result: true },
);
expect(moved.evidence_id).toBe(first.evidence_id);
expect(changedProvider.evidence_id).not.toBe(first.evidence_id);
},
);
it("creates provider-neutral, deterministic Evidence v2", () => {
const observation = {
operation: "procedure_info",
parameters: { document: null, procedure: "0x1000" },
result: { name: "main" },
rawResult: { token: "<redacted:token>" },
} as const;
const evidence = createEvidence(TARGET, PROVIDER, observation);
expect(evidenceSchema.parse(evidence)).toEqual(evidence);
expect(parseEvidence(evidence)).toEqual(evidence);
expect(evidence).toMatchObject({
schema_version: 2,
provider: PROVIDER,
subject: { digest: { sha256: "a".repeat(64) } },
confidence: "observed",
authority: "shipped-artifact",
raw_result: { token: "<redacted:token>" },
normalized_result: { name: "main" },
});
expect(evidence.evidence_id).toMatch(/^ev_[a-f0-9]{64}$/u);
expect(createEvidence(TARGET, PROVIDER, observation)).toEqual(evidence);
});
it("preserves legacy IDs while committing profiles on new observations", () => {
const legacy = createEvidence(TARGET, PROVIDER, {
operation: "procedure_info",
parameters: { procedure: "main" },
result: { name: "main" },
});
expect("analysis_profile" in legacy).toBe(false);
expect(parseEvidence(legacy)).toEqual(legacy);
const profiled = createEvidence(TARGET, PROVIDER, {
operation: "procedure_info",
parameters: { procedure: "main" },
result: { name: "main" },
analysisProfile: PROFILE,
});
expect(profiled).toMatchObject({ analysis_profile: PROFILE });
expect(profiled.evidence_id).not.toBe(legacy.evidence_id);
expect(() =>
createEvidence(
TARGET,
{ ...PROVIDER, id: "other" },
{
operation: "procedure_info",
parameters: {},
result: null,
analysisProfile: PROFILE,
},
),
).toThrow(/profile provider/u);
});
it("excludes local paths but includes redacted raw results in identity", () => {
const observation = {
operation: "health",
parameters: {},
result: true,
rawResult: { pid: 100 },
} as const;
const first = createEvidence(TARGET, PROVIDER, observation);
const moved = createEvidence(
{ ...TARGET, path: "/other/renamed-fixture" },
PROVIDER,
observation,
);
expect(moved.evidence_id).toBe(first.evidence_id);
const changedRaw = createEvidence(TARGET, PROVIDER, {
...observation,
rawResult: { pid: 200 },
});
expect(changedRaw.evidence_id).not.toBe(first.evidence_id);
});
it("rejects semantic tampering", () => {
const evidence = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: {},
result: true,
});
expect(() =>
parseEvidence({ ...evidence, normalized_result: false }),
).toThrow("semantic identifier");
});
it("rejects Evidence v1 with actionable upgrade guidance", () => {
expect(() => parseEvidence({ schema_version: 1 })).toThrow(
"Evidence v1 is not accepted. Produce Evidence v2.",
);
});
it("deduplicates and atomically imports bounded bundles", () => {
const evidence = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: {},
result: true,
});
const ledger = new EvidenceLedger({ maxRecords: 1, maxBytes: 1_000_000 });
expect(ledger.record(evidence)).toEqual({ ok: true, value: "added" });
expect(ledger.record(evidence)).toEqual({ ok: true, value: "duplicate" });
const relocated = createEvidence(
{ ...TARGET, path: "/relocated/different-name" },
PROVIDER,
{ operation: "health", parameters: {}, result: true },
);
expect(relocated.evidence_id).toBe(evidence.evidence_id);
expect(ledger.record(relocated)).toEqual({
ok: true,
value: "duplicate",
});
expect(ledger.import(createEvidenceBundle([evidence]))).toEqual({
ok: true,
value: 0,
});
expect(ledger.export().records).toEqual([evidence]);
ledger.clear();
expect(ledger.export().records).toEqual([]);
});
it("treats canonical JSON key order as semantically irrelevant", () => {
const evidence = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: { alpha: 1, beta: 2 },
result: { alpha: 1, beta: 2 },
});
const reordered = evidenceSchema.parse({
...evidence,
parameters: { beta: 2, alpha: 1 },
normalized_result: { beta: 2, alpha: 1 },
});
const ledger = new EvidenceLedger({ maxRecords: 1, maxBytes: 1_000_000 });
expect(ledger.record(evidence)).toEqual({ ok: true, value: "added" });
expect(ledger.record(reordered)).toEqual({
ok: true,
value: "duplicate",
});
});
it("returns typed record and byte limit failures without eviction", () => {
const first = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: {},
result: true,
});
const second = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: { changed: true },
result: true,
});
const recordBound = new EvidenceLedger({
maxRecords: 1,
maxBytes: 1_000_000,
});
expect(recordBound.record(first).ok).toBe(true);
expect(recordBound.record(second)).toMatchObject({
ok: false,
error: { _tag: "EvidenceLimitError", limit: "records", maximum: 1 },
});
expect(recordBound.export().records).toEqual([first]);
const byteBound = new EvidenceLedger({ maxRecords: 10, maxBytes: 1 });
expect(byteBound.record(first)).toMatchObject({
ok: false,
error: { _tag: "EvidenceLimitError", limit: "bytes", maximum: 1 },
});
expect(byteBound.export().records).toEqual([]);
});
it("counts unknown revisions when enforcing direct record limits", () => {
const ledger = new EvidenceLedger({
maxRecords: 2,
maxBytes: 1_000_000,
});
const mutation = createEvidence(undefined, PROVIDER, {
predicateType: "rea.residual-unknown-mutation/v1",
operation: "record_unknown",
parameters: {},
result: { action: "record" },
});
const unknown = recordUnknownInputSchema.parse({
approved: true,
question: "What remains unresolved?",
severity: "high",
domain: "record-limit-test",
supporting_evidence_ids: [],
contradicting_evidence_ids: [],
required_authority: "shipped-artifact",
required_confidence: "observed",
required_environment: null,
recommended_probes: [],
relationships: [],
});
expect(ledger.recordUnknown(unknown, mutation).ok).toBe(true);
const direct = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: {},
result: true,
});
expect(ledger.record(direct)).toMatchObject({
ok: false,
error: { _tag: "EvidenceLimitError", limit: "records", maximum: 2 },
});
expect(ledger.export()).toMatchObject({
records: [mutation],
unknowns: [expect.objectContaining({ domain: "record-limit-test" })],
});
});
it("derives byte-stable manifests independent of record insertion order", () => {
const artifactEvidence = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: {},
result: true,
});
const captureEvidence = createEvidence(
undefined,
{
id: "process",
name: "Process capture",
version: "1",
},
{
predicateType: "rea.process-capture/v1",
operation: "capture_process_scenario",
parameters: {},
result: { exit: 0 },
authority: "controlled-replay",
environment: {
id: "linux-x64",
platform: "linux",
architecture: "x64",
isolation: "process",
},
},
);
const forward = createEvidenceBundle([artifactEvidence, captureEvidence]);
const reverse = createEvidenceBundle([captureEvidence, artifactEvidence]);
expect(JSON.stringify(reverse)).toBe(JSON.stringify(forward));
expect(forward).toMatchObject({
artifacts: [{ digest: { sha256: TARGET.sha256 }, format: "mach-o" }],
providers: [{ id: "fixture" }, { id: "process" }],
environments: [{ id: "linux-x64" }],
scenarios: [{ evidence_id: captureEvidence.evidence_id }],
captures: [{ evidence_id: captureEvidence.evidence_id }],
});
});
it("rejects conflicting duplicate IDs without mutating the ledger", () => {
const ledger = new EvidenceLedger({ maxRecords: 2, maxBytes: 1_000_000 });
const first = createEvidence(TARGET, PROVIDER, {
operation: "health",
parameters: {},
result: true,
rawResult: { pid: 1 },
});
expect(ledger.record(first).ok).toBe(true);
const bundle = createEvidenceBundle([first]);
expect(
ledger.import({
...bundle,
records: [{ ...first, normalized_result: false }],
}),
).toMatchObject({
ok: false,
error: { _tag: "EvidenceIntegrityError" },
});
expect(ledger.export().records).toEqual([first]);
});
it("atomically rejects Evidence plus unknown when the batch exceeds limits", () => {
const ledger = new EvidenceLedger({ maxRecords: 2, maxBytes: 1_000_000 });
const output = createEvidence(undefined, PROVIDER, {
operation: "derived",
parameters: {},
result: { status: "unknown" },
});
const mutation = createEvidence(undefined, PROVIDER, {
predicateType: "rea.residual-unknown-mutation/v1",
operation: "record_unknown",
parameters: {},
result: { action: "record" },
});
const unknown = recordUnknownInputSchema.parse({
approved: true,
question: "What remains unresolved?",
severity: "high",
domain: "atomic-test",
supporting_evidence_ids: [],
contradicting_evidence_ids: [],
required_authority: "shipped-artifact",
required_confidence: "observed",
required_environment: null,
recommended_probes: [],
relationships: [],
});
expect(ledger.recordWithUnknown(output, unknown, mutation)).toMatchObject({
ok: false,
error: { _tag: "EvidenceLimitError" },
});
expect(ledger.export()).toMatchObject({ records: [], unknowns: [] });
});
});