Files
rea/src/domain/javascript/javascriptApplicationVersionComparison.test.ts
T
morluto 30db499db3 fix(javascript): preserve graph facts across omissions and versions
Omit absent inventory facts without erasing explicit unknowns. Compare canonical file occurrences by source path and digest while preserving complete container provenance in observations and Evidence.

Consolidate candidate-capacity fixtures around unresolved alternatives and update inventory coverage and source-occurrence oracles. Preserve resource, identity, relationship uncertainty, cancellation, and transfer regressions.
2026-10-24 06:20:36 +08:00

442 lines
13 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { createEvidence } from "../evidence.js";
import type { JsonValue } from "../jsonValue.js";
import {
createJavaScriptApplicationEdge,
createJavaScriptApplicationGraph,
createJavaScriptApplicationNode,
isValidatedImmutableJavaScriptApplicationGraph,
parseJavaScriptApplicationGraph,
type JavaScriptApplicationGraph,
} from "./javascriptApplicationGraph.js";
import {
artifactEvidence,
completeCoverage,
} from "./javascriptApplicationGraph.fixture.js";
import { compareJavaScriptApplicationVersions } from "./javascriptApplicationVersionComparison.js";
import {
applicationVersionComparisonResultSchema,
ownedApplicationVersionComparisonResultSchema,
} from "./javascriptApplicationVersionComparisonSchemas.js";
const LEFT_SHA = "a".repeat(64);
const RIGHT_SHA = "b".repeat(64);
const buildGraph = (
artifactSha: string,
version: number,
): JavaScriptApplicationGraph => {
const nodes = ["a", "b", "c"].map((name) =>
createJavaScriptApplicationNode({
kind: "javascript-module",
identity: {
strategy: "canonical-path",
stability: "artifact-version",
artifact_sha256: artifactSha,
path: `${name}.js`,
},
observations: [
{
label: `${name}.js`,
properties: {
structural_fingerprint_sha256: name
.charCodeAt(0)
.toString(16)
.padStart(64, "0"),
structural_fingerprint_algorithm: "fixture",
source_sha256: (name === "b" ? version : 0)
.toString(16)
.padStart(64, "0"),
},
evidence: artifactEvidence(artifactSha, `${name}.js`),
},
],
}),
);
const [a, b, c] = nodes;
if (a === undefined || b === undefined || c === undefined)
throw new Error("Missing fixture node");
return createJavaScriptApplicationGraph({
schema: "JavaScriptApplicationGraph",
root_node_ids: [a.node_id],
nodes,
edges: [
createJavaScriptApplicationEdge({
source_node_id: a.node_id,
target_node_id: b.node_id,
relation: "imports",
properties: {},
evidence: artifactEvidence(artifactSha, "a.js"),
}),
createJavaScriptApplicationEdge({
source_node_id: b.node_id,
target_node_id: c.node_id,
relation: "imports",
properties: {},
evidence: artifactEvidence(artifactSha, "b.js"),
}),
],
coverage: completeCoverage,
limitations: [],
});
};
const compare = () =>
compareJavaScriptApplicationVersions({
left: {
evidenceId: `ev_${"1".repeat(64)}`,
rootArtifactSha256: LEFT_SHA,
graph: buildGraph(LEFT_SHA, 1),
},
right: {
evidenceId: `ev_${"2".repeat(64)}`,
rootArtifactSha256: RIGHT_SHA,
graph: buildGraph(RIGHT_SHA, 2),
},
leftNativeEvidence: [],
rightNativeEvidence: [],
});
const unrelatedGraph = (artifactSha: string): JavaScriptApplicationGraph => {
const node = createJavaScriptApplicationNode({
kind: "javascript-chunk",
identity: {
strategy: "canonical-path",
stability: "artifact-version",
artifact_sha256: artifactSha,
path: "unrelated.js",
},
observations: [
{
label: "unrelated.js",
properties: {},
evidence: artifactEvidence(artifactSha, "unrelated.js"),
},
],
});
return createJavaScriptApplicationGraph({
schema: "JavaScriptApplicationGraph",
root_node_ids: [node.node_id],
nodes: [node],
edges: [],
coverage: completeCoverage,
limitations: [],
});
};
const nativeEvidence = (sha256: string | undefined, ordinal: number) =>
createEvidence(
sha256 === undefined
? undefined
: {
path: `/fixture/native-${ordinal}.js`,
sha256,
format: "javascript",
},
{ id: "fixture", name: "Fixture", version: "1" },
{
operation: "analyze_javascript_module",
parameters: { ordinal },
confidence: "observed",
authority: "shipped-artifact",
result: { ordinal },
},
);
const occurrenceGraph = (
rootSha256: string,
occurrence: JsonValue,
): JavaScriptApplicationGraph => {
const sourceSha256 = "c".repeat(64);
const module = createJavaScriptApplicationNode({
kind: "javascript-module",
identity: {
strategy: "artifact-local-key",
stability: "artifact-version",
artifact_sha256: sourceSha256,
namespace: "source-module",
key: `${rootSha256}:main.js`,
},
observations: [
{
label: "main.js",
properties: {
semantic_role: "source-module",
source_sha256: sourceSha256,
file_occurrence: occurrence,
},
evidence: artifactEvidence(sourceSha256, "main.js"),
},
],
});
return createJavaScriptApplicationGraph({
schema: "JavaScriptApplicationGraph",
root_node_ids: [module.node_id],
nodes: [module],
edges: [],
coverage: completeCoverage,
limitations: [],
});
};
const occurrenceDifferences: readonly {
readonly name: string;
readonly leftOccurrence: JsonValue;
readonly rightOccurrence: JsonValue;
}[] = [
{
name: "path",
leftOccurrence: {
container_sha256: LEFT_SHA,
path: "main.js",
sha256: "c".repeat(64),
},
rightOccurrence: {
container_sha256: RIGHT_SHA,
path: "moved.js",
sha256: "c".repeat(64),
},
},
{
name: "content digest",
leftOccurrence: {
container_sha256: LEFT_SHA,
path: "main.js",
sha256: "c".repeat(64),
},
rightOccurrence: {
container_sha256: RIGHT_SHA,
path: "main.js",
sha256: "d".repeat(64),
},
},
{
name: "noncanonical data",
leftOccurrence: { container_sha256: LEFT_SHA },
rightOccurrence: { container_sha256: RIGHT_SHA },
},
];
it.each(occurrenceDifferences)(
"keeps occurrence $name differences visible without altering source evidence",
({ leftOccurrence, rightOccurrence }) => {
const left = occurrenceGraph(LEFT_SHA, leftOccurrence);
const right = occurrenceGraph(RIGHT_SHA, rightOccurrence);
const original = JSON.stringify([left, right]);
const result = compareJavaScriptApplicationVersions({
left: {
evidenceId: `ev_${"1".repeat(64)}`,
rootArtifactSha256: LEFT_SHA,
graph: left,
},
right: {
evidenceId: `ev_${"2".repeat(64)}`,
rootArtifactSha256: RIGHT_SHA,
graph: right,
},
leftNativeEvidence: [],
rightNativeEvidence: [],
});
expect(result.items).toMatchObject([
{
status: "changed",
match: { basis: "exact-module-source-digest", confidence: "exact" },
dimensions: ["properties"],
},
]);
expect(JSON.stringify([left, right])).toBe(original);
expect(left.nodes[0]?.observations[0]?.properties.file_occurrence).toEqual(
leftOccurrence,
);
expect(right.nodes[0]?.observations[0]?.properties.file_occurrence).toEqual(
rightOccurrence,
);
expect(left.nodes[0]?.observations[0]?.evidence).toEqual(
right.nodes[0]?.observations[0]?.evidence,
);
},
);
describe("application version comparison Evidence links", () => {
it("links every exact digest observation only to its paired or unmatched item", () => {
const left = buildGraph(LEFT_SHA, 1);
const right = buildGraph(RIGHT_SHA, 2);
const leftEvidenceId = `ev_${"1".repeat(64)}`;
const rightEvidenceId = `ev_${"2".repeat(64)}`;
const leftBDigest = `${"0".repeat(63)}1`;
const leftNative = [
nativeEvidence(leftBDigest, 1),
nativeEvidence(leftBDigest, 2),
nativeEvidence("9".repeat(64), 3),
];
const rightNative = [
nativeEvidence("0".repeat(64), 4),
nativeEvidence(undefined, 6),
];
const unrelatedRight = unrelatedGraph(RIGHT_SHA);
const unmatched = compareJavaScriptApplicationVersions({
left: {
evidenceId: leftEvidenceId,
rootArtifactSha256: LEFT_SHA,
graph: left,
},
right: {
evidenceId: rightEvidenceId,
rootArtifactSha256: RIGHT_SHA,
graph: unrelatedRight,
},
leftNativeEvidence: leftNative,
rightNativeEvidence: rightNative,
});
const leftB = left.nodes.find(({ observations }) =>
observations.some(({ label }) => label === "b.js"),
);
if (leftB === undefined) throw new Error("Missing left b.js node");
const unmatchedB = unmatched.items.find(
({ left_node_id: id }) => id === leftB.node_id,
);
expect(unmatchedB?.evidence_links).toEqual(
[
leftEvidenceId,
rightEvidenceId,
leftNative[0]!.evidence_id,
leftNative[1]!.evidence_id,
].sort(),
);
const unrelatedItem = unmatched.items.find(
({ right_node_id: id }) => id === unrelatedRight.nodes[0]?.node_id,
);
expect(unrelatedItem?.evidence_links).toEqual(
[leftEvidenceId, rightEvidenceId].sort(),
);
const paired = compareJavaScriptApplicationVersions({
left: {
evidenceId: leftEvidenceId,
rootArtifactSha256: LEFT_SHA,
graph: left,
},
right: {
evidenceId: rightEvidenceId,
rootArtifactSha256: RIGHT_SHA,
graph: right,
},
leftNativeEvidence: [nativeEvidence("0".repeat(64), 5)],
rightNativeEvidence: rightNative,
});
const leftA = left.nodes.find(({ observations }) =>
observations.some(({ label }) => label === "a.js"),
);
if (leftA === undefined) throw new Error("Missing left a.js node");
const pairedA = paired.items.find(
({ left_node_id: id }) => id === leftA.node_id,
);
expect(pairedA?.match.status).toBe("matched");
expect(pairedA?.evidence_links).toEqual(
[
leftEvidenceId,
rightEvidenceId,
nativeEvidence("0".repeat(64), 5).evidence_id,
rightNative[0]!.evidence_id,
].sort(),
);
});
});
describe("application version comparison graph ownership", () => {
it("returns a sealed change graph that passes complete validation", () => {
const result = compare();
expect(
result.graph.edges.some(({ relation }) => relation === "changed_from"),
).toBe(true);
expect(isValidatedImmutableJavaScriptApplicationGraph(result.graph)).toBe(
true,
);
expect(Object.isFrozen(result.graph.edges)).toBe(true);
expect(parseJavaScriptApplicationGraph(result.graph)).toEqual(result.graph);
expect(applicationVersionComparisonResultSchema.parse(result)).toEqual(
result,
);
});
it("retains deduplicated complete input limits on the change graph and inferred edge", () => {
const leftLimits = [
{ name: "modules", value: 100, unit: "items" as const },
];
const rightLimits = [
{ name: "source-bytes", value: 4096, unit: "bytes" as const },
{ name: "modules", value: 100, unit: "items" as const },
];
const withLimits = (
graph: JavaScriptApplicationGraph,
limits: JavaScriptApplicationGraph["coverage"]["limits"],
): JavaScriptApplicationGraph => {
const { graph_id: _graphId, ...input } = graph;
return createJavaScriptApplicationGraph({
...input,
coverage: { ...graph.coverage, limits },
});
};
const result = compareJavaScriptApplicationVersions({
left: {
evidenceId: `ev_${"1".repeat(64)}`,
rootArtifactSha256: LEFT_SHA,
graph: withLimits(buildGraph(LEFT_SHA, 1), leftLimits),
},
right: {
evidenceId: `ev_${"2".repeat(64)}`,
rootArtifactSha256: RIGHT_SHA,
graph: withLimits(buildGraph(RIGHT_SHA, 2), rightLimits),
},
leftNativeEvidence: [],
rightNativeEvidence: [],
});
const expectedLimits = [leftLimits[0]!, rightLimits[0]!];
const changed = result.graph.edges.find(
({ relation }) => relation === "changed_from",
);
expect(changed).toBeDefined();
expect(result.graph.coverage).toMatchObject({
status: "complete",
truncated: false,
omitted_count: 0,
limits: expectedLimits,
});
expect(changed?.evidence.coverage).toMatchObject({
status: "complete",
truncated: false,
omitted_count: 0,
limits: expectedLimits,
});
});
it("does not reuse a change graph without an owned proof", () => {
const result = compare();
const copy = structuredClone(result.graph);
expect(isValidatedImmutableJavaScriptApplicationGraph(copy)).toBe(false);
expect(() =>
ownedApplicationVersionComparisonResultSchema.parse({
...result,
graph: copy,
}),
).toThrow();
expect(
applicationVersionComparisonResultSchema.parse({ ...result, graph: copy })
.graph,
).toEqual(result.graph);
});
it("keeps coverage validation for owned results", () => {
const result = compare();
expect(() =>
ownedApplicationVersionComparisonResultSchema.parse({
...result,
coverage: { ...result.coverage, status: "partial" },
}),
).toThrow("Comparison coverage must match source graph completeness");
});
});