Files
rea/src/domain/javascript/javascriptApplicationVersionUnavailableScopes.test.ts
T
morluto 8d198d6ec7 fix(javascript): preserve unavailable scope uncertainty in comparisons
Classify wholly unknown or unavailable node observations at the version-item owner and retain their limitations across matched and unmatched comparisons. Preserve positive mixed observations and genuine additions/removals.

Validated source regressions and public CLI/stdout MCP workflows in the 103-test focused gate; typecheck and lint pass. Fixes #5534.
2026-10-20 23:14:47 +08:00

208 lines
6.5 KiB
TypeScript

import { expect, it } from "vitest";
import {
createJavaScriptApplicationGraph,
createJavaScriptApplicationNode,
} from "./javascriptApplicationGraph.js";
import {
artifactEvidence,
completeCoverage,
} from "./javascriptApplicationGraph.fixture.js";
import { compareJavaScriptApplicationVersions } from "./javascriptApplicationVersionComparison.js";
const LEFT_SHA = "a".repeat(64);
const RIGHT_SHA = "b".repeat(64);
const LIMITATION =
"Source is outside the caller-selected source_paths; it was inventoried but not analyzed.";
const evidenceId = (character: string) => `ev_${character.repeat(64)}`;
const node = (sha256: string, path: string) =>
createJavaScriptApplicationNode({
kind: "javascript-asset",
identity: { strategy: "content-digest", stability: "global-exact", sha256 },
observations: [
{
label: path,
properties: { path },
evidence: artifactEvidence(sha256, path),
},
],
});
const unavailableScope = (
artifactSha: string,
state: "unknown" | "unavailable" = "unavailable",
) =>
createJavaScriptApplicationNode({
kind: "unknown",
identity: {
strategy: "artifact-local-key",
stability: "artifact-version",
artifact_sha256: artifactSha,
namespace: "static-parse-scope",
key: "select-javascript-sources:lib.js",
},
observations: [
{
label: "lib.js parse scope",
properties: { path: "lib.js", operation: "select-javascript-sources" },
evidence: {
...artifactEvidence(artifactSha, "lib.js"),
state,
confidence: "unknown",
limitations: [LIMITATION],
},
},
],
});
const observedAssetWithUnavailableFact = () => {
const digest = "e".repeat(64);
return createJavaScriptApplicationNode({
kind: "javascript-asset",
identity: {
strategy: "content-digest",
stability: "global-exact",
sha256: digest,
},
observations: [
{
label: "new.js",
properties: { path: "new.js" },
evidence: artifactEvidence(digest, "new.js"),
},
{
label: "new.js parse scope",
properties: { operation: "parse-javascript" },
evidence: {
...artifactEvidence(digest, "new.js"),
state: "unavailable",
confidence: "unknown",
limitations: [LIMITATION],
},
},
],
});
};
const graph = (nodes: ReturnType<typeof node>[]) =>
createJavaScriptApplicationGraph({
schema: "JavaScriptApplicationGraph",
root_node_ids: [nodes[0]!.node_id],
nodes,
edges: [],
coverage: completeCoverage,
limitations: [],
});
const compare = (
left: ReturnType<typeof node>[],
right: ReturnType<typeof node>[],
) =>
compareJavaScriptApplicationVersions({
left: {
evidenceId: evidenceId("1"),
rootArtifactSha256: LEFT_SHA,
graph: graph(left),
},
right: {
evidenceId: evidenceId("2"),
rootArtifactSha256: RIGHT_SHA,
graph: graph(right),
},
leftNativeEvidence: [],
rightNativeEvidence: [],
});
it("keeps unmatched unavailable scopes unknown in either direction", () => {
const leftRoot = node("d".repeat(64), "main.js");
const rightRoot = node("d".repeat(64), "main.js");
const leftScope = unavailableScope(LEFT_SHA);
const rightScope = unavailableScope(RIGHT_SHA);
const forward = compare([leftRoot], [rightRoot, rightScope]);
const reverse = compare([leftRoot, leftScope], [rightRoot]);
expect(
forward.items.find(
({ right_node_id }) => right_node_id === rightScope.node_id,
),
).toMatchObject({
status: "unknown",
dimensions: ["availability"],
limitations: [LIMITATION],
evidence_links: [evidenceId("1"), evidenceId("2")],
});
expect(
reverse.items.find(
({ left_node_id }) => left_node_id === leftScope.node_id,
),
).toMatchObject({
status: "unknown",
dimensions: ["availability"],
limitations: [LIMITATION],
});
expect(forward.summary).toMatchObject({ added: 0, removed: 0, unknown: 1 });
expect(reverse.summary).toMatchObject({ added: 0, removed: 0, unknown: 1 });
});
it("keeps matched unavailable scopes unknown with their source limitation", () => {
const leftRoot = node("d".repeat(64), "main.js");
const rightRoot = node("d".repeat(64), "main.js");
const leftScope = unavailableScope(LEFT_SHA);
const rightScope = unavailableScope(LEFT_SHA);
const result = compare([leftRoot, leftScope], [rightRoot, rightScope]);
expect(
result.items.find(({ left_node_id }) => left_node_id === leftScope.node_id),
).toMatchObject({
right_node_id: rightScope.node_id,
status: "unknown",
dimensions: [],
limitations: [LIMITATION],
});
expect(result.summary).toMatchObject({ unchanged: 1, unknown: 1 });
});
it("retains unknown scope facts and reports genuine entity changes", () => {
const leftRoot = node("d".repeat(64), "main.js");
const rightRoot = node("d".repeat(64), "main.js");
const unknown = unavailableScope(RIGHT_SHA, "unknown");
const added = node("e".repeat(64), "new.js");
const removed = node("f".repeat(64), "removed.js");
const mixedEvidenceAddition = observedAssetWithUnavailableFact();
const unknownResult = compare([leftRoot], [rightRoot, unknown]);
const addedResult = compare([leftRoot], [rightRoot, added]);
const removedResult = compare([leftRoot, removed], [rightRoot]);
const mixedResult = compare([leftRoot], [rightRoot, mixedEvidenceAddition]);
expect(
unknownResult.items.find(
({ right_node_id }) => right_node_id === unknown.node_id,
),
).toMatchObject({ status: "unknown", limitations: [LIMITATION] });
expect(unknownResult.summary).toMatchObject({ added: 0, unknown: 1 });
expect(
addedResult.items.find(
({ right_node_id }) => right_node_id === added.node_id,
),
).toMatchObject({ status: "added", dimensions: ["availability"] });
expect(addedResult.summary.added).toBe(1);
expect(
mixedResult.items.find(
({ right_node_id }) => right_node_id === mixedEvidenceAddition.node_id,
),
).toMatchObject({ status: "added", limitations: [LIMITATION] });
expect(mixedResult.summary.added).toBe(1);
expect(
removedResult.items.find(
({ left_node_id }) => left_node_id === removed.node_id,
),
).toMatchObject({ status: "removed", dimensions: ["availability"] });
expect(removedResult.summary.removed).toBe(1);
expect(
compare(
[leftRoot, node("c".repeat(64), "same.js")],
[rightRoot, node("c".repeat(64), "same.js")],
).summary,
).toMatchObject({
unchanged: 2,
added: 0,
removed: 0,
});
});