import { expect, it } from "vitest"; import { buildJavaScriptSemanticGraph } from "./JavaScriptSemanticGraphBuilder.js"; import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js"; import type { JavaScriptArtifactFile } from "./JavaScriptArtifactFiles.js"; import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js"; import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js"; const SHA256 = "a".repeat(64); const GRAPH_ID = `jag_${"b".repeat(64)}`; it("projects literal events and exact timer cancellation", () => { const graph = graphFor(` function run(bus, dynamicName) { const handle = setInterval(tick, 10); bus.on("ready", handler); bus.once(dynamicName, handler); bus.emit("ready"); bus.removeListener("ready", handler); clearInterval(handle); } `); const relations = new Set(graph.relations.map(({ relation }) => relation)); for (const expected of [ "cancels-timer", "dispatches-candidate", "registers-listener", "removes-listener", "schedules-timer", ] as const) expect(relations.has(expected)).toBe(true); expect( graph.nodes.find( ({ kind, properties }) => kind === "event" && properties.event_name === "ready", ), ).toBeDefined(); expect( graph.unknowns.some( ({ family, detail }) => family === "event" && detail.includes("Dynamic once event name"), ), ).toBe(true); expect( graph.coverage.families.find(({ family }) => family === "event"), ).toMatchObject({ status: "partial" }); expect( graph.coverage.families.find(({ family }) => family === "timer"), ).toMatchObject({ status: "partial" }); }); it("projects child-process creation, I/O, listeners, and signals", () => { const graph = graphFor(` import { spawn } from "node:child_process"; function run(command) { const child = spawn( command, ["--mode", "fast"], { env: process.env, stdio: "pipe" }, ); child.on("exit", onExit); child.once("error", onError); child.kill("SIGTERM"); } `); const relations = new Set(graph.relations.map(({ relation }) => relation)); for (const expected of [ "connects-stdio", "forwards-signal", "listens-error", "listens-exit", "spawns", "supplies-argv", "supplies-env", ] as const) expect(relations.has(expected)).toBe(true); expect( graph.nodes.find( ({ kind, properties }) => kind === "child-process" && properties.method === "spawn" && properties.command === null, ), ).toBeDefined(); expect( graph.unknowns.some( ({ family, detail }) => family === "child-process" && detail.includes("command remains unresolved"), ), ).toBe(true); expect( graph.coverage.families.find(({ family }) => family === "child-process"), ).toMatchObject({ status: "partial" }); }); it("keeps destructured request, spawn, and timer results as projections", () => { const graph = graphFor(` import { spawn } from "node:child_process"; async function run() { const { data } = await fetch("https://example.test"); const [ child ] = spawn("worker", []); child.on("exit", done); const { handle } = setTimeout(done, 5); clearTimeout(handle); return data.json(); } `); const projections = graph.relations.filter( ({ relation }) => relation === "destructures", ); expect( projections.map(({ properties }) => properties.projection_path), ).toEqual(expect.arrayContaining([["data"], [0], ["handle"]])); expect( projections.every( ({ properties }) => properties.projection_resolution === "complete", ), ).toBe(true); expect( graph.relations.find( ({ relation, properties }) => relation === "consumed-by" && properties.method === "json", ), ).toMatchObject({ resolution: "candidate" }); expect( graph.relations.find(({ relation }) => relation === "listens-exit"), ).toMatchObject({ resolution: "candidate" }); expect( graph.relations.find(({ relation }) => relation === "cancels-timer"), ).toMatchObject({ resolution: "candidate" }); expect( graph.relations.some( ({ relation, resolution }) => ["consumed-by", "listens-exit", "cancels-timer"].includes(relation) && resolution === "resolved", ), ).toBe(false); }); it("does not alias nested call arguments to an outer assigned result", () => { const graph = graphFor(` import { spawn } from "node:child_process"; function run() { const child = wrap(spawn("worker", [])); child.on("exit", done); const handle = String(setTimeout(done, 5)); clearTimeout(handle); } `); expect( graph.relations.some(({ relation }) => ["listens-exit", "cancels-timer", "destructures"].includes(relation), ), ).toBe(false); }); it("projects config precedence, request fields, and boundaries", () => { const graph = graphFor(` import { readFileSync } from "node:fs"; async function run(schema) { const endpoint = process.env.API_URL ?? "https://fallback.test"; const raw = readFileSync("./config.json", "utf8"); const parsed = JSON.parse(raw); const port = Number(process.argv[2]); const validated = schema.parse(parsed); const response = await fetch(endpoint, { method: "POST", body: validated, }); return response.json(); } `); const relations = new Set(graph.relations.map(({ relation }) => relation)); for (const expected of [ "coerces", "constructs-request", "consumed-by", "defaults", "overrides", "parses", "reads-argv", "reads-config", "reads-environment", "supplies-request-field", ] as const) expect(relations.has(expected)).toBe(true); expect( graph.nodes.find( ({ kind, properties }) => kind === "request" && properties.method === "fetch", ), ).toBeDefined(); expect( graph.nodes.filter(({ kind }) => kind === "boundary"), ).not.toHaveLength(0); expect( graph.coverage.families.find(({ family }) => family === "configuration"), ).toMatchObject({ status: "partial" }); expect( graph.coverage.families.find(({ family }) => family === "request"), ).toMatchObject({ status: "partial" }); expect( graph.coverage.families.find(({ family }) => family === "boundary"), ).toMatchObject({ status: "partial" }); }); it("projects built-in resource acquisition and release", () => { const graph = graphFor(` import { open } from "node:fs/promises"; import { connect } from "node:net"; async function run(path) { const file = await open(path); const socket = connect({ port: 9000 }); await file.close(); socket.end(); } `); const relations = new Set(graph.relations.map(({ relation }) => relation)); expect(relations.has("acquires")).toBe(true); expect(relations.has("releases")).toBe(true); expect(graph.nodes.filter(({ kind }) => kind === "resource")).toHaveLength(2); expect( graph.coverage.families.find( ({ family }) => family === "resource-lifecycle", ), ).toMatchObject({ status: "partial" }); }); it.each(["+= 1", "++"])( "projects both property dependencies for %s", (operator) => { const graph = graphFor(` const source = { count: 1 }; source.count ${operator}; `); const relations = graph.relations.map(({ relation }) => relation); expect(relations).toContain("reads-property"); expect(relations).toContain("writes-property"); }, ); it("does not project a literal property slot for dynamic destructuring", () => { const graph = graphFor(` const source = { token: "TOKEN" }; const key = getKey(); const { [key]: value } = source; `); expect( graph.relations.filter(({ relation }) => relation === "destructures"), ).toEqual([]); expect( graph.nodes.find( ({ kind, properties }) => kind === "property-slot" && properties.name === "key", ), ).toBeUndefined(); }); it("projects literal seeds and static object flow", () => { const graph = graphFor(` const source = { token: "TOKEN", count: 1 }; const { token } = source; const copy = { ...source }; source.count = 2; const read = source.token; `); const relations = new Set(graph.relations.map(({ relation }) => relation)); for (const expected of [ "destructures", "reads-property", "spreads", "writes-property", ] as const) expect(relations.has(expected)).toBe(true); expect( graph.nodes.find( ({ kind, properties }) => kind === "property-slot" && properties.name === "token", ), ).toBeDefined(); const literal = queryJavaScriptSemanticGraph(graph, { seed: { kind: "literal", value: "TOKEN" }, direction: "forward-influence", }); expect(literal.status).toBe("ambiguous"); expect(literal.summary.total_seed_matches).toBe(3); const property = queryJavaScriptSemanticGraph(graph, { seed: { kind: "property", name: "token" }, direction: "forward-influence", }); expect(property.status).toBe("found"); }); it("does not promote transformed return expressions to resolved aliases", () => { const graph = graphFor(` function increment(value) { return value + 1; } const input = 1; const output = increment(input); `); const returnNode = graph.nodes.find(({ kind }) => kind === "return-site"); if (returnNode === undefined) throw new Error("Expected return site"); expect( graph.relations.filter( ({ relation, target_node_id }) => relation === "aliases" && target_node_id === returnNode.node_id, ), ).toEqual([expect.objectContaining({ resolution: "candidate" })]); const output = graph.nodes.find( ({ kind, label }) => kind === "binding" && label === "output", ); const input = graph.nodes.find( ({ kind, label }) => kind === "binding" && label === "input", ); if (output === undefined || input === undefined) throw new Error("Expected caller bindings"); const strict = queryJavaScriptSemanticGraph(graph, { seed: { kind: "semantic-node", node_id: output.node_id }, direction: "backward-provenance", }); expect(strict.nodes.map(({ node_id }) => node_id)).not.toContain( input.node_id, ); expect(strict.status).toBe("ambiguous"); }); it("uses unavailable evidence when no source produced semantic IR", () => { const graph = buildJavaScriptSemanticGraph({ rootArtifactSha256: SHA256, applicationGraph: { graph_id: GRAPH_ID, nodes: [] }, analysis: emptyAnalysis(), }); expect(graph.nodes[0]?.evidence).toMatchObject({ authority: "unknown", state: "unavailable", location: { available: false, reason: "not-observed" }, }); }); const emptyAnalysis = (): JavaScriptArtifactAnalysis => ({ files: [], packages: [], json_modules: [], html_scripts: [], source_maps: [], visited_ast_nodes: 0, findings: 0, modules: 0, parse_failures: 0, truncated_scopes: 0, limitations: [], }); const graphFor = (source: string) => { const file: JavaScriptArtifactFile = { path: "app.js", container_sha256: SHA256, sha256: SHA256, bytes: Buffer.byteLength(source), inventory_artifact_id: `art_${SHA256}`, kind: "javascript", unpacked: false, text: { included: true, value: source }, }; const analysis: JavaScriptArtifactAnalysis = { files: [ { file, javascript: null, semantic: { ir: analyzeJavaScriptSemantics(source), }, }, ], packages: [], json_modules: [], html_scripts: [], source_maps: [], visited_ast_nodes: 0, findings: 0, modules: 0, parse_failures: 0, truncated_scopes: 0, limitations: [], }; return buildJavaScriptSemanticGraph({ rootArtifactSha256: SHA256, applicationGraph: { graph_id: GRAPH_ID, nodes: [] }, analysis, }); };