Files
reverse-engineering-anything/src/domain/javascript/javascriptApplicationVersionRelocation.test.ts
T
morluto 556504edcb fix(javascript): distinguish relocation from application changes
Keep location-only matched pairs unchanged under complete coverage while retaining both coordinates and location dimensions. Preserve incomplete coverage as unknown and retain definite content, property, and relationship changes.

Validated with focused domain, compiled CLI and MCP workflows, static checks and docs checks. A tiny public CLI bundle fixture reproduces the issue; the reported Bitwarden releases were not rerun.

Refs #4520
2026-10-17 12:36:21 +08:00

221 lines
6.2 KiB
TypeScript

import { expect, it } from "vitest";
import {
artifactEvidence,
completeCoverage,
fixtureNode,
} from "./javascriptApplicationGraph.fixture.js";
import {
createJavaScriptApplicationEdge,
createJavaScriptApplicationGraph,
type JavaScriptApplicationGraph,
} from "./javascriptApplicationGraph.js";
import { compareJavaScriptApplicationVersions } from "./javascriptApplicationVersionComparison.js";
import { applicationVersionComparisonResultSchema } from "./javascriptApplicationVersionComparisonSchemas.js";
const graph = (
sha: string,
offset: number,
coverage: JavaScriptApplicationGraph["coverage"],
options: {
content?: string;
permission?: string;
relation?: "handles" | "invokes";
} = {},
) => {
const evidence = {
...artifactEvidence(sha, "main.js"),
location: {
available: true as const,
value: {
kind: "file-offset-range" as const,
start: offset,
end: offset + 10,
},
},
};
const module = fixtureNode(
"javascript-module",
{
strategy: "artifact-local-key",
stability: "artifact-version",
artifact_sha256: sha,
namespace: "webpack-module",
key: "11",
},
"module",
{
source_sha256: options.content ?? "c".repeat(64),
structural_fingerprint_sha256: "d".repeat(64),
structural_fingerprint_algorithm: "fixture",
},
evidence,
);
const channel = fixtureNode(
"ipc-channel",
{
strategy: "artifact-local-key",
stability: "artifact-version",
artifact_sha256: sha,
namespace: "ipc",
key: "unchanged-channel",
},
"unchanged-channel",
{
channel: "unchanged-channel",
...(options.permission === undefined
? {}
: { permission: options.permission }),
},
evidence,
);
return createJavaScriptApplicationGraph({
schema: "JavaScriptApplicationGraph",
root_node_ids: [module.node_id],
nodes: [module, channel],
edges: [
createJavaScriptApplicationEdge({
source_node_id: module.node_id,
target_node_id: channel.node_id,
relation: options.relation ?? "handles",
properties: {},
evidence,
}),
],
coverage,
limitations:
coverage.status === "complete"
? []
: [
"Controlled incomplete inventory; unobserved relationships remain unknown.",
],
});
};
const compare = (
left: JavaScriptApplicationGraph,
right: JavaScriptApplicationGraph,
) =>
compareJavaScriptApplicationVersions({
left: {
evidenceId: `ev_${"1".repeat(64)}`,
rootArtifactSha256: "a".repeat(64),
graph: left,
},
right: {
evidenceId: `ev_${"2".repeat(64)}`,
rootArtifactSha256: "b".repeat(64),
graph: right,
},
leftNativeEvidence: [],
rightNativeEvidence: [],
});
const partialCoverage: JavaScriptApplicationGraph["coverage"] = {
...completeCoverage,
status: "partial",
omitted_count: null,
};
it("keeps relocated exact modules and inferred channels unchanged while retaining both coordinates", () => {
const left = graph("a".repeat(64), 10, completeCoverage),
right = graph("b".repeat(64), 50, completeCoverage);
const result = applicationVersionComparisonResultSchema.parse(
JSON.parse(JSON.stringify(compare(left, right))),
);
expect(result.summary).toEqual({
unchanged: 2,
changed: 0,
added: 0,
removed: 0,
unknown: 0,
});
expect(result.items).toHaveLength(2);
for (const item of result.items)
expect(item).toMatchObject({
status: "unchanged",
dimensions: ["location"],
});
expect(result.matching).toMatchObject({
exact_module_source_digest: 1,
semantic_key: 1,
});
expect(result.graph.nodes).toHaveLength(4);
expect(
result.graph.nodes.map((node) => node.observations[0]?.evidence.location),
).toEqual(
expect.arrayContaining([
left.nodes[0]?.observations[0]?.evidence.location,
right.nodes[0]?.observations[0]?.evidence.location,
]),
);
expect(
result.graph.edges.filter((edge) => edge.relation === "changed_from"),
).toEqual([]);
});
it.each([false, true])(
"keeps relocation and uncertain relationships unknown under incomplete coverage (relationship change: %s)",
(changeRelation) => {
const result = compare(
graph("a".repeat(64), 10, completeCoverage),
graph("b".repeat(64), 50, partialCoverage, {
relation: changeRelation ? "invokes" : "handles",
}),
);
expect(result.summary).toEqual({
unchanged: 0,
changed: 0,
added: 0,
removed: 0,
unknown: 2,
});
for (const item of result.items)
expect(item).toMatchObject({
status: "unknown",
dimensions: changeRelation
? ["location", "relationships", "coverage"]
: ["location", "coverage"],
});
},
);
it("still classifies complete relationship changes beside relocation as changed", () => {
const result = compare(
graph("a".repeat(64), 10, completeCoverage),
graph("b".repeat(64), 50, completeCoverage, { relation: "invokes" }),
);
expect(result.summary.changed).toBe(2);
for (const item of result.items)
expect(item.dimensions).toEqual(["location", "relationships"]);
});
it("retains definite content changes under partial coverage alongside relocation", () => {
const result = compare(
graph("a".repeat(64), 10, completeCoverage),
graph("b".repeat(64), 50, partialCoverage, { content: "e".repeat(64) }),
);
expect(
result.items.find((item) => item.node_kind === "javascript-module"),
).toMatchObject({
status: "changed",
dimensions: ["content", "location", "properties"],
});
expect(
result.items.find((item) => item.node_kind === "ipc-channel")?.status,
).toBe("unknown");
});
it("retains definite property changes beside relocation", () => {
const result = compare(
graph("a".repeat(64), 10, completeCoverage),
graph("b".repeat(64), 50, partialCoverage, { permission: "changed" }),
);
expect(
result.items.find((item) => item.node_kind === "ipc-channel"),
).toMatchObject({
status: "changed",
dimensions: ["location", "properties"],
});
expect(
result.items.find((item) => item.node_kind === "javascript-module")?.status,
).toBe("unknown");
});