403 lines
12 KiB
TypeScript
403 lines
12 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { createEvidence, type Evidence } from "../src/domain/evidence.js";
|
|
import { createEvidenceBundle } from "../src/domain/evidenceBundle.js";
|
|
import {
|
|
reconstructionVerificationResultSchema,
|
|
verifyReconstruction,
|
|
} from "../src/domain/reconstructionVerification.js";
|
|
import type { JsonValue } from "../src/domain/jsonValue.js";
|
|
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../src/contracts/processCaptureExample.js";
|
|
import { ARTIFACT_COMPARISON_EXAMPLE } from "../src/contracts/artifactComparisonExample.js";
|
|
import { createResidualUnknown } from "../src/domain/residualUnknown.js";
|
|
|
|
const environment = {
|
|
id: "fixture-linux",
|
|
platform: "linux",
|
|
architecture: "x86_64",
|
|
isolation: "container" as const,
|
|
};
|
|
|
|
const source = (
|
|
digit: string,
|
|
authority: "controlled-replay" | "shipped-artifact",
|
|
confidence: "observed" | "derived" = "observed",
|
|
): Evidence =>
|
|
createEvidence(
|
|
{
|
|
path: `/tmp/source-${digit}`,
|
|
sha256: digit.repeat(64),
|
|
format: "file",
|
|
},
|
|
{ id: "fixture", name: "Fixture", version: "1" },
|
|
{
|
|
predicateType:
|
|
authority === "controlled-replay"
|
|
? "rea.process-capture/v4"
|
|
: "rea.analysis/v2",
|
|
operation:
|
|
authority === "controlled-replay"
|
|
? "capture_process_scenario"
|
|
: "inventory_artifact",
|
|
parameters: {},
|
|
result:
|
|
authority === "controlled-replay"
|
|
? EMPTY_PROCESS_CAPTURE_EXAMPLE
|
|
: { digit },
|
|
confidence,
|
|
authority,
|
|
environment: authority === "controlled-replay" ? environment : null,
|
|
},
|
|
);
|
|
|
|
const processResult = (
|
|
terminal: "unchanged" | "changed" | "unknown" = "unchanged",
|
|
): JsonValue => ({
|
|
status: terminal === "changed" ? "changed" : terminal,
|
|
terminal,
|
|
interaction: "unchanged",
|
|
exit: "unchanged",
|
|
filesystem: "unchanged",
|
|
protocol: "unchanged",
|
|
process: "unchanged",
|
|
shim: "unchanged",
|
|
first_divergence:
|
|
terminal === "changed"
|
|
? {
|
|
status: "found",
|
|
dimension: "terminal",
|
|
index: 0,
|
|
left_at_ms: 0,
|
|
right_at_ms: 0,
|
|
left: "left",
|
|
right: "right",
|
|
}
|
|
: terminal === "unknown"
|
|
? { status: "unknown", reason: "Incomplete fixture." }
|
|
: { status: "none" },
|
|
limitations: [],
|
|
});
|
|
|
|
const processComparison = (
|
|
left: Evidence,
|
|
right: Evidence,
|
|
result: JsonValue,
|
|
extraLinks: readonly string[] = [],
|
|
): Evidence =>
|
|
createEvidence(
|
|
undefined,
|
|
{
|
|
id: "rea-process",
|
|
name: "REA deterministic process harness",
|
|
version: "3",
|
|
},
|
|
{
|
|
predicateType: "rea.process-comparison/v3",
|
|
operation: "compare_process_captures",
|
|
parameters: {
|
|
left_evidence_id: left.evidence_id,
|
|
right_evidence_id: right.evidence_id,
|
|
},
|
|
result,
|
|
confidence: "derived",
|
|
authority: "analyst-inference",
|
|
evidenceLinks: [left.evidence_id, right.evidence_id, ...extraLinks],
|
|
},
|
|
);
|
|
|
|
const behavioralSpec = (comparison: Evidence) => ({
|
|
schema_version: 1 as const,
|
|
name: "CLI compatibility",
|
|
claims: [
|
|
{
|
|
kind: "behavioral" as const,
|
|
claim_id: "terminal-output",
|
|
title: "Terminal output remains equal",
|
|
comparison_evidence_id: comparison.evidence_id,
|
|
dimension: "terminal" as const,
|
|
},
|
|
],
|
|
});
|
|
|
|
const artifactResult = (partial = false): JsonValue => ({
|
|
status: "unchanged",
|
|
left_manifest_id: `agm_${"3".repeat(64)}`,
|
|
right_manifest_id: `agm_${"4".repeat(64)}`,
|
|
summary: { unchanged: 1, added: 0, removed: 0, changed: 0, unknown: 0 },
|
|
changes: {
|
|
items: [],
|
|
offset: 0,
|
|
limit: 100,
|
|
total: partial ? 1 : 0,
|
|
next_offset: partial ? 1 : null,
|
|
},
|
|
limitations: [],
|
|
});
|
|
|
|
const artifactComparison = (
|
|
left: Evidence,
|
|
right: Evidence,
|
|
partial = false,
|
|
): Evidence =>
|
|
createEvidence(
|
|
undefined,
|
|
{
|
|
id: "rea-artifact-comparison",
|
|
name: "REA artifact comparison",
|
|
version: "1",
|
|
},
|
|
{
|
|
predicateType: "rea.artifact-comparison/v1",
|
|
operation: "compare_artifacts",
|
|
parameters: {
|
|
left_evidence_ids: [left.evidence_id],
|
|
right_evidence_ids: [right.evidence_id],
|
|
},
|
|
result: artifactResult(partial),
|
|
confidence: "derived",
|
|
authority: "analyst-inference",
|
|
evidenceLinks: [left.evidence_id, right.evidence_id],
|
|
},
|
|
);
|
|
|
|
describe("reconstruction verification", () => {
|
|
it("accepts interaction and shim behavioral claim dimensions", () => {
|
|
const left = source("1", "controlled-replay");
|
|
const right = source("2", "controlled-replay");
|
|
const result = {
|
|
status: "changed",
|
|
terminal: "unchanged",
|
|
interaction: "changed",
|
|
exit: "unchanged",
|
|
filesystem: "unchanged",
|
|
protocol: "unchanged",
|
|
process: "unchanged",
|
|
shim: "unchanged",
|
|
first_divergence: {
|
|
status: "found",
|
|
dimension: "interaction",
|
|
index: 0,
|
|
left_at_ms: 0,
|
|
right_at_ms: 0,
|
|
left: "left",
|
|
right: "right",
|
|
},
|
|
limitations: [],
|
|
} satisfies JsonValue;
|
|
const comparison = processComparison(left, right, result);
|
|
|
|
expect(
|
|
verifyReconstruction(
|
|
{
|
|
schema_version: 1,
|
|
name: "Interaction reconstruction",
|
|
claims: [
|
|
{
|
|
kind: "behavioral",
|
|
claim_id: "interaction",
|
|
title: "Interaction remains equivalent",
|
|
comparison_evidence_id: comparison.evidence_id,
|
|
dimension: "interaction",
|
|
},
|
|
{
|
|
kind: "behavioral",
|
|
claim_id: "shim",
|
|
title: "Shim remains equivalent",
|
|
comparison_evidence_id: comparison.evidence_id,
|
|
dimension: "shim",
|
|
},
|
|
],
|
|
},
|
|
createEvidenceBundle([comparison, right, left]),
|
|
0,
|
|
100,
|
|
),
|
|
).toMatchObject({
|
|
summary: { total: 2, passed: 1, failed: 1, unknown: 0 },
|
|
});
|
|
});
|
|
|
|
it("passes a complete authoritative claim with two-sided citations", () => {
|
|
const left = source("1", "controlled-replay");
|
|
const right = source("2", "controlled-replay");
|
|
const comparison = processComparison(left, right, processResult());
|
|
const result = verifyReconstruction(
|
|
behavioralSpec(comparison),
|
|
createEvidenceBundle([comparison, right, left]),
|
|
0,
|
|
100,
|
|
);
|
|
expect(reconstructionVerificationResultSchema.parse(result)).toMatchObject({
|
|
status: "pass",
|
|
summary: { total: 1, passed: 1, failed: 0, unknown: 0 },
|
|
claims: { total: 1, next_offset: null },
|
|
});
|
|
expect(result.claims.items[0]?.evidence_links).toEqual(
|
|
expect.arrayContaining([
|
|
comparison.evidence_id,
|
|
left.evidence_id,
|
|
right.evidence_id,
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("fails an authoritative observed disagreement", () => {
|
|
const left = source("1", "controlled-replay");
|
|
const right = source("2", "controlled-replay");
|
|
const comparison = processComparison(left, right, processResult("changed"));
|
|
const result = verifyReconstruction(
|
|
behavioralSpec(comparison),
|
|
createEvidenceBundle([left, right, comparison]),
|
|
0,
|
|
100,
|
|
);
|
|
expect(result).toMatchObject({
|
|
status: "fail",
|
|
summary: { failed: 1 },
|
|
claims: { items: [{ status: "fail", observed_status: "changed" }] },
|
|
});
|
|
});
|
|
|
|
it("keeps insufficient authority and partial structural evidence unknown", () => {
|
|
const weakLeft = source("1", "controlled-replay", "derived");
|
|
const replayRight = source("2", "controlled-replay");
|
|
const runtime = processComparison(weakLeft, replayRight, processResult());
|
|
expect(
|
|
verifyReconstruction(
|
|
behavioralSpec(runtime),
|
|
createEvidenceBundle([weakLeft, replayRight, runtime]),
|
|
0,
|
|
100,
|
|
).status,
|
|
).toBe("unknown");
|
|
|
|
const left = ARTIFACT_COMPARISON_EXAMPLE.left;
|
|
const right = ARTIFACT_COMPARISON_EXAMPLE.right;
|
|
const comparison = artifactComparison(left, right, true);
|
|
const result = verifyReconstruction(
|
|
{
|
|
schema_version: 1,
|
|
name: "Artifact structure",
|
|
claims: [
|
|
{
|
|
kind: "structural-artifact",
|
|
claim_id: "artifact-graph",
|
|
title: "Artifact graph remains equal",
|
|
comparison_evidence_id: comparison.evidence_id,
|
|
dimension: "overall",
|
|
},
|
|
],
|
|
},
|
|
createEvidenceBundle([left, comparison, right]),
|
|
0,
|
|
100,
|
|
);
|
|
expect(result.status).toBe("unknown");
|
|
expect(result.recommended_probes[0]?.operation).toBe("inventory_artifact");
|
|
});
|
|
|
|
it("rejects dangling or extra comparison closure", () => {
|
|
const left = source("1", "controlled-replay");
|
|
const right = source("2", "controlled-replay");
|
|
const extra = source("3", "controlled-replay");
|
|
const comparison = processComparison(left, right, processResult(), [
|
|
extra.evidence_id,
|
|
]);
|
|
expect(() =>
|
|
verifyReconstruction(
|
|
behavioralSpec(comparison),
|
|
createEvidenceBundle([left, right, extra, comparison]),
|
|
0,
|
|
100,
|
|
),
|
|
).toThrow(/closure disagrees/u);
|
|
});
|
|
|
|
it("pages claims deterministically and gives specification a stable digest", () => {
|
|
const left = source("1", "controlled-replay");
|
|
const right = source("2", "controlled-replay");
|
|
const comparison = processComparison(left, right, processResult());
|
|
const claims = [
|
|
{
|
|
kind: "behavioral" as const,
|
|
claim_id: "z-exit",
|
|
title: "Exit remains equal",
|
|
comparison_evidence_id: comparison.evidence_id,
|
|
dimension: "exit" as const,
|
|
},
|
|
behavioralSpec(comparison).claims[0],
|
|
];
|
|
const bundle = createEvidenceBundle([right, comparison, left]);
|
|
const first = verifyReconstruction(
|
|
{ schema_version: 1, name: "Compatibility", claims },
|
|
bundle,
|
|
0,
|
|
1,
|
|
);
|
|
const repeated = verifyReconstruction(
|
|
{
|
|
name: "Compatibility",
|
|
claims: [...claims].reverse(),
|
|
schema_version: 1,
|
|
},
|
|
bundle,
|
|
0,
|
|
1,
|
|
);
|
|
expect(first).toEqual(repeated);
|
|
expect(first.claims.items[0]?.claim_id).toBe("terminal-output");
|
|
expect(first.claims.next_offset).toBe(1);
|
|
});
|
|
|
|
it("keeps all active unknowns gating while bounding their projection", () => {
|
|
const left = source("1", "controlled-replay");
|
|
const right = source("2", "controlled-replay");
|
|
const comparison = processComparison(left, right, processResult());
|
|
const mutations = Array.from({ length: 101 }, (_, index) =>
|
|
createEvidence(
|
|
undefined,
|
|
{ id: "fixture", name: "Fixture", version: "1" },
|
|
{
|
|
predicateType: "rea.residual-unknown-mutation/v1",
|
|
operation: "record_unknown",
|
|
parameters: { index },
|
|
result: { action: "record" },
|
|
confidence: "derived",
|
|
authority: "analyst-inference",
|
|
evidenceLinks: [comparison.evidence_id],
|
|
},
|
|
),
|
|
);
|
|
const unknowns = mutations.map((mutation, index) =>
|
|
createResidualUnknown(
|
|
{
|
|
approved: true,
|
|
question: `Unresolved replay ${index}`,
|
|
severity: "high",
|
|
domain: "reconstruction-verification",
|
|
supporting_evidence_ids: [comparison.evidence_id],
|
|
contradicting_evidence_ids: [],
|
|
required_authority: "controlled-replay",
|
|
required_confidence: "observed",
|
|
required_environment: null,
|
|
recommended_probes: [],
|
|
relationships: [],
|
|
},
|
|
mutation.evidence_id,
|
|
null,
|
|
),
|
|
);
|
|
const result = verifyReconstruction(
|
|
behavioralSpec(comparison),
|
|
createEvidenceBundle([left, right, comparison, ...mutations], unknowns),
|
|
0,
|
|
100,
|
|
);
|
|
expect(result.status).toBe("unknown");
|
|
expect(result.claims.items[0]?.unknown_ids).toHaveLength(100);
|
|
expect(result.claims.items[0]?.limitations).toContain(
|
|
"1 additional active residual unknowns affect this claim but are omitted by the 100-item display limit.",
|
|
);
|
|
});
|
|
});
|