641 lines
22 KiB
TypeScript
641 lines
22 KiB
TypeScript
import {
|
|
semanticFixtureGraph,
|
|
semanticFixtureInput,
|
|
semanticGraphTestEvidence,
|
|
} from "../../../tests/support/javascriptSemanticGraphFixture.js";
|
|
import { expect, it } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { digestCanonicalValue } from "../canonicalDigest.js";
|
|
import type { JsonValue } from "../jsonValue.js";
|
|
import type { ApplicationGraphEvidence } from "./javascriptApplicationEvidenceSchemas.js";
|
|
import {
|
|
JAVASCRIPT_SEMANTIC_RELATION_FAMILY,
|
|
JAVASCRIPT_SEMANTIC_NODE_KINDS,
|
|
JAVASCRIPT_SEMANTIC_RELATIONS,
|
|
type JavaScriptSemanticGraphInput,
|
|
} from "./javascriptSemanticGraphSchemas.js";
|
|
import {
|
|
createJavaScriptSemanticGraph,
|
|
createJavaScriptSemanticGraphNode,
|
|
createJavaScriptSemanticGraphRelation,
|
|
createJavaScriptSemanticGraphUnknown,
|
|
createImmutableJavaScriptSemanticGraphSteps,
|
|
isValidatedImmutableJavaScriptSemanticGraph,
|
|
JavaScriptSemanticEvidenceContextRegistry,
|
|
javaScriptSemanticGraphSchema,
|
|
resolveJavaScriptSemanticEvidence,
|
|
type JavaScriptSemanticGraph,
|
|
} from "./javascriptSemanticGraph.js";
|
|
import type { JavaScriptSemanticGraphNode } from "./javascriptSemanticGraphSchemas.js";
|
|
import {
|
|
parseJavaScriptSemanticGraph,
|
|
serializeJavaScriptSemanticGraph,
|
|
} from "./javascriptSemanticGraphSerialization.js";
|
|
import { queryJavaScriptSemanticGraph } from "./javascriptSemanticQuery.js";
|
|
|
|
const SHA = "a".repeat(64);
|
|
const node = (
|
|
kind: (typeof JAVASCRIPT_SEMANTIC_NODE_KINDS)[number],
|
|
role: string,
|
|
properties: Record<string, JsonValue> = {},
|
|
evidenceContexts: JavaScriptSemanticEvidenceContextRegistry,
|
|
options: {
|
|
readonly functionNodeId?: string | null;
|
|
readonly modulePath?: string;
|
|
} = {},
|
|
): JavaScriptSemanticGraphNode =>
|
|
createJavaScriptSemanticGraphNode(
|
|
{
|
|
kind,
|
|
identity: {
|
|
artifact_sha256: SHA,
|
|
module_path: options.modulePath ?? "bundle.js",
|
|
source_range: {
|
|
start: { line: 1, column: role.length },
|
|
end: { line: 1, column: role.length + 1 },
|
|
},
|
|
role_key: role,
|
|
},
|
|
function_node_id: options.functionNodeId ?? null,
|
|
application_node_ids: [],
|
|
label: role,
|
|
properties,
|
|
evidence: semanticGraphTestEvidence(),
|
|
},
|
|
evidenceContexts,
|
|
);
|
|
|
|
it("accepts semantic module paths longer than the former schema ceiling", () => {
|
|
const modulePath = `${"segment/".repeat(600)}entry.js`;
|
|
const evidenceContexts = new JavaScriptSemanticEvidenceContextRegistry();
|
|
expect(
|
|
node("module", "long-path", {}, evidenceContexts, { modulePath }).identity
|
|
.module_path,
|
|
).toBe(modulePath);
|
|
});
|
|
|
|
it("captures caller-owned node identity, properties, and provenance", () => {
|
|
const contexts = new JavaScriptSemanticEvidenceContextRegistry();
|
|
const input = {
|
|
kind: "literal",
|
|
identity: {
|
|
artifact_sha256: SHA,
|
|
module_path: "bundle.js",
|
|
source_range: null,
|
|
role_key: "literal",
|
|
},
|
|
function_node_id: null,
|
|
application_node_ids: [],
|
|
label: "literal",
|
|
properties: { nested: ["original"] },
|
|
evidence: semanticGraphTestEvidence(),
|
|
};
|
|
const captured = createJavaScriptSemanticGraphNode(input, contexts);
|
|
const expected = structuredClone(captured);
|
|
const expectedEvidence = structuredClone(input.evidence);
|
|
|
|
input.identity.module_path = "replaced.js";
|
|
input.properties.nested[0] = "replaced";
|
|
input.evidence.limitations.push("later mutation");
|
|
if (input.evidence.location.available)
|
|
input.evidence.location.value = {
|
|
kind: "artifact-path",
|
|
path: "replaced.js",
|
|
};
|
|
|
|
expect(captured).toEqual(expected);
|
|
expect(
|
|
resolveJavaScriptSemanticEvidence(
|
|
{ evidence_contexts: contexts.contexts },
|
|
captured.evidence,
|
|
),
|
|
).toEqual(expectedEvidence);
|
|
});
|
|
|
|
const registryForGraph = (
|
|
graph: JavaScriptSemanticGraph,
|
|
): JavaScriptSemanticEvidenceContextRegistry => {
|
|
const registry = new JavaScriptSemanticEvidenceContextRegistry();
|
|
for (const { evidence: reference } of [
|
|
...graph.nodes,
|
|
...graph.relations,
|
|
...graph.unknowns,
|
|
...graph.fingerprints,
|
|
])
|
|
registry.intern(resolveJavaScriptSemanticEvidence(graph, reference));
|
|
return registry;
|
|
};
|
|
|
|
const graphWithCandidateRelations = (): JavaScriptSemanticGraph => {
|
|
const base = semanticFixtureGraph();
|
|
const evidenceContexts = registryForGraph(base);
|
|
const relations = base.relations.map(
|
|
({ relation_id: _relationId, ...relation }) =>
|
|
createJavaScriptSemanticGraphRelation(
|
|
{
|
|
...relation,
|
|
resolution: "candidate",
|
|
evidence: resolveJavaScriptSemanticEvidence(base, relation.evidence),
|
|
},
|
|
evidenceContexts,
|
|
),
|
|
);
|
|
const binding = base.nodes.find(({ label }) => label === "binding");
|
|
const callable = base.nodes.find(({ label }) => label === "function");
|
|
if (binding === undefined || callable === undefined)
|
|
throw new Error("Expected candidate-cycle endpoints");
|
|
for (const [source, target] of [
|
|
[binding, callable],
|
|
[callable, binding],
|
|
] as const)
|
|
relations.push(
|
|
createJavaScriptSemanticGraphRelation(
|
|
{
|
|
source_node_id: source.node_id,
|
|
target_node_id: target.node_id,
|
|
relation: "owns",
|
|
resolution: "candidate",
|
|
properties: {},
|
|
evidence: semanticGraphTestEvidence("inferred"),
|
|
},
|
|
evidenceContexts,
|
|
),
|
|
);
|
|
|
|
const { graph_id: _graphId, ...input } = base;
|
|
return createJavaScriptSemanticGraph({
|
|
...input,
|
|
evidence_contexts: evidenceContexts.contexts,
|
|
relations,
|
|
coverage: {
|
|
...input.coverage,
|
|
families: input.coverage.families.map((family) => ({
|
|
...family,
|
|
retained_relations: relations.filter(
|
|
({ relation }) =>
|
|
JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation] === family.family,
|
|
).length,
|
|
})),
|
|
},
|
|
});
|
|
};
|
|
|
|
it("counts candidate edges by query direction and relation filter", () => {
|
|
const graph = graphWithCandidateRelations();
|
|
const binding = graph.nodes.find(({ label }) => label === "binding");
|
|
if (binding === undefined) throw new Error("Expected binding seed");
|
|
const query = (
|
|
direction: "forward-influence" | "backward-provenance",
|
|
allowedRelations: (typeof JAVASCRIPT_SEMANTIC_RELATIONS)[number][],
|
|
) =>
|
|
queryJavaScriptSemanticGraph(graph, {
|
|
seed: { kind: "semantic-node", node_id: binding.node_id },
|
|
direction,
|
|
allowed_relations: allowedRelations,
|
|
});
|
|
|
|
expect(query("forward-influence", ["captures"]).status).toBe("ambiguous");
|
|
expect(query("forward-influence", ["defines"]).status).toBe("found");
|
|
expect(query("backward-provenance", ["defines"]).status).toBe("ambiguous");
|
|
expect(query("backward-provenance", ["captures"]).status).toBe("found");
|
|
});
|
|
|
|
it("matches scalar literal seeds and skips non-scalar literal metadata", () => {
|
|
const input = semanticFixtureInput();
|
|
const metadataContexts = new JavaScriptSemanticEvidenceContextRegistry();
|
|
const literals = [
|
|
node("literal", "string-seven", { value: "7" }, metadataContexts),
|
|
node("literal", "number-seven", { value: 7 }, metadataContexts),
|
|
node("literal", "boolean-true", { value: true }, metadataContexts),
|
|
node("literal", "null", { value: null }, metadataContexts),
|
|
node("literal", "empty-string", { value: "" }, metadataContexts),
|
|
node("literal", "zero", { value: 0 }, metadataContexts),
|
|
node("literal", "negative-zero", { value: -0 }, metadataContexts),
|
|
node("literal", "missing-value", {}, metadataContexts),
|
|
node(
|
|
"literal",
|
|
"structured-value",
|
|
{ value: { nested: "metadata" } },
|
|
metadataContexts,
|
|
),
|
|
];
|
|
input.nodes.push(...literals);
|
|
const graph = createJavaScriptSemanticGraph(input);
|
|
const query = (value: string | number | boolean | null) =>
|
|
queryJavaScriptSemanticGraph(graph, {
|
|
seed: { kind: "literal", value },
|
|
direction: "forward-influence",
|
|
}).seed_node_ids;
|
|
const nodeId = (role: string) => {
|
|
const literal = literals.find(({ identity }) => identity.role_key === role);
|
|
if (literal === undefined) throw new Error(`Missing literal ${role}`);
|
|
return literal.node_id;
|
|
};
|
|
|
|
expect(query("7")).toEqual([nodeId("string-seven")]);
|
|
expect(query(7)).toEqual([nodeId("number-seven")]);
|
|
expect(query(true)).toEqual([nodeId("boolean-true")]);
|
|
expect(query(null)).toEqual([nodeId("null")]);
|
|
expect(query("")).toEqual([nodeId("empty-string")]);
|
|
expect(query(0)).toEqual(
|
|
[nodeId("zero"), nodeId("negative-zero")].toSorted(),
|
|
);
|
|
expect(query("unmatched")).toEqual([]);
|
|
});
|
|
|
|
it("counts candidate ownership cycles once and traverses them finitely", () => {
|
|
const graph = graphWithCandidateRelations();
|
|
const binding = graph.nodes.find(({ label }) => label === "binding");
|
|
if (binding === undefined) throw new Error("Expected binding seed");
|
|
const result = queryJavaScriptSemanticGraph(graph, {
|
|
seed: { kind: "semantic-node", node_id: binding.node_id },
|
|
direction: "ownership",
|
|
allowed_relations: ["owns"],
|
|
include_ambiguous_dynamic_edges: true,
|
|
});
|
|
|
|
expect(result.status).toBe("ambiguous");
|
|
expect(result.relations).toHaveLength(2);
|
|
expect(result.summary.traversed_nodes).toBe(2);
|
|
expect(result.summary.traversed_relations).toBe(2);
|
|
expect(
|
|
queryJavaScriptSemanticGraph(graph, {
|
|
seed: { kind: "semantic-node", node_id: binding.node_id },
|
|
direction: "ownership",
|
|
allowed_relations: ["constructs-request"],
|
|
}).status,
|
|
).toBe("found");
|
|
});
|
|
|
|
it("defines every required relation family and canonicalizes records", () => {
|
|
expect(JAVASCRIPT_SEMANTIC_RELATIONS).toContain("argument-to-parameter");
|
|
expect(JAVASCRIPT_SEMANTIC_RELATIONS).toContain("detaches-task");
|
|
expect(JAVASCRIPT_SEMANTIC_RELATIONS).toContain("forwards-signal");
|
|
expect(JAVASCRIPT_SEMANTIC_RELATIONS).toContain("validates");
|
|
const graph = semanticFixtureGraph();
|
|
expect(
|
|
parseJavaScriptSemanticGraph(
|
|
JSON.parse(serializeJavaScriptSemanticGraph(graph)),
|
|
),
|
|
).toEqual(graph);
|
|
expect(graph.nodes.map(({ node_id }) => node_id)).toEqual(
|
|
graph.nodes.map(({ node_id }) => node_id).toSorted(),
|
|
);
|
|
});
|
|
|
|
it("rejects stale identities, dangling endpoints, and incomplete coverage claims", () => {
|
|
const graph = semanticFixtureGraph();
|
|
expect(() =>
|
|
parseJavaScriptSemanticGraph({
|
|
...graph,
|
|
graph_id: `jsrg_${"0".repeat(64)}`,
|
|
}),
|
|
).toThrow();
|
|
expect(() =>
|
|
createJavaScriptSemanticGraph({
|
|
...semanticFixtureInput(),
|
|
relations: [
|
|
{
|
|
...semanticFixtureInput().relations[0],
|
|
target_node_id: `jsrg_node_${"f".repeat(64)}`,
|
|
},
|
|
],
|
|
}),
|
|
).toThrow();
|
|
expect(() =>
|
|
createJavaScriptSemanticGraph({
|
|
...semanticFixtureInput(),
|
|
coverage: { ...semanticFixtureInput().coverage, omitted_nodes: 1 },
|
|
}),
|
|
).toThrow(/Complete graph coverage/u);
|
|
const unknownGraphInput = semanticFixtureInput(true);
|
|
const originalUnknown = unknownGraphInput.unknowns[0];
|
|
if (originalUnknown === undefined)
|
|
throw new Error("Expected unknown frontier");
|
|
const { unknown_id: _unknownId, ...unknownInput } = originalUnknown;
|
|
const validatedUnknownGraph =
|
|
createJavaScriptSemanticGraph(unknownGraphInput);
|
|
const evidenceContexts = registryForGraph(validatedUnknownGraph);
|
|
unknownGraphInput.unknowns = [
|
|
createJavaScriptSemanticGraphUnknown(
|
|
{
|
|
...unknownInput,
|
|
candidate_node_ids: [`jsrg_node_${"f".repeat(64)}`],
|
|
evidence: resolveJavaScriptSemanticEvidence(
|
|
validatedUnknownGraph,
|
|
originalUnknown.evidence,
|
|
),
|
|
},
|
|
evidenceContexts,
|
|
),
|
|
];
|
|
unknownGraphInput.evidence_contexts = evidenceContexts.contexts;
|
|
expect(() => createJavaScriptSemanticGraph(unknownGraphInput)).toThrow(
|
|
/candidate node is absent/u,
|
|
);
|
|
});
|
|
|
|
it("validates evidence context ownership and references", () => {
|
|
const graph = semanticFixtureGraph();
|
|
const alteredContext = {
|
|
...graph,
|
|
evidence_contexts: graph.evidence_contexts.map((context, index) =>
|
|
index === 0
|
|
? { ...context, context_id: `jsrg_evidence_${"f".repeat(64)}` }
|
|
: context,
|
|
),
|
|
};
|
|
const alteredIssues = javaScriptSemanticGraphSchema.safeParse(alteredContext);
|
|
expect(alteredIssues.success).toBe(false);
|
|
if (alteredIssues.success) throw new Error("Expected invalid context digest");
|
|
expect(alteredIssues.error.issues.map(({ message }) => message)).toContain(
|
|
"Evidence context identifier is stale",
|
|
);
|
|
expect(alteredIssues.error.issues.map(({ message }) => message)).toContain(
|
|
"Evidence reference names an absent context",
|
|
);
|
|
|
|
const changedPayload = javaScriptSemanticGraphSchema.safeParse({
|
|
...graph,
|
|
evidence_contexts: graph.evidence_contexts.map((context, index) =>
|
|
index === 0 ? { ...context, authority: "user-assertion" } : context,
|
|
),
|
|
});
|
|
expect(changedPayload.success).toBe(false);
|
|
if (changedPayload.success)
|
|
throw new Error("Expected changed context payload rejection");
|
|
expect(changedPayload.error.issues.map(({ message }) => message)).toContain(
|
|
"Evidence context identifier is stale",
|
|
);
|
|
|
|
const withUnknown = semanticFixtureGraph(true);
|
|
const extraContext = withUnknown.evidence_contexts.find(
|
|
({ context_id }) =>
|
|
!graph.evidence_contexts.some(
|
|
(existing) => existing.context_id === context_id,
|
|
),
|
|
);
|
|
if (extraContext === undefined)
|
|
throw new Error("Expected the unknown fact to own a distinct context");
|
|
const orphanIssues = javaScriptSemanticGraphSchema.safeParse({
|
|
...graph,
|
|
evidence_contexts: [...graph.evidence_contexts, extraContext].toSorted(
|
|
(left, right) => left.context_id.localeCompare(right.context_id),
|
|
),
|
|
});
|
|
expect(orphanIssues.success).toBe(false);
|
|
if (orphanIssues.success)
|
|
throw new Error("Expected orphan context rejection");
|
|
expect(orphanIssues.error.issues.map(({ message }) => message)).toContain(
|
|
"Evidence context is not referenced by a graph fact",
|
|
);
|
|
|
|
for (const fact of [
|
|
"nodes",
|
|
"relations",
|
|
"unknowns",
|
|
"fingerprints",
|
|
] as const) {
|
|
const facts = withUnknown[fact];
|
|
const first = facts[0];
|
|
if (first === undefined) throw new Error(`Expected a ${fact} fact`);
|
|
const dangling = javaScriptSemanticGraphSchema.safeParse({
|
|
...withUnknown,
|
|
[fact]: [
|
|
{
|
|
...first,
|
|
evidence: {
|
|
...first.evidence,
|
|
context_id: `jsrg_evidence_${"e".repeat(64)}`,
|
|
},
|
|
},
|
|
...facts.slice(1),
|
|
],
|
|
});
|
|
expect(dangling.success, fact).toBe(false);
|
|
if (dangling.success)
|
|
throw new Error(`Expected dangling ${fact} reference`);
|
|
expect(
|
|
dangling.error.issues.map(({ message }) => message),
|
|
fact,
|
|
).toContain("Evidence reference names an absent context");
|
|
}
|
|
|
|
const observedNode = graph.nodes[0];
|
|
if (observedNode === undefined) throw new Error("Expected observed node");
|
|
const impossibleEvidence = javaScriptSemanticGraphSchema.safeParse({
|
|
...graph,
|
|
nodes: [
|
|
{
|
|
...observedNode,
|
|
evidence: {
|
|
...observedNode.evidence,
|
|
location: {
|
|
available: false,
|
|
reason: "not-observed",
|
|
detail: "Location removed from an observed fact.",
|
|
},
|
|
},
|
|
},
|
|
...graph.nodes.slice(1),
|
|
],
|
|
});
|
|
expect(impossibleEvidence.success).toBe(false);
|
|
if (impossibleEvidence.success)
|
|
throw new Error("Expected incompatible evidence context and location");
|
|
expect(
|
|
impossibleEvidence.error.issues.map(({ message }) => message),
|
|
).toContain("Evidence context is incompatible with its fact location");
|
|
});
|
|
|
|
it("shares provenance contexts while preserving fact-specific locations and unknown IDs", () => {
|
|
const graph = semanticFixtureGraph(true);
|
|
const first = graph.unknowns[0];
|
|
if (first === undefined) throw new Error("Expected an unknown frontier");
|
|
const evidenceContexts = registryForGraph(graph);
|
|
const { unknown_id: _unknownId, ...unknown } = first;
|
|
const second = createJavaScriptSemanticGraphUnknown(
|
|
{
|
|
...unknown,
|
|
evidence: {
|
|
...resolveJavaScriptSemanticEvidence(graph, first.evidence),
|
|
location: {
|
|
available: false,
|
|
reason: "unresolved",
|
|
detail: "Another source location remains unresolved.",
|
|
},
|
|
},
|
|
},
|
|
evidenceContexts,
|
|
);
|
|
expect(second.evidence.context_id).toBe(first.evidence.context_id);
|
|
expect(second.evidence.location).not.toEqual(first.evidence.location);
|
|
expect(second.unknown_id).not.toBe(first.unknown_id);
|
|
expect(evidenceContexts.contexts).toEqual(graph.evidence_contexts);
|
|
});
|
|
|
|
const complete = <Value>(steps: Iterator<void, Value>): Value => {
|
|
for (;;) {
|
|
const next = steps.next();
|
|
if (next.done) return next.value;
|
|
}
|
|
};
|
|
|
|
it("captures nested input before yielding and preserves the synchronous graph commitment", () => {
|
|
const input = semanticFixtureInput(true);
|
|
const node = input.nodes[0];
|
|
if (node === undefined) throw new Error("Expected fixture node");
|
|
const nested = { text: 'Unicode: \u{1f600} \ud800 " \\ ', rows: [1, 2, 3] };
|
|
node.properties = { nested, padding: "x".repeat(64_000) };
|
|
input.nodes.reverse();
|
|
input.relations.reverse();
|
|
input.coverage.families.reverse();
|
|
const expected = createJavaScriptSemanticGraph(input);
|
|
const steps = createImmutableJavaScriptSemanticGraphSteps(input);
|
|
expect(steps.next().done).toBe(false);
|
|
nested.text = "Changed while the owned graph is being hashed.";
|
|
nested.rows.push(4);
|
|
input.relations.length = 0;
|
|
input.coverage.families.length = 0;
|
|
const graph = complete(steps);
|
|
expect(graph).toEqual(expected);
|
|
expect(
|
|
parseJavaScriptSemanticGraph(
|
|
JSON.parse(serializeJavaScriptSemanticGraph(graph)),
|
|
),
|
|
).toEqual(expected);
|
|
expect(isValidatedImmutableJavaScriptSemanticGraph(graph)).toBe(true);
|
|
expect(isValidatedImmutableJavaScriptSemanticGraph(expected)).toBe(false);
|
|
expect(Object.isFrozen(nested)).toBe(false);
|
|
});
|
|
|
|
const invalidGraphCases: {
|
|
readonly label: string;
|
|
readonly change: (input: JavaScriptSemanticGraphInput) => void;
|
|
readonly message: string;
|
|
}[] = [
|
|
{
|
|
label: "node identity and ownership",
|
|
change: (input) => {
|
|
input.nodes = input.nodes.map((node, index) =>
|
|
index === 0
|
|
? {
|
|
...node,
|
|
node_id: `jsrg_node_${"f".repeat(64)}`,
|
|
function_node_id: `jsrg_node_${"e".repeat(64)}`,
|
|
}
|
|
: node,
|
|
);
|
|
},
|
|
message: "Node identifier is stale",
|
|
},
|
|
{
|
|
label: "relation endpoints and authority",
|
|
change: (input) => {
|
|
input.relations = input.relations.map((relation) => ({
|
|
...relation,
|
|
source_node_id: `jsrg_node_${"f".repeat(64)}`,
|
|
target_node_id: `jsrg_node_${"f".repeat(64)}`,
|
|
resolution: "candidate",
|
|
}));
|
|
},
|
|
message: "Relation endpoints must name semantic nodes",
|
|
},
|
|
{
|
|
label: "unknown identity and candidates",
|
|
change: (input) => {
|
|
input.unknowns = input.unknowns.map((unknown) => ({
|
|
...unknown,
|
|
candidate_node_ids: [`jsrg_node_${"f".repeat(64)}`],
|
|
}));
|
|
},
|
|
message: "Unknown frontier candidate node is absent",
|
|
},
|
|
{
|
|
label: "fingerprint identity and components",
|
|
change: (input) => {
|
|
input.fingerprints = input.fingerprints.map((fingerprint) => ({
|
|
...fingerprint,
|
|
function_node_id: `jsrg_node_${"f".repeat(64)}`,
|
|
fingerprint_sha256: "f".repeat(64),
|
|
components: { ...fingerprint.components, parameter_arity: 42 },
|
|
}));
|
|
},
|
|
message: "Fingerprint component commitment is stale",
|
|
},
|
|
{
|
|
label: "coverage completeness and references",
|
|
change: (input) => {
|
|
input.coverage.status = "complete";
|
|
input.coverage.families = input.coverage.families.map((family) => ({
|
|
...family,
|
|
retained_relations: family.retained_relations + 1,
|
|
unknown_ids: [`jsrg_unknown_${"f".repeat(64)}`],
|
|
}));
|
|
},
|
|
message: "Family retained relation count does not match graph content",
|
|
},
|
|
];
|
|
|
|
it.each(invalidGraphCases)(
|
|
"retains full validation diagnostics for $label in owned steps",
|
|
({ change, message }) => {
|
|
const input = semanticFixtureInput(true);
|
|
change(input);
|
|
const issues = (action: () => unknown) => {
|
|
try {
|
|
action();
|
|
} catch (error: unknown) {
|
|
if (error instanceof z.ZodError) return error.issues;
|
|
throw error;
|
|
}
|
|
throw new Error("Expected invalid graph to fail validation");
|
|
};
|
|
const expected = issues(() => createJavaScriptSemanticGraph(input));
|
|
expect(expected.map(({ message }) => message)).toContain(message);
|
|
const steps = createImmutableJavaScriptSemanticGraphSteps(input);
|
|
expect(steps.next().done).toBe(false);
|
|
expect(issues(() => complete(steps))).toEqual(expected);
|
|
},
|
|
);
|
|
|
|
it("interns each distinct evidence context once under its content digest", () => {
|
|
const registry = new JavaScriptSemanticEvidenceContextRegistry();
|
|
const elsewhere: ApplicationGraphEvidence = {
|
|
...semanticGraphTestEvidence(),
|
|
location: {
|
|
available: true,
|
|
value: {
|
|
kind: "source-range",
|
|
source: "other.js",
|
|
start: { line: 7, column: 2 },
|
|
end: { line: 7, column: 9 },
|
|
},
|
|
},
|
|
};
|
|
const first = registry.intern(semanticGraphTestEvidence());
|
|
const repeated = registry.intern(elsewhere);
|
|
const reordered = registry.intern({
|
|
...semanticGraphTestEvidence("inferred"),
|
|
limitations: [
|
|
"Static reachability does not prove runtime execution.",
|
|
"Static reachability does not prove runtime execution.",
|
|
],
|
|
});
|
|
const inferred = registry.intern(semanticGraphTestEvidence("inferred"));
|
|
const { location: _location, ...context } = semanticGraphTestEvidence();
|
|
|
|
expect(repeated.context_id).toBe(first.context_id);
|
|
expect(repeated.location).toEqual(elsewhere.location);
|
|
expect(reordered.context_id).toBe(inferred.context_id);
|
|
expect(inferred.context_id).not.toBe(first.context_id);
|
|
expect(first.context_id).toBe(
|
|
`jsrg_evidence_${digestCanonicalValue(context, "test")}`,
|
|
);
|
|
expect(registry.contexts.map(({ context_id }) => context_id)).toEqual(
|
|
[first.context_id, inferred.context_id].toSorted(),
|
|
);
|
|
expect(registry.resolve(repeated)).toEqual(elsewhere);
|
|
});
|