test: classify filesystem application workflows as boundaries

This commit is contained in:
morluto
2026-10-12 04:53:10 +08:00
parent 918713af37
commit 804c287e44
11 changed files with 71 additions and 69 deletions
@@ -1,366 +0,0 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Ajv2020 } from "ajv/dist/2020.js";
import { expect, it } from "vitest";
import { z } from "zod";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { matchJavaScriptApplicationVersions } from "../../domain/javascript/javascriptApplicationVersionKeys.js";
import { findApplicationFeatureSeeds } from "../../domain/javascript/javascriptFeatureSeed.js";
import { traceApplicationFeatureInputSchema } from "../../domain/javascript/javascriptFeatureTraceSchemas.js";
import { analyzeJavaScriptApplication } from "../../../tests/support/javascriptApplicationScope.js";
const analyzeSource = async (
source: string,
extraFiles: Readonly<Record<string, string>> = {},
) => {
const inputPath = await createTestTempDirectory("rea-js-empty-literal-");
await writeFile(join(inputPath, "app.js"), `${source}\n`);
for (const [path, text] of Object.entries(extraFiles))
await writeFile(join(inputPath, path), text);
const result = await analyzeJavaScriptApplication({
input_path: inputPath,
format: "directory",
});
if (!result.ok)
throw new Error(`Expected analysis success: ${result.error.message}`);
return javascriptApplicationAnalysisResultSchema.parse(
result.value.normalized_result,
);
};
it("retains empty literal evidence across the application boundary surfaces", async () => {
const sources = [
"fetch('');",
"new WebSocket('');",
"navigator.serviceWorker.register('');",
"new Worker('');",
"const x = require('');",
"import y from ''; y();",
"import('');",
"localStorage.getItem('');",
"localStorage.setItem('', 1);",
"indexedDB.open('');",
"win.loadFile('');",
"win.loadURL('');",
"app.get('', handler);",
"process.on('', () => 1);",
"const { EventEmitter } = require('events'); new EventEmitter().on('', () => 1);",
"const { spawn } = require('child_process'); spawn('/bin/x').on('', () => 1);",
"const { ipcMain } = require('electron'); ipcMain.handle('', () => 1);",
"const { ipcRenderer } = require('electron'); ipcRenderer.invoke('');",
"const { contextBridge } = require('electron'); contextBridge.exposeInMainWorld('', {});",
];
const output = await analyzeSource(sources.join("\n"));
expect(output.graph.nodes).toEqual(
expect.arrayContaining([
expect.objectContaining({ kind: "endpoint" }),
expect.objectContaining({ kind: "worker" }),
expect.objectContaining({ kind: "ipc-channel" }),
expect.objectContaining({ kind: "context-bridge-api" }),
]),
);
expect(output.semantic_graph?.nodes).toEqual(
expect.arrayContaining([
expect.objectContaining({
kind: "request",
properties: expect.objectContaining({ endpoint: "" }),
}),
expect.objectContaining({
kind: "event",
properties: expect.objectContaining({ event_name: "" }),
}),
]),
);
});
it("analyzes a plain long-line loader without package metadata", async () => {
const output = await analyzeSource(
`var Module = {}; fetch(""); new Worker(""); var payload = "${"x".repeat(65_536)}";`,
{ "game-data.js": "Module.data = { ready: true };" },
);
const nodes = output.graph.nodes.filter(
({ kind }) => kind === "endpoint" || kind === "worker",
);
expect(nodes.map(({ kind }) => kind).sort()).toEqual(["endpoint", "worker"]);
for (const node of nodes) {
expect(node.identity).toMatchObject({
strategy: "artifact-local-key",
key: "",
});
expect(node.observations.map(({ label }) => label)).toEqual([null]);
}
});
it("keeps exact values for empty literals without synthetic application labels", async () => {
const output = await analyzeSource(
"fetch(''); fetch('\"\"'); fetch('/api'); process.on('', () => 1);",
);
const endpoints = output.graph.nodes
.filter(({ kind }) => kind === "endpoint")
.map(({ identity, observations }) => ({
key: identity.strategy === "artifact-local-key" ? identity.key : null,
labels: observations.map(({ label }) => label),
values: observations.map(({ properties }) => properties.value),
}));
// The empty endpoint has no display label, so it cannot be confused with
// a literal two-quote endpoint; both keep exact identities.
expect(endpoints).toEqual(
expect.arrayContaining([
{ key: "", labels: [null], values: [""] },
{ key: '""', labels: ['""'], values: ['""'] },
{ key: "/api", labels: ["/api"], values: ["/api"] },
]),
);
expect(endpoints).toHaveLength(3);
expect(
output.semantic_graph?.nodes
.filter(({ kind }) => kind === "event")
.map(({ label, properties }) => ({
label,
eventName: properties.event_name,
})),
).toEqual([{ label: '""', eventName: "" }]);
});
it.each([
{ version: 3, sources: [""], names: [], mappings: "AAAA" },
{ version: 3, sourceRoot: null, sources: [""], names: [], mappings: "AAAA" },
{ version: 3, sourceRoot: "", sources: [""], names: [], mappings: "AAAA" },
{
version: 3,
sections: [
{
offset: { line: 0, column: 0 },
map: { version: 3, sources: [""], names: [], mappings: "AAAA" },
},
],
},
])("projects an empty source-map source name: %j", async (map) => {
const output = await analyzeSource("//# sourceMappingURL=app.js.map", {
"app.js.map": JSON.stringify(map),
});
const originals = output.graph.nodes
.filter(({ kind }) => kind === "source-module")
.map(({ identity, observations }) => ({
originalSource:
identity.strategy === "source-map-original"
? identity.original_source
: null,
sources: observations.map(
({ source_map_reference }) => source_map_reference?.source_name,
),
}));
expect(originals).toEqual([{ originalSource: "", sources: [""] }]);
});
it("keeps empty and two-quote source-map names distinct", async () => {
const output = await analyzeSource("//# sourceMappingURL=app.js.map", {
"app.js.map": JSON.stringify({
version: 3,
sources: ["", '""'],
names: [],
mappings: "AAAA,CCAA",
}),
});
expect(
output.graph.nodes
.filter(({ kind }) => kind === "source-module")
.map(({ identity }) =>
identity.strategy === "source-map-original"
? identity.original_source
: null,
)
.sort(),
).toEqual(["", '""']);
});
it("does not match an empty endpoint with a literal two-quote seed", async () => {
const output = await analyzeSource("fetch(''); fetch('\"\"');");
const endpoints = new Map(
output.graph.nodes
.filter(({ kind }) => kind === "endpoint")
.map((node) => [
node.node_id,
node.identity.strategy === "artifact-local-key"
? node.identity.key
: null,
]),
);
const matches = findApplicationFeatureSeeds(output.graph.nodes, {
kind: "string",
value: '""',
match: "exact",
case_sensitive: true,
}).filter(({ node_id: nodeId }) => endpoints.has(nodeId));
expect(matches.map(({ node_id: nodeId }) => endpoints.get(nodeId))).toEqual([
'""',
]);
});
const IPC_SOURCE =
"const { ipcMain } = require('electron'); ipcMain.handle('', () => 1); ipcMain.handle('\"\"', () => 2);";
const ipcChannels = (
nodes: Awaited<ReturnType<typeof analyzeSource>>["graph"]["nodes"],
) =>
new Map(
nodes
.filter(({ kind }) => kind === "ipc-channel")
.map((node) => [node.node_id, node.observations[0]?.properties.channel]),
);
it("selects an empty IPC channel with an exact empty seed", async () => {
const output = await analyzeSource(IPC_SOURCE);
const channels = ipcChannels(output.graph.nodes);
const matches = findApplicationFeatureSeeds(output.graph.nodes, {
kind: "channel",
value: "",
match: "exact",
case_sensitive: true,
});
expect(matches.map(({ node_id: nodeId }) => channels.get(nodeId))).toEqual([
"",
]);
});
const seedSchema = traceApplicationFeatureInputSchema.shape.seed;
const validateAdvertisedSeed = new Ajv2020({
strict: false,
validateFormats: false,
}).compile(z.toJSONSchema(seedSchema, { io: "input" }));
it.each([
[{ kind: "channel", value: "", match: "exact" }, true],
[{ kind: "string", value: "", match: "exact" }, true],
[{ kind: "channel", value: "/x" }, true],
[{ kind: "string", value: "" }, false],
[{ kind: "channel", value: "" }, false],
[{ kind: "channel", value: "", match: "contains" }, false],
[{ kind: "node-id", value: "", match: "exact" }, false],
] as const)(
"validates empty seed %j at runtime and in the advertised schema",
(seed, accepted) => {
expect(seedSchema.safeParse(seed).success).toBe(accepted);
expect(validateAdvertisedSeed(seed)).toBe(accepted);
},
);
it("pairs the same empty IPC channel across application versions", async () => {
const left = await analyzeSource(IPC_SOURCE);
const right = await analyzeSource(`${IPC_SOURCE} // v2`);
const leftChannels = ipcChannels(left.graph.nodes);
const rightChannels = ipcChannels(right.graph.nodes);
const matching = matchJavaScriptApplicationVersions(
left.graph.nodes,
right.graph.nodes,
);
expect(
matching.pairs
.filter(({ left: node }) => node.kind === "ipc-channel")
.map(({ left: leftNode, right: rightNode }) => [
leftChannels.get(leftNode.node_id),
rightChannels.get(rightNode.node_id),
]),
).toEqual(
expect.arrayContaining([
["", ""],
['""', '""'],
]),
);
expect(matching.unmatchedLeft.map(({ kind }) => kind)).not.toContain(
"ipc-channel",
);
});
it("selects an empty context-bridge API with an exact empty seed", async () => {
const output = await analyzeSource(
"const { contextBridge } = require('electron'); contextBridge.exposeInMainWorld('', {}); contextBridge.exposeInMainWorld('\"\"', {});",
);
const apis = new Map(
output.graph.nodes
.filter(({ kind }) => kind === "context-bridge-api")
.map((node) => [node.node_id, node.observations[0]?.properties.api_key]),
);
const matches = findApplicationFeatureSeeds(output.graph.nodes, {
kind: "api",
value: "",
match: "exact",
case_sensitive: true,
});
expect(matches.map(({ node_id: nodeId }) => apis.get(nodeId))).toEqual([""]);
});
it("pairs indexed source-map leaves by raw source name and root", async () => {
const map = (value: number) =>
JSON.stringify({
version: 3,
sections: ["one", "two"].map((name, line) => ({
offset: { line, column: 0 },
map: {
version: 3,
sourceRoot: `../${name}`,
sources: ["same.js"],
sourcesContent: [`export const value = ${String(value)};`],
names: [],
mappings: "AAAA",
},
})),
});
const left = await analyzeSource("//# sourceMappingURL=app.js.map", {
"app.js.map": map(1),
});
const right = await analyzeSource("//# sourceMappingURL=app.js.map", {
"app.js.map": map(2),
});
const matching = matchJavaScriptApplicationVersions(
left.graph.nodes,
right.graph.nodes,
);
const originals = matching.pairs.filter(
({ left: node }) => node.kind === "source-module",
);
expect(originals).toHaveLength(2);
for (const pair of originals) {
expect(pair).toMatchObject({
basis: "source-map-identity",
confidence: "high",
});
expect(pair.left.identity).toMatchObject({
original_source: "same.js",
source_root: expect.any(String),
});
if (pair.left.identity.strategy !== "source-map-original")
throw new Error("Expected source identity");
expect(pair.right.identity).toMatchObject({
source_root: pair.left.identity.source_root,
});
}
});
it("reports malformed source roots without manufacturing an original path", async () => {
const output = await analyzeSource("//# sourceMappingURL=app.js.map", {
"app.js.map": JSON.stringify({
version: 3,
sourceRoot: 42,
sources: ["a.js"],
names: [],
mappings: "AAAA",
}),
});
expect(
output.graph.nodes.filter(({ kind }) => kind === "source-module"),
).toEqual([]);
expect(
output.graph.nodes.flatMap(({ observations }) => observations),
).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
limitation: "Source map sourceRoot must be a string or null.",
}),
evidence: expect.objectContaining({ state: "unavailable" }),
}),
);
});
@@ -1,70 +0,0 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { analyzeJavaScriptApplication } from "../../../tests/support/javascriptApplicationScope.js";
it.each([
{
expression:
'flag ? require("./dependency.js").value : require("./dependency.js").value',
resolved: true,
},
{
expression:
'require("./dependency.js").value || require("./dependency.js").value',
resolved: true,
},
{
expression: 'flag ? require("./dependency.js").value : localValue',
resolved: false,
},
{
expression: 'require("./dependency.js").value ?? localValue',
resolved: false,
},
])(
"preserves export provenance for $expression",
async ({ expression, resolved }) => {
const root = await createTestTempDirectory("rea-js-module-provenance-");
await Promise.all([
writeFile(
join(root, "app.js"),
`const selected = ${expression};\nexport { selected };\n`,
),
writeFile(join(root, "dependency.js"), "exports.value = 1;\n"),
]);
const result = await analyzeJavaScriptApplication({
input_path: root,
format: "directory",
});
if (!result.ok) throw result.error;
const analysis = javascriptApplicationAnalysisResultSchema.parse(
JSON.parse(JSON.stringify(result.value.normalized_result)),
);
const exportImports = analysis.graph.edges.filter(
({ relation, properties }) =>
relation === "imports" && properties.exported_name === "selected",
);
if (!resolved) {
expect(exportImports).toEqual([]);
return;
}
expect(exportImports).toContainEqual(
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
specifier: "./dependency.js",
imported_path: ["value"],
resolved_path: "dependency.js",
resolution_status: "resolved",
}),
}),
);
},
);
@@ -1,302 +0,0 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { parseEvidence } from "../../domain/evidence.js";
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { writeJavaScriptSourceSelectionFixture } from "../../../tests/fixtures/javascriptSourceSelection.js";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import { scanCanonicalArtifactInventory } from "../../../tests/fixtures/artifactInventory.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { analyzeJavaScriptApplication } from "../../../tests/support/javascriptApplicationScope.js";
describe("Selected JavaScript source failure diagnostics", () => {
it("analyzes selected files in first-request order and reports only selected progress", async () => {
const root = await createTestTempDirectory("rea-js-selected-priority-");
await writeJavaScriptSourceSelectionFixture(root);
await writeFile(join(root, "z-later.mjs"), "export const later = 1;\n");
const progress: string[] = [];
const result = await analyzeJavaScriptApplication(
{
input_path: root,
source_paths: ["z-later.mjs", "main.mjs", "z-later.mjs"],
},
{
progress: {
report: async (event) => {
if (event.phase === "parse_javascript_source")
progress.push(event.message);
},
},
},
);
expect(result.ok).toBe(true);
expect(progress).toEqual([
"Parsing and projecting z-later.mjs (1/2)",
"Parsing and projecting main.mjs (2/2)",
]);
});
it("verifies unselected bytes against the inventoried identity without decoding them", async () => {
const root = await createTestTempDirectory("rea-js-unselected-integrity-");
await writeJavaScriptSourceSelectionFixture(root);
const result = await analyzeJavaScriptApplication(
{ input_path: root, source_paths: ["main.mjs"] },
{
progress: {
report: async (event) => {
if (event.phase === "read_javascript_artifacts")
await writeFile(
join(root, "vendor.js"),
"changed after inventory\n",
);
},
},
},
);
if (result.ok)
throw new Error(
"Unselected content changes must still fail integrity verification",
);
expect(projectAnalysisError(result.error)).toMatchObject({
code: "artifact_operation_failed",
details: { reason: "integrity" },
});
});
it("retains selected facts and unselected scope after cancellation between selected sources", async () => {
const root = await createTestTempDirectory("rea-js-selected-cancellation-");
await writeJavaScriptSourceSelectionFixture(root);
await writeFile(join(root, "z-later.mjs"), "export const later = 1;\n");
const controller = new AbortController();
const source_paths = ["main.mjs", "z-later.mjs"];
const result = await analyzeJavaScriptApplication(
{ input_path: root, source_paths },
{
signal: controller.signal,
progress: {
report: async (event) => {
if (
event.phase === "parse_javascript_source" &&
event.message.includes("z-later.mjs")
)
controller.abort();
},
},
},
);
if (result.ok) throw new Error("Expected selected-source cancellation");
expect(projectAnalysisError(result.error).code).toBe("cancelled");
const evidence = parseEvidence(result.error.partialObservation);
const partial = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
expect(partial.source_selection).toEqual({
requested_paths: source_paths,
unselected_files: 5,
});
expect(
partial.semantic_graph.nodes.some(
({ kind, label }) => kind === "function" && label === "selected",
),
).toBe(true);
expect(partial.statistics.parsed_javascript_files).toBe(1);
expect(partial.graph.coverage.status).toBe("partial");
expect(
partial.graph.nodes
.flatMap(({ observations }) =>
observations.map(({ properties }) => properties),
)
.filter(
(properties) => properties.text_status === "outside-analysis-scope",
),
).toHaveLength(5);
});
});
describe("JavaScript application failure diagnostics", () => {
it("identifies the rejected result field in caller-visible diagnostics", async () => {
const inputPath = await createTestTempDirectory("rea-js-schema-failure-");
await writeFile(join(inputPath, "main.js"), "export const value = 1;\n");
const cause = new z.ZodError([
{
code: "custom",
path: ["semantic_graph", "nodes", 42, "application_node_ids", 0],
message: "Semantic node references an absent application node",
},
]);
const result = await analyzeJavaScriptApplication(
{ input_path: inputPath, format: "directory" },
{
progress: {
report: async (event) => {
if (event.terminal) throw cause;
},
},
},
);
if (result.ok) throw new Error("Expected analysis failure");
expect(result.error.cause).toBe(cause);
expect(projectAnalysisError(result.error)).toMatchObject({
code: "unreadable_output",
details: {
operation: "analyze_javascript_application",
reason:
"Result schema rejected 1 issue at /semantic_graph/nodes/42/application_node_ids/0 (custom)",
},
});
});
it("retains unexpected failures and their input identity in caller-visible diagnostics", async () => {
const inputPath = await createTestTempDirectory("rea-js-failure-");
await writeFile(join(inputPath, "main.js"), "export const value = 1;\n");
const cause = new RangeError("Invalid string length");
// Retain the original failure locally, but never serialize its object graph.
cause.cause = cause;
const result = await analyzeJavaScriptApplication(
{ input_path: inputPath, format: "directory" },
{
progress: {
report: async (event) => {
if (event.terminal) throw cause;
},
},
},
);
if (result.ok) throw new Error("Expected analysis failure");
expect(result.error.cause).toBe(cause);
expect(projectAnalysisError(result.error)).toMatchObject({
code: "execution_failure",
details: {
provider_id: "rea-javascript-application",
operation: "analyze_javascript_application",
diagnostics: {
input_path: inputPath,
error_name: "RangeError",
error_message: "Invalid string length",
},
},
});
const projection = projectAnalysisError(result.error);
expect(JSON.parse(JSON.stringify(projection))).toEqual(projection);
});
it("reports a missing selection as invalid input, not an engine failure", async () => {
const root = await createTestTempDirectory("rea-js-missing-");
const result = await analyzeJavaScriptApplication({
input_path: join(root, "missing"),
format: "directory",
});
if (result.ok) throw new Error("Expected missing artifact failure");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "invalid_request",
details: {
operation: "analyze_javascript_application",
issues: [{ path: ["input_path"], reason: "invalid_value" }],
},
});
});
it.each(["reported failure", null])(
"retains non-Error failures (%s)",
async (cause) => {
const inputPath = await createTestTempDirectory("rea-js-non-error-");
await writeFile(join(inputPath, "main.js"), "export const value = 1;\n");
const result = await analyzeJavaScriptApplication(
{ input_path: inputPath, format: "directory" },
{
progress: {
report: async (event) => {
if (event.terminal) throw cause;
},
},
},
);
if (result.ok) throw new Error("Expected analysis failure");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "execution_failure",
details: {
diagnostics: {
error_name: "UnknownError",
error_message:
typeof cause === "string"
? cause
: "JavaScript analysis failed with a non-Error value",
},
},
});
},
);
it.each([new Error("x".repeat(10_000)), "y".repeat(10_000)])(
"preserves long failure messages through JSON projection (%#)",
async (cause) => {
const inputPath = await createTestTempDirectory("rea-js-oversized-");
await writeFile(join(inputPath, "main.js"), "export const value = 1;\n");
const message = cause instanceof Error ? cause.message : cause;
const result = await analyzeJavaScriptApplication(
{ input_path: inputPath, format: "directory" },
{
progress: {
report: async (event) => {
if (event.terminal) throw cause;
},
},
},
);
if (result.ok) throw new Error("Expected analysis failure");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "execution_failure",
details: {
diagnostics: {
error_message: message,
},
},
});
const projection = projectAnalysisError(result.error);
expect(JSON.parse(JSON.stringify(projection))).toEqual(projection);
},
);
});
describe("JavaScript application cleanup diagnostics", () => {
it("preserves cleanup and a complete inventory when translating reader failures", async () => {
const inputPath = await createTestTempDirectory("rea-js-cleanup-failure-");
await writeFile(join(inputPath, "main.js"), "export const value = 1;\n");
const inventory = await scanCanonicalArtifactInventory(inputPath);
const cleanup = {
reason: "reader remains open",
resources: [`JavaScript artifact reader for ${inputPath}`],
};
const partialObservation = {
kind: "artifact-inventory" as const,
inventory,
};
const failure = new ArtifactReaderFailure("io", "Reader cleanup failed", {
cleanup,
partialObservation,
});
const result = await analyzeJavaScriptApplication(
{ input_path: inputPath, format: "directory" },
{
progress: {
report: async () => {
throw failure;
},
},
},
);
if (result.ok) throw new Error("Expected reader failure");
expect(result.error).toMatchObject({
_tag: "ArtifactOperationError",
cleanup,
partialObservation,
});
});
});
@@ -1,70 +0,0 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { javaScriptSemanticQueryResultSchema } from "../../domain/javascript/javascriptSemanticQuerySchemas.js";
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
import { analyzeJavaScriptApplication } from "../../../tests/support/javascriptApplicationScope.js";
it.each([
{
source: "export const value = -0;\n",
target: { kind: "binding", label: "value" },
},
{
source: "export const root = { value: -0 };\n",
target: { kind: "property-slot", propertyPointer: "/value" },
},
])(
"keeps negative-zero value unknown in an exported query (%#)",
async ({ source, target }) => {
const root = await createTestTempDirectory("rea-signed-zero-");
await writeFile(join(root, "app.js"), source);
const result = await analyzeJavaScriptApplication({
input_path: root,
format: "directory",
});
if (!result.ok) throw result.error;
const analysis = javascriptApplicationAnalysisResultSchema.parse(
result.value.normalized_result,
);
const node =
target.kind === "binding"
? analysis.semantic_graph.nodes.find(
({ kind, label }) => kind === "binding" && label === target.label,
)
: analysis.semantic_graph.nodes.find(
({ kind, properties }) =>
kind === "property-slot" &&
properties.property_pointer === target.propertyPointer,
);
if (node === undefined)
throw new Error("Expected the selected semantic node");
const query = queryJavaScriptSemanticGraph(analysis.semantic_graph, {
seed: { kind: "semantic-node", node_id: node.node_id },
direction: "backward-provenance",
allowed_relations: ["defines"],
});
const transportedQuery = javaScriptSemanticQueryResultSchema.parse(
JSON.parse(JSON.stringify(query)),
);
expect(transportedQuery.nodes.some(({ kind }) => kind === "literal")).toBe(
false,
);
expect(transportedQuery.coverage.status).toBe("partial");
expect(transportedQuery.unknowns).toEqual(
expect.arrayContaining([
expect.objectContaining({
reason: "unknown-value",
node_id: node.node_id,
detail: expect.stringContaining(
"Negative zero cannot be preserved by JSON primitive evidence.",
),
}),
]),
);
},
);
@@ -1,73 +0,0 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { analyzeJavaScriptApplication } from "../../../tests/support/javascriptApplicationScope.js";
const graphIdentities = async (source: string) => {
const inputPath = await createTestTempDirectory("rea-js-template-literal-");
await writeFile(join(inputPath, "app.js"), `${source}\n`);
await writeFile(join(inputPath, "dep.js"), "module.exports = 1;\n");
await writeFile(join(inputPath, "worker.js"), "self.onmessage = null;\n");
const result = await analyzeJavaScriptApplication({
input_path: inputPath,
format: "directory",
});
if (!result.ok)
throw new Error(`Expected analysis success: ${result.error.message}`);
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
result.value.normalized_result,
);
return (
graph.nodes
.filter(({ kind }) => kind !== "javascript-asset" && kind !== "artifact")
// Artifact digests differ between spellings; keep the derived keys.
.map(({ kind, identity }) => ({
kind,
strategy: identity.strategy,
...(identity.strategy === "artifact-local-key"
? {
namespace: identity.namespace,
// Module-export identity is a JSON tuple whose first entry
// binds the whole container; only that digest changes here.
key:
identity.namespace === "module-export"
? JSON.stringify(
(JSON.parse(identity.key) as unknown[]).slice(1),
)
: identity.namespace === "source-module"
? identity.key.replace(/^[0-9a-f]{64}:/u, "")
: identity.key,
}
: {}),
}))
.sort((left, right) =>
JSON.stringify(left).localeCompare(JSON.stringify(right)),
)
);
};
// Each quoted literal has the same length as its template spelling, so
// source locations and every derived identity stay comparable.
it.each([
"fetch('https://api.example.com/v1/users');",
"localStorage.setItem('token', 1);",
"new Worker('./worker.js');",
"const dep = require('./dep.js'); dep();",
"import('./dep.js');",
])(
"reads a template without substitutions as its exact string: %s",
async (quoted) => {
const template = quoted.replaceAll("'", "`");
const expected = await graphIdentities(quoted);
expect(
expected.some(({ kind }) =>
["endpoint", "storage", "worker", "javascript-module"].includes(kind),
),
).toBe(true);
await expect(graphIdentities(template)).resolves.toEqual(expected);
},
);
@@ -1,144 +0,0 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { canonicalJson } from "../../domain/comparisonSemantics.js";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js";
import { analyzeJavaScriptApplication } from "../../../tests/support/javascriptApplicationScope.js";
type ApplicationAnalysis = ReturnType<
typeof javascriptApplicationAnalysisResultSchema.parse
>;
const analyzeFiles = async (
files: ReadonlyMap<string, string>,
): Promise<ApplicationAnalysis> => {
const root = await createTestTempDirectory("rea-typescript-application-");
await Promise.all(
[...files].map(([path, source]) => writeFile(join(root, path), source)),
);
const result = await analyzeJavaScriptApplication({
input_path: root,
format: "directory",
});
if (!result.ok) throw result.error;
const analysis = javascriptApplicationAnalysisResultSchema.parse(
result.value.normalized_result,
);
return javascriptApplicationAnalysisResultSchema.parse(
JSON.parse(canonicalJson(analysis, "TypeScript application analysis")),
);
};
it.each(["types.d.ts", "types.d.mts", "types.d.cts"])(
"keeps valid ambient declarations complete in %s",
async (path) => {
const source = "export const value: number;\n";
const analysis = await analyzeFiles(new Map([[path, source]]));
expect(analysis.graph.coverage.status).toBe("complete");
expect(parseJavaScriptSource(source, path)?.errors).toEqual([]);
},
);
it("retains the missing-initializer diagnostic for ordinary TypeScript", () => {
const errors = parseJavaScriptSource(
"export const value: number;",
"types.ts",
)?.errors;
expect(errors?.map(({ message }) => message)).toContain(
"Missing initializer in const declaration. (1:26)",
);
});
it("keeps CommonJS TypeScript angle-bracket assertions complete", async () => {
const source = "export const value = <number>42;\n";
const analysis = await analyzeFiles(new Map([["app.cts", source]]));
expect(analysis.graph.coverage.status).toBe("complete");
expect(parseJavaScriptSource(source, "app.cts")?.errors).toEqual([]);
});
it("retains exported constant evidence through a type-only satisfies expression", async () => {
const analysis = await analyzeFiles(
new Map([["app.ts", 'export const value = "ready" satisfies string;\n']]),
);
expect(
analysis.semantic_graph.nodes.filter(
({ kind, properties }) =>
kind === "literal" && properties.value === "ready",
),
).toHaveLength(1);
});
it("resolves erased wrappers through the composed application graph", async () => {
const source = `
const dependency = require("./dependency.ts")["value" as const] satisfies number;
export { dependency };
function render<T>() { return "ready"; }
export const specialized = render<string> satisfies () => string;
specialized();
`;
const analysis = await analyzeFiles(
new Map([
["app.mts", source],
["dependency.ts", "export const value = 1;\n"],
]),
);
expect(analysis.graph.edges).toContainEqual(
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
specifier: "./dependency.ts",
imported_path: ["value"],
resolved_path: "dependency.ts",
resolution_status: "resolved",
}),
}),
);
expect(analysis.semantic_graph.relations).toContainEqual(
expect.objectContaining({ relation: "calls", resolution: "resolved" }),
);
expect(
analysis.graph.nodes.flatMap(({ observations }) => observations),
).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
semantic_role: "export-return-shapes",
exported_name: "specialized",
static_return_shapes: expect.arrayContaining([
expect.objectContaining({
value_status: "literal",
fields: expect.arrayContaining([
expect.objectContaining({ path: "", value: "ready" }),
]),
}),
]),
}),
}),
);
expect(parseJavaScriptSource(source, "app.mts")?.errors).toEqual([]);
});
it("applies TypeScript .mts ambiguity rules while retaining unambiguous generics", () => {
expect(
parseJavaScriptSource(
"export const identity = <T,>(value: T) => value;",
"app.mts",
)?.errors,
).toEqual([]);
expect(
parseJavaScriptSource(
"export const identity = <T>(value: T) => value;",
"app.mts",
)?.errors.map(({ reasonCode }) => reasonCode),
).toContain("ReservedArrowTypeParam");
expect(parseJavaScriptSource("const value = <Widget />;", "app.mts")).toBe(
null,
);
});
@@ -1,94 +0,0 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, relative } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { parseExpression } from "@babel/parser";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { staticPath } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js";
import { analyzeJavaScriptApplication } from "../../../tests/support/javascriptApplicationScope.js";
const urlCases = [
["./preload%20name.cjs", "preload name.cjs", "main.mjs"],
["./pr%C3%A9load.cjs", "préload.cjs", "main.mjs"],
["./preload%3F%23.cjs", "preload?#.cjs", "main.mjs"],
["./preload.cjs?cache=1#v2", "preload.cjs", "main.mjs"],
["../preload.cjs?cache=1", "preload.cjs", "src/main.mjs"],
["./sub/../preload.cjs", "preload.cjs", "main.mjs"],
["./preload.cjs", "preload.cjs", "main.mjs"],
] as const;
const unknownExpressions = [
'fileURLToPath(new URL("./invalid%.cjs", import.meta.url))',
'fileURLToPath(new URL("./preload%2fname.cjs", import.meta.url))',
'fileURLToPath(new URL("./preload%5cname.cjs", import.meta.url))',
'fileURLToPath(new URL("./%2e/preload.cjs", import.meta.url))',
'fileURLToPath(new URL("https://example.test/preload.cjs", import.meta.url))',
'fileURLToPath(new URL("//example.test/preload.cjs", import.meta.url))',
'fileURLToPath(new URL("#fragment", import.meta.url))',
'fileURLToPath(new URL("?cache=1", import.meta.url))',
'fileURLToPath(new URL("./preload.cjs", runtimeBase))',
"fileURLToPath(new URL(runtimePath, import.meta.url))",
'convertFileURLToPath(new URL("./preload.cjs", import.meta.url))',
];
describe("source-relative file URL paths", () => {
it.each(urlCases)(
"resolves %s to the file read by Node",
async (reference, target, sourcePath) => {
const root = await createTestTempDirectory("rea-file-url-path-");
const sourceFile = join(root, sourcePath);
await mkdir(dirname(sourceFile), { recursive: true });
await writeFile(join(root, target), 'module.exports = "real preload";\n');
if (reference.includes("%") && sourcePath === "main.mjs")
await writeFile(
join(root, reference),
'module.exports = "encoded-name decoy";\n',
);
const nativePath = fileURLToPath(
new URL(reference, pathToFileURL(sourceFile)),
);
expect(relative(root, nativePath)).toBe(target);
expect(await readFile(nativePath, "utf8")).toContain("real preload");
await writeFile(
sourceFile,
`import { fileURLToPath } from "node:url";\nnew BrowserWindow({ webPreferences: { preload: fileURLToPath(new URL(${JSON.stringify(reference)}, import.meta.url)) } });\n`,
);
const result = await analyzeJavaScriptApplication({
input_path: root,
format: "directory",
});
if (!result.ok) throw result.error;
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
result.value.normalized_result,
);
const preloads = graph.nodes.filter(
({ kind }) => kind === "electron-preload",
);
expect(preloads).toHaveLength(1);
expect(preloads[0]?.observations).not.toHaveLength(0);
for (const observation of preloads[0]?.observations ?? [])
expect(observation.properties).toMatchObject({
resolved_path: target,
resolution_status: "resolved",
resolution_context: "filesystem-expression",
});
},
);
it.each(unknownExpressions)("leaves unsupported %s unknown", (expression) => {
expect(
staticPath(parseExpression(expression, { sourceType: "module" })),
).toBeUndefined();
});
it("does not URL-decode literal filesystem punctuation", () => {
const expression = 'path.join(__dirname, "preload%20?#.cjs")';
expect(
staticPath(parseExpression(expression, { sourceType: "module" })),
).toBe("preload%20?#.cjs");
});
});
@@ -1,412 +0,0 @@
import { describe, expect, it } from "vitest";
import { execFile } from "node:child_process";
import { mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
import { analyzeJavaScriptApplication } from "../../../tests/support/javascriptApplicationScope.js";
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
const filesFor = (paths: readonly string[]) =>
new Map<string, JavaScriptArtifactFile>(
["app/main.js", ...paths].map((path) => [
path,
{
path,
container_sha256: "a".repeat(64),
sha256: "b".repeat(64),
bytes: 0,
inventory_artifact_id: `artifact-${path}`,
kind: "javascript",
unpacked: false,
text: { included: true, value: "" },
},
]),
);
describe("prefix-only Node builtins", () => {
it.each(["import", "require"] as const)(
"resolves ordinary packages named test, sqlite and sea for %s",
async (moduleKind) => {
const root = await createTestTempDirectory("rea-prefix-only-builtins-");
const names = ["test", "sqlite", "sea"];
for (const name of [...names, "fs"]) {
const directory = join(root, "node_modules", name);
await mkdir(directory, { recursive: true });
await writeFile(
join(directory, "index.js"),
`module.exports = ${JSON.stringify(name)};`,
);
}
const main = join(
root,
moduleKind === "import" ? "main.mjs" : "main.cjs",
);
await writeFile(
main,
names
.map((name, index) =>
moduleKind === "import"
? `import value${index} from ${JSON.stringify(name)}; console.log(value${index});`
: `console.log(require(${JSON.stringify(name)}));`,
)
.join("\n") +
(moduleKind === "import"
? '\nimport "fs"; import "node:test";'
: '\nrequire("fs"); require("node:test");'),
);
const native = await promisify(execFile)(process.execPath, [main], {
timeout: 5_000,
});
expect(native.stdout.trim().split("\n").slice(0, 3)).toEqual(names);
const result = await analyzeJavaScriptApplication({
input_path: root,
format: "directory",
});
if (!result.ok) throw result.error;
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
result.value.normalized_result,
);
const imports = graph.edges.filter((edge) => edge.relation === "imports");
for (const name of names) {
const matches = imports.filter(
(edge) => edge.properties.specifier === name,
);
expect(matches.length).toBeGreaterThan(0);
for (const edge of matches)
expect(edge.properties.resolved_path).toBe(
`node_modules/${name}/index.js`,
);
}
for (const name of ["fs", "node:test"]) {
const matches = imports.filter(
(edge) => edge.properties.specifier === name,
);
expect(matches.length).toBeGreaterThan(0);
for (const edge of matches)
expect(edge.properties.resolved_path).toBeNull();
}
},
);
});
describe("module specifier punctuation", () => {
it.each(["#", "?"])(
"keeps CommonJS punctuation and strips URL suffixes for ESM (%s)",
(punctuation) => {
const literal = `app/dep.cjs${punctuation}literal.cjs`;
const stripped = "app/dep.cjs";
const commonJs = resolveArtifactPathByContext({
declaredPath: `./dep.cjs${punctuation}literal.cjs`,
sourcePath: "app/main.js",
context: "module-specifier",
files: filesFor([literal, stripped]),
moduleKind: "require",
});
expect(commonJs).toMatchObject({
resolution_status: "resolved",
resolved_path: literal,
});
const esm = resolveArtifactPathByContext({
declaredPath: `./dep.cjs${punctuation}literal.cjs`,
sourcePath: "app/main.js",
context: "module-specifier",
files: filesFor([literal, stripped]),
moduleKind: "import",
});
expect(esm).toMatchObject({
resolution_status: "resolved",
resolved_path: stripped,
});
const noStrippedTarget = resolveArtifactPathByContext({
declaredPath: `./dep.cjs${punctuation}literal.cjs`,
sourcePath: "app/main.js",
context: "module-specifier",
files: filesFor([literal]),
moduleKind: "import",
});
expect(noStrippedTarget).toMatchObject({
resolution_status: "not-found",
resolved_path: null,
});
},
);
it.each(["#", "?"])(
"keeps HTML URL suffix handling for %s",
(punctuation) => {
expect(
resolveArtifactPathByContext({
declaredPath: `./dep.cjs${punctuation}literal.cjs`,
sourcePath: "app/main.js",
context: "html-reference",
files: filesFor([
"app/dep.cjs",
`app/dep.cjs${punctuation}literal.cjs`,
]),
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "app/dep.cjs",
});
},
);
});
describe("ESM module URL decoding", () => {
it("resolves URL-encoded imports to the files loaded by Node", async () => {
const cases = [
["./plain.mjs", "plain.mjs"],
["./space%20name.mjs", "space name.mjs"],
["./pr%C3%A9load.mjs", "préload.mjs"],
["./name%3F%23.mjs", "name?#.mjs"],
["./percent%2520.mjs?cache=1#v2", "percent%20.mjs"],
] as const;
const root = await createTestTempDirectory("rea-module-url-");
const main = join(root, "main.mjs");
await writeFile(
main,
cases
.map(
([specifier], index) =>
`import value${index} from ${JSON.stringify(specifier)}; console.log(value${index});`,
)
.join("\n"),
);
for (const [specifier, target] of cases) {
await writeFile(
join(root, target),
`export default ${JSON.stringify(target)};`,
);
const encoded = specifier.slice(2).split("?", 1)[0]?.split("#", 1)[0];
if (encoded !== undefined && encoded !== target)
await writeFile(join(root, encoded), 'export default "encoded decoy";');
}
const native = await promisify(execFile)(process.execPath, [main], {
timeout: 5_000,
});
expect(native.stdout.trim().split("\n")).toEqual(
cases.map(([, target]) => target),
);
const result = await analyzeJavaScriptApplication({
input_path: root,
format: "directory",
});
if (!result.ok) throw result.error;
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
result.value.normalized_result,
);
for (const [specifier, target] of cases) {
const imports = graph.edges.filter(
(edge) =>
edge.relation === "imports" &&
edge.properties.specifier === specifier,
);
expect(imports.length).toBeGreaterThan(0);
for (const edge of imports)
expect(edge.properties.resolved_path).toBe(target);
}
});
it("preserves CommonJS literal percent names", () => {
expect(
resolveArtifactPathByContext({
declaredPath: "./space%20name.cjs",
sourcePath: "app/main.js",
context: "module-specifier",
moduleKind: "require",
files: filesFor(["app/space%20name.cjs", "app/space name.cjs"]),
}),
).toMatchObject({
resolved_path: "app/space%20name.cjs",
resolution_status: "resolved",
});
});
it.each(["./invalid%.mjs", "./invalid%C3.mjs", "./nul%00.mjs"])(
"rejects malformed or NUL URL %s",
(declaredPath) => {
expect(
resolveArtifactPathByContext({
declaredPath,
sourcePath: "app/main.js",
context: "module-specifier",
moduleKind: "import",
files: filesFor([`app/${declaredPath.slice(2)}`]),
}),
).toMatchObject({ resolved_path: null, resolution_status: "rejected" });
},
);
});
const packageEntryUrlCases = [
["exports", "./actual%20file.cjs", "actual file.cjs"],
["exports", "./actual.cjs?variant", "actual.cjs"],
["exports", "./actual.cjs#variant", "actual.cjs"],
["exports", "./actual%23file.cjs", "actual#file.cjs"],
["exports", "./actual%3Ffile.cjs", "actual?file.cjs"],
["exports", "./actual%252Ffile.cjs", "actual%2Ffile.cjs"],
["exports", "./actual.cjs", "actual.cjs"],
["main", "./actual%20file.cjs", "actual%20file.cjs"],
["main", "./actual?file.cjs", "actual?file.cjs"],
["main", "./actual#file.cjs", "actual#file.cjs"],
] as const;
it.each(["import", "require"] as const)(
"matches Node %s across package exports and main path representations",
async (moduleKind) => {
const root = await createTestTempDirectory("rea-package-export-url-");
for (const [
index,
[field, declaredPath, target],
] of packageEntryUrlCases.entries()) {
const name = `fixture-${index}`;
const packageRoot = join(root, "node_modules", name);
await mkdir(packageRoot, { recursive: true });
await writeFile(
join(packageRoot, "package.json"),
JSON.stringify({
name,
[field]: declaredPath,
}),
);
await writeFile(
join(packageRoot, target),
`module.exports=${JSON.stringify(target)};`,
);
const literal = declaredPath.slice(2).split("?", 1)[0]?.split("#", 1)[0];
if (literal !== undefined && literal !== target)
await writeFile(
join(packageRoot, literal),
'module.exports="literal decoy";',
);
}
const main = join(root, moduleKind === "import" ? "main.mjs" : "main.cjs");
await writeFile(
main,
packageEntryUrlCases
.map((_, index) =>
moduleKind === "import"
? `import value${index} from "fixture-${index}"; console.log(value${index});`
: `console.log(require("fixture-${index}"));`,
)
.join("\n"),
);
const native = await promisify(execFile)(process.execPath, [main], {
timeout: 5_000,
});
expect(native.stdout.trim().split("\n")).toEqual(
packageEntryUrlCases.map(([, , target]) => target),
);
const result = await analyzeJavaScriptApplication({
input_path: root,
format: "directory",
});
if (!result.ok) throw result.error;
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
result.value.normalized_result,
);
for (const [
index,
[field, declaredPath, target],
] of packageEntryUrlCases.entries()) {
const imports = graph.edges.filter(
(edge) =>
edge.relation === "imports" &&
edge.properties.specifier === `fixture-${index}`,
);
expect(
imports.length,
`${moduleKind}: ${field} ${declaredPath}`,
).toBeGreaterThan(0);
for (const edge of imports)
expect(edge.properties.resolved_path).toBe(
`node_modules/fixture-${index}/${target}`,
);
}
},
);
describe("package exports URL path rejections", () => {
it.each(
(["import", "require"] as const).flatMap((moduleKind) =>
[
"./actual%file.cjs",
"./actual%E0%A4file.cjs",
"./actual%00file.cjs",
"./actual%2Ffile.cjs",
"./actual%5Cfile.cjs",
"./actual%2Efile.cjs",
"./../../../../escape.cjs",
].map((declaredPath) => ({ moduleKind, declaredPath })),
),
)(
"rejects $moduleKind URL $declaredPath without borrowing a literal decoy",
({ moduleKind, declaredPath }) => {
const packagePath = "app/node_modules/fixture/package.json";
const files = filesFor([
packagePath,
`app/node_modules/fixture/${declaredPath.slice(2)}`,
]);
const metadata = files.get(packagePath);
if (metadata === undefined) throw new Error("Missing fixture metadata");
files.set(packagePath, {
...metadata,
kind: "package-json",
text: {
included: true,
value: JSON.stringify({ exports: declaredPath }),
},
});
expect(
resolveArtifactPathByContext({
declaredPath: "fixture",
sourcePath: "app/main.js",
context: "module-specifier",
moduleKind,
files,
}),
).toMatchObject({ resolved_path: null, resolution_status: "rejected" });
},
);
it.each(["import", "require"] as const)(
"keeps %s exports inside their inventoried container",
(moduleKind) => {
const packagePath = "app/node_modules/fixture/package.json";
const target = "app/node_modules/fixture/actual file.cjs";
const files = filesFor([packagePath, target]);
const metadata = files.get(packagePath);
const crossContainer = files.get(target);
if (metadata === undefined || crossContainer === undefined)
throw new Error("Missing fixture files");
files.set(packagePath, {
...metadata,
kind: "package-json",
text: {
included: true,
value: '{"exports":"./actual%20file.cjs"}',
},
});
files.set(target, {
...crossContainer,
container_sha256: "c".repeat(64),
});
expect(
resolveArtifactPathByContext({
declaredPath: "fixture",
sourcePath: "app/main.js",
context: "module-specifier",
moduleKind,
files,
}),
).toMatchObject({ resolved_path: null, resolution_status: "not-found" });
},
);
});
@@ -1,58 +0,0 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { analyzeJavaScriptApplication } from "../../../tests/support/javascriptApplicationScope.js";
it("classifies overloaded open calls from lexical receiver facts", async () => {
const inputPath = await createTestTempDirectory(
"rea-js-open-classification-",
);
await writeFile(
join(inputPath, "app.js"),
`
const popup = window;
popup.open(url, "/preview");
{
const indexedDB = { open() {} };
indexedDB.open("PROPFIND", "/dav");
indexedDB.open(databaseName, "2");
}
const xhr = new XMLHttpRequest();
xhr.open(method, "/api");
parent.open("GET", "/parent-target");
self.open("POST", "/self-target");
document.open("GET", "/document-target");
{
const parent = new XMLHttpRequest();
parent.open(method, "/shadowed-parent");
}
indexedDB.open("records", "2");
`,
);
const result = await analyzeJavaScriptApplication({
input_path: inputPath,
format: "directory",
});
if (!result.ok)
throw new Error(`Expected analysis success: ${result.error.message}`);
const { graph } = javascriptApplicationAnalysisResultSchema.parse(
result.value.normalized_result,
);
const endpoints = graph.nodes
.filter(({ kind }) => kind === "endpoint")
.flatMap(({ observations }) =>
observations.map(({ properties }) => properties.value),
)
.sort();
const storage = graph.nodes
.filter(({ kind }) => kind === "storage")
.flatMap(({ observations }) =>
observations.map(({ properties }) => properties.storage_kind),
);
expect(endpoints).toEqual(["/api", "/dav", "/shadowed-parent"]);
expect(storage).toEqual(["indexed-db"]);
});