mirror of
https://github.com/morluto/rea
synced 2026-10-26 22:28:27 -04:00
test: classify filesystem application workflows as boundaries
This commit is contained in:
@@ -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"]);
|
||||
});
|
||||
Reference in New Issue
Block a user