321 lines
9.2 KiB
TypeScript
321 lines
9.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
createJavaScriptApplicationGraph,
|
|
createJavaScriptApplicationNode,
|
|
} from "../src/domain/javascriptApplicationGraph.js";
|
|
import { createHistoricalSourceGraph } from "../src/domain/referenceSourceGraph.js";
|
|
import { compareSourceToBundle } from "../src/domain/sourceToBundleComparison.js";
|
|
import { sourceToBundleComparisonResultSchema } from "../src/domain/sourceToBundleComparisonSchemas.js";
|
|
import { artifactEvidence } from "./fixtures/javascriptApplicationGraph.js";
|
|
|
|
const HASH = {
|
|
artifact: "1".repeat(64),
|
|
unchanged: "2".repeat(64),
|
|
modified: "3".repeat(64),
|
|
removed: "4".repeat(64),
|
|
split: "5".repeat(64),
|
|
merged: "6".repeat(64),
|
|
duplicated: "7".repeat(64),
|
|
} as const;
|
|
const EVIDENCE_ID = `ev_${"a".repeat(64)}`;
|
|
|
|
describe("historical source to bundle comparison", () => {
|
|
it("classifies exact, changed, absent, split, and merged mappings stably", () => {
|
|
const reference = historicalGraph("complete");
|
|
const graph = applicationGraph("complete");
|
|
const input = {
|
|
reference,
|
|
application: {
|
|
evidenceId: EVIDENCE_ID,
|
|
rootArtifactSha256: HASH.artifact,
|
|
graph,
|
|
},
|
|
limits: limits(),
|
|
};
|
|
|
|
const first = compareSourceToBundle(input);
|
|
const second = compareSourceToBundle(input);
|
|
|
|
expect(first).toEqual(second);
|
|
expect(() =>
|
|
sourceToBundleComparisonResultSchema.parse(first),
|
|
).not.toThrow();
|
|
expect(first.summary).toEqual({
|
|
unchanged: 1,
|
|
modified: 0,
|
|
removed: 1,
|
|
split: 1,
|
|
merged: 2,
|
|
duplicated: 1,
|
|
unknown: 1,
|
|
});
|
|
expect(item(first, "src/unchanged.ts")).toMatchObject({
|
|
status: "unchanged",
|
|
confidence: "exact",
|
|
candidates: [
|
|
expect.objectContaining({
|
|
signals: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
kind: "exact-source-digest",
|
|
weight: 100,
|
|
}),
|
|
]),
|
|
}),
|
|
],
|
|
});
|
|
expect(item(first, "src/modified.ts")).toMatchObject({
|
|
status: "unknown",
|
|
confidence: "unknown",
|
|
candidates: [
|
|
expect.objectContaining({
|
|
signals: expect.arrayContaining([
|
|
expect.objectContaining({ kind: "current-path-exact", weight: 60 }),
|
|
]),
|
|
}),
|
|
],
|
|
});
|
|
expect(item(first, "src/removed.ts")).toMatchObject({
|
|
status: "removed",
|
|
current_node_ids: [],
|
|
});
|
|
expect(item(first, "src/split.ts")).toMatchObject({
|
|
status: "split",
|
|
confidence: "medium",
|
|
current_node_ids: [expect.any(String), expect.any(String)],
|
|
});
|
|
expect(item(first, "src/merged-a.ts").status).toBe("merged");
|
|
expect(item(first, "src/merged-b.ts").status).toBe("merged");
|
|
expect(item(first, "src/duplicated.ts")).toMatchObject({
|
|
status: "duplicated",
|
|
confidence: "exact",
|
|
current_node_ids: [expect.any(String), expect.any(String)],
|
|
});
|
|
expect(first.coverage.status).toBe("complete-within-inputs");
|
|
});
|
|
|
|
it("keeps absence unknown when either inventory is incomplete", () => {
|
|
const reference = historicalGraph("partial", ["src/removed.ts"]);
|
|
const graph = applicationGraph("partial");
|
|
const result = compareSourceToBundle({
|
|
reference,
|
|
application: {
|
|
evidenceId: EVIDENCE_ID,
|
|
rootArtifactSha256: HASH.artifact,
|
|
graph,
|
|
},
|
|
limits: limits(),
|
|
});
|
|
|
|
expect(result.summary).toMatchObject({ removed: 0, unknown: 1 });
|
|
expect(result.items[0]).toMatchObject({
|
|
source_path: "src/removed.ts",
|
|
status: "unknown",
|
|
confidence: "unknown",
|
|
});
|
|
expect(result.coverage.status).toBe("partial");
|
|
});
|
|
|
|
it("keeps exact mappings unknown when candidate output is truncated", () => {
|
|
const result = compareSourceToBundle({
|
|
reference: historicalGraph("complete"),
|
|
application: {
|
|
evidenceId: EVIDENCE_ID,
|
|
rootArtifactSha256: HASH.artifact,
|
|
graph: applicationGraph("complete"),
|
|
},
|
|
limits: { ...limits(), max_candidate_nodes: 1 },
|
|
});
|
|
|
|
expect(item(result, "src/duplicated.ts").status).toBe("unknown");
|
|
expect(
|
|
item(result, "src/duplicated.ts").omitted_candidates,
|
|
).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("reports the candidate frontier instead of deciding past a limit", () => {
|
|
const result = compareSourceToBundle({
|
|
reference: historicalGraph("complete", ["src/split.ts"]),
|
|
application: {
|
|
evidenceId: EVIDENCE_ID,
|
|
rootArtifactSha256: HASH.artifact,
|
|
graph: applicationGraph("complete"),
|
|
},
|
|
limits: { ...limits(), max_candidate_evaluations: 1 },
|
|
});
|
|
|
|
expect(result.items[0]).toMatchObject({
|
|
status: "unknown",
|
|
confidence: "unknown",
|
|
omitted_candidates: 1,
|
|
});
|
|
expect(result.coverage).toMatchObject({
|
|
status: "truncated",
|
|
candidate_evaluations: 1,
|
|
omitted_candidate_evaluations: 1,
|
|
});
|
|
});
|
|
});
|
|
|
|
const historicalGraph = (
|
|
inventoryState: "complete" | "partial",
|
|
paths = [
|
|
"src/duplicated.ts",
|
|
"src/merged-a.ts",
|
|
"src/merged-b.ts",
|
|
"src/modified.ts",
|
|
"src/removed.ts",
|
|
"src/split.ts",
|
|
"src/unchanged.ts",
|
|
],
|
|
) =>
|
|
createHistoricalSourceGraph({
|
|
schema: "HistoricalSourceGraph/v1",
|
|
authority: "historical-reference",
|
|
root_alias: "$REFERENCE_ROOT",
|
|
inventory_state: inventoryState,
|
|
entries: [
|
|
{
|
|
path: "src",
|
|
kind: "directory",
|
|
classifications: ["source"],
|
|
tree_state: inventoryState === "complete" ? "enumerated" : "partial",
|
|
limitations:
|
|
inventoryState === "complete"
|
|
? []
|
|
: ["Fixture inventory is partial."],
|
|
},
|
|
...paths.map((path) => ({
|
|
path,
|
|
kind: "file" as const,
|
|
sha256: sourceDigest(path),
|
|
size: 10,
|
|
language: "TypeScript",
|
|
classifications: ["source"] as const,
|
|
content_state: "hashed" as const,
|
|
limitations: [],
|
|
})),
|
|
],
|
|
relationships: [],
|
|
parse_failures: [],
|
|
exclusions: [],
|
|
languages: ["TypeScript"],
|
|
manifests: [],
|
|
vcs: { kind: "none", head: null, dirty: null },
|
|
provenance: {
|
|
importer: "rea-test",
|
|
importer_version: "1",
|
|
caller: "source-to-bundle-test",
|
|
},
|
|
limitations:
|
|
inventoryState === "complete" ? [] : ["Fixture inventory is partial."],
|
|
});
|
|
|
|
const applicationGraph = (coverage: "complete" | "partial") => {
|
|
const nodes = [
|
|
moduleNode("duplicated-a", {
|
|
path: "bundle-a.js",
|
|
source_sha256: HASH.duplicated,
|
|
}),
|
|
moduleNode("duplicated-b", {
|
|
path: "bundle-b.js",
|
|
source_sha256: HASH.duplicated,
|
|
}),
|
|
moduleNode("unchanged", {
|
|
path: "bundle.js",
|
|
source_sha256: HASH.unchanged,
|
|
}),
|
|
createJavaScriptApplicationNode({
|
|
kind: "javascript-asset",
|
|
identity: {
|
|
strategy: "canonical-path",
|
|
stability: "artifact-version",
|
|
artifact_sha256: HASH.artifact,
|
|
path: "src/modified.ts",
|
|
},
|
|
observations: [
|
|
{
|
|
label: "src/modified.ts",
|
|
properties: { path: "src/modified.ts" },
|
|
evidence: artifactEvidence(HASH.artifact, "src/modified.ts"),
|
|
},
|
|
],
|
|
}),
|
|
moduleNode("split-a", { path: "bundle-a/src/split.ts" }),
|
|
moduleNode("split-b", { path: "bundle-b/src/split.ts" }),
|
|
moduleNode("merged", {
|
|
path: "bundle.js",
|
|
source_sha256: HASH.merged,
|
|
}),
|
|
];
|
|
return createJavaScriptApplicationGraph({
|
|
schema: "JavaScriptApplicationGraph",
|
|
schema_version: 1,
|
|
root_node_ids: nodes.map(({ node_id: nodeId }) => nodeId),
|
|
nodes,
|
|
edges: [],
|
|
coverage:
|
|
coverage === "complete"
|
|
? {
|
|
status: "complete",
|
|
truncated: false,
|
|
omitted_count: 0,
|
|
limits: [],
|
|
}
|
|
: {
|
|
status: "partial",
|
|
truncated: true,
|
|
omitted_count: 1,
|
|
limits: [{ name: "fixture", value: 5, unit: "items" }],
|
|
},
|
|
limitations:
|
|
coverage === "complete" ? [] : ["Fixture graph coverage is partial."],
|
|
});
|
|
};
|
|
|
|
const moduleNode = (key: string, properties: Record<string, string>) =>
|
|
createJavaScriptApplicationNode({
|
|
kind: "javascript-module",
|
|
identity: {
|
|
strategy: "artifact-local-key",
|
|
stability: "artifact-version",
|
|
artifact_sha256: HASH.artifact,
|
|
namespace: "test",
|
|
key,
|
|
},
|
|
observations: [
|
|
{
|
|
label: key,
|
|
properties,
|
|
evidence: artifactEvidence(HASH.artifact, "bundle.js"),
|
|
},
|
|
],
|
|
});
|
|
|
|
const sourceDigest = (path: string): string => {
|
|
if (path.includes("duplicated")) return HASH.duplicated;
|
|
if (path.includes("merged")) return HASH.merged;
|
|
if (path.includes("modified")) return HASH.modified;
|
|
if (path.includes("removed")) return HASH.removed;
|
|
if (path.includes("split")) return HASH.split;
|
|
return HASH.unchanged;
|
|
};
|
|
|
|
const limits = () => ({
|
|
max_source_files: 100,
|
|
max_application_nodes: 100,
|
|
max_candidate_nodes: 100,
|
|
max_candidate_evaluations: 10_000,
|
|
});
|
|
|
|
const item = (
|
|
result: ReturnType<typeof compareSourceToBundle>,
|
|
sourcePath: string,
|
|
) => {
|
|
const value = result.items.find(
|
|
({ source_path: path }) => path === sourcePath,
|
|
);
|
|
if (value === undefined) throw new TypeError(`Missing item: ${sourcePath}`);
|
|
return value;
|
|
};
|