271 lines
8.4 KiB
TypeScript
271 lines
8.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { INVESTIGATION_EXAMPLES } from "../src/contracts/investigationExamples.js";
|
|
import { createEvidence, type Evidence } from "../src/domain/evidence.js";
|
|
import type { JsonValue } from "../src/domain/jsonValue.js";
|
|
import {
|
|
correlateStaticAndRuntime,
|
|
staticRuntimeCorrelationResultSchema,
|
|
} from "../src/domain/staticRuntimeCorrelation.js";
|
|
|
|
const staticComparison =
|
|
INVESTIGATION_EXAMPLES.find_changed_behavior.comparisons[0];
|
|
if (staticComparison === undefined)
|
|
throw new Error("Missing static comparison fixture");
|
|
|
|
const capture = (digit: string): Evidence =>
|
|
createEvidence(
|
|
undefined,
|
|
{ id: "fixture", name: "Fixture", version: "1" },
|
|
{
|
|
predicateType: "rea.process-capture/v4",
|
|
operation: "capture_process_scenario",
|
|
parameters: {},
|
|
result: { digit },
|
|
authority: "controlled-replay",
|
|
environment: {
|
|
id: `fixture-${digit}`,
|
|
platform: "test",
|
|
architecture: "test",
|
|
isolation: "process",
|
|
},
|
|
},
|
|
);
|
|
|
|
const leftCapture = capture("1");
|
|
const rightCapture = capture("2");
|
|
|
|
const processComparison = (
|
|
terminal: "unchanged" | "changed" | "unknown" | "truncated",
|
|
): Evidence => {
|
|
const result = {
|
|
status: terminal,
|
|
terminal,
|
|
interaction: terminal === "truncated" ? "truncated" : "unchanged",
|
|
exit: terminal === "truncated" ? "truncated" : "unchanged",
|
|
filesystem: terminal === "truncated" ? "truncated" : "unchanged",
|
|
protocol: terminal === "truncated" ? "truncated" : "unchanged",
|
|
process: terminal === "truncated" ? "truncated" : "unchanged",
|
|
shim: terminal === "truncated" ? "truncated" : "unchanged",
|
|
first_divergence:
|
|
terminal === "changed"
|
|
? {
|
|
status: "found",
|
|
dimension: "terminal",
|
|
index: 0,
|
|
left_at_ms: 0,
|
|
right_at_ms: 0,
|
|
left: "left",
|
|
right: "right",
|
|
}
|
|
: terminal === "truncated" || terminal === "unknown"
|
|
? { status: "unknown", reason: "Incomplete fixture." }
|
|
: { status: "none" },
|
|
limitations: terminal === "unknown" ? ["Terminal unavailable."] : [],
|
|
} satisfies JsonValue;
|
|
return 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: leftCapture.evidence_id,
|
|
right_evidence_id: rightCapture.evidence_id,
|
|
},
|
|
result,
|
|
confidence: "derived",
|
|
authority: "analyst-inference",
|
|
evidenceLinks: [leftCapture.evidence_id, rightCapture.evidence_id],
|
|
},
|
|
);
|
|
};
|
|
|
|
const mapping = (
|
|
runtime: Evidence,
|
|
expected: "cochanged" | "static_only" | "runtime_only" | "both_unchanged",
|
|
dimension: "terminal" | "interaction" | "exit" | "shim" = "terminal",
|
|
) => ({
|
|
static_comparisons: [staticComparison],
|
|
runtime_comparisons: [runtime],
|
|
mappings: [
|
|
{
|
|
static: {
|
|
comparison_evidence_id: staticComparison.evidence_id,
|
|
selector: {
|
|
kind: "function" as const,
|
|
dimension: "pseudocode" as const,
|
|
},
|
|
},
|
|
runtime: {
|
|
comparison_evidence_id: runtime.evidence_id,
|
|
dimension,
|
|
},
|
|
side_alignment: "left_to_left" as const,
|
|
hypothesis: {
|
|
statement: "The static edit is associated with terminal behavior.",
|
|
expected_pattern: expected,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
describe("static/runtime correlation", () => {
|
|
it("accepts interaction and shim runtime dimensions", () => {
|
|
const runtime = processComparison("unchanged");
|
|
expect(
|
|
correlateStaticAndRuntime(mapping(runtime, "static_only", "interaction")),
|
|
).toMatchObject({ status: "correlated" });
|
|
expect(
|
|
correlateStaticAndRuntime(mapping(runtime, "static_only", "shim")),
|
|
).toMatchObject({ status: "correlated" });
|
|
});
|
|
|
|
it("keeps consistent cochange as a hypothesis with complete citations", () => {
|
|
const runtime = processComparison("changed");
|
|
const result = correlateStaticAndRuntime(mapping(runtime, "cochanged"));
|
|
expect(staticRuntimeCorrelationResultSchema.parse(result)).toMatchObject({
|
|
status: "correlated",
|
|
summary: { hypotheses: 1, contradictions: 0, unresolved: 0 },
|
|
correlations: {
|
|
total: 1,
|
|
items: [
|
|
{
|
|
observed_pattern: "cochanged",
|
|
classification: "hypothesis",
|
|
side_alignment: "left_to_left",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(result.correlations.items[0]?.evidence_links).toEqual(
|
|
expect.arrayContaining([
|
|
staticComparison.evidence_id,
|
|
runtime.evidence_id,
|
|
leftCapture.evidence_id,
|
|
rightCapture.evidence_id,
|
|
]),
|
|
);
|
|
expect(result.limitations.join(" ")).toMatch(
|
|
/does not establish causality/u,
|
|
);
|
|
});
|
|
|
|
it("classifies an explicit expectation mismatch as contradiction", () => {
|
|
const result = correlateStaticAndRuntime(
|
|
mapping(processComparison("unchanged"), "cochanged"),
|
|
);
|
|
expect(result).toMatchObject({
|
|
status: "contradicted",
|
|
summary: { hypotheses: 0, contradictions: 1, unresolved: 0 },
|
|
correlations: {
|
|
items: [
|
|
{ observed_pattern: "static_only", classification: "contradiction" },
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("lets unknown and truncation dominate rather than imply equivalence", () => {
|
|
const unknown = correlateStaticAndRuntime(
|
|
mapping(processComparison("unknown"), "cochanged"),
|
|
);
|
|
const truncated = correlateStaticAndRuntime(
|
|
mapping(processComparison("truncated"), "cochanged"),
|
|
);
|
|
expect(unknown).toMatchObject({
|
|
status: "unknown",
|
|
correlations: { items: [{ classification: "unresolved_branch" }] },
|
|
});
|
|
expect(truncated).toMatchObject({
|
|
status: "truncated",
|
|
correlations: { items: [{ observed_pattern: "truncated" }] },
|
|
});
|
|
});
|
|
|
|
it("is order-stable, paginated, and rejects duplicates and absent selectors", () => {
|
|
const runtime = processComparison("changed");
|
|
const base = mapping(runtime, "cochanged");
|
|
const second = {
|
|
...base.mappings[0],
|
|
runtime: { ...base.mappings[0]!.runtime, dimension: "exit" as const },
|
|
hypothesis: {
|
|
statement: "The edit is associated with exit behavior.",
|
|
expected_pattern: "static_only" as const,
|
|
},
|
|
};
|
|
const forward = correlateStaticAndRuntime({
|
|
...base,
|
|
mappings: [base.mappings[0], second],
|
|
offset: 0,
|
|
limit: 1,
|
|
});
|
|
const reversed = correlateStaticAndRuntime({
|
|
...base,
|
|
mappings: [second, base.mappings[0]],
|
|
offset: 0,
|
|
limit: 1,
|
|
});
|
|
expect(reversed).toEqual(forward);
|
|
expect(forward.correlations).toMatchObject({ total: 2, next_offset: 1 });
|
|
expect(() =>
|
|
correlateStaticAndRuntime({
|
|
...base,
|
|
mappings: [base.mappings[0], base.mappings[0]],
|
|
}),
|
|
).toThrow(/duplicate mappings/u);
|
|
expect(() =>
|
|
correlateStaticAndRuntime({
|
|
...base,
|
|
mappings: [
|
|
{
|
|
...base.mappings[0],
|
|
static: {
|
|
...base.mappings[0]!.static,
|
|
selector: { kind: "function", dimension: "assembly" },
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
).not.toThrow();
|
|
expect(() =>
|
|
correlateStaticAndRuntime({
|
|
...base,
|
|
mappings: [
|
|
{
|
|
...base.mappings[0],
|
|
static: {
|
|
comparison_evidence_id: runtime.evidence_id,
|
|
selector: { kind: "function", dimension: "pseudocode" },
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
).toThrow(/absent static comparison/u);
|
|
});
|
|
|
|
it("rejects comparison citations that disagree with source parameters", () => {
|
|
const runtime = processComparison("changed");
|
|
const forged = createEvidence(undefined, runtime.provider, {
|
|
predicateType: runtime.predicate_type,
|
|
operation: runtime.operation,
|
|
parameters: {
|
|
left_evidence_id: leftCapture.evidence_id,
|
|
right_evidence_id: staticComparison.evidence_id,
|
|
},
|
|
result: runtime.normalized_result,
|
|
confidence: runtime.confidence,
|
|
authority: runtime.authority,
|
|
evidenceLinks: runtime.evidence_links,
|
|
});
|
|
const input = mapping(forged, "cochanged");
|
|
expect(() => correlateStaticAndRuntime(input)).toThrow(
|
|
/closure disagrees with its source parameters/u,
|
|
);
|
|
});
|
|
});
|