Files
rea/tests/acceptance/applications/JavaScriptModuleLookupCli.test.ts
T
morluto c0904b4c1a fix(javascript): apply loader-owned module lookup rules
Keep relative ESM imports exact, restrict Node legacy probes to .js/.json/.node, and treat empty package main as absent. Stop Node package-main lookup before nested package reinterpretation, while retaining generic artifact inference.

Replace stale recursive and .cjs probe expectations. Verify CLI and MCP graph results against real Node 24 runtime controls, including exact suffixes, index fallback, and nested package main refusal.

Refs #2020, #2021, #2022
2026-10-12 11:10:32 +08:00

169 lines
6.4 KiB
TypeScript

import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
const execute = promisify(execFile);
const createModuleLookupFixture = async () => {
const root = await createTestTempDirectory("rea-module-lookup-");
await writeFixtureFiles(root, {
"package.json": JSON.stringify({ name: "module-lookup-fixture" }),
"esm/entry.mjs": [
'import "./extensionless";',
'import "./directory";',
'import "./explicit.js";',
'import "./exact.js";',
'import fallback from "empty-main";',
'import "nested-main";',
"void fallback;",
].join("\n"),
"esm/extensionless.js": "module.exports = 1;",
"esm/directory/index.js": "module.exports = 2;",
"esm/explicit.js.js": "module.exports = 3;",
"esm/exact.js": "module.exports = 4;",
"cjs/entry.cjs": [
'require("./extensionless");',
'require("./feature");',
'require("./feature.cjs");',
'require("./default-js");',
'require("./default-json");',
'require("./default-node");',
'require("empty-main");',
'require("nested-main");',
].join("\n"),
"cjs/extensionless.cjs": "module.exports = 1;",
"cjs/feature.cjs": "module.exports = 2;",
"cjs/default-js.js": "module.exports = 3;",
"cjs/default-json.json": JSON.stringify({ value: 4 }),
// Inventory-only placeholder: never pass this path to Node's runtime.
"cjs/default-node.node": "source-owned opaque native-module placeholder",
"node_modules/empty-main/package.json": JSON.stringify({ main: "" }),
"node_modules/empty-main/index.js": "module.exports = 42;",
"node_modules/nested-main/package.json": JSON.stringify({ main: "sub" }),
"node_modules/nested-main/sub/package.json": JSON.stringify({
main: "other",
}),
"node_modules/nested-main/sub/other.js": "module.exports = 99;",
"runtime-control.mjs": [
'const specifiers = ["./esm/extensionless", "./esm/directory", "./esm/explicit.js", "./esm/exact.js", "empty-main", "nested-main"];',
"const results = [];",
"for (const specifier of specifiers) {",
" try { await import(specifier); results.push({ specifier, ok: true }); }",
" catch (error) { results.push({ specifier, ok: false, code: error.code }); }",
"}",
"console.log(JSON.stringify(results));",
].join("\n"),
"runtime-control.cjs": [
'const specifiers = ["./cjs/extensionless", "./cjs/feature", "./cjs/feature.cjs", "./cjs/default-js", "./cjs/default-json", "empty-main", "nested-main"];',
"const results = [];",
"for (const specifier of specifiers) {",
" try { require(specifier); results.push({ specifier, ok: true }); }",
" catch (error) { results.push({ specifier, ok: false, code: error.code }); }",
"}",
"console.log(JSON.stringify(results));",
].join("\n"),
});
return root;
};
cliTest.for(["CLI", "stdio MCP"] as const)(
"matches Node module lookup for ESM and CommonJS through %s",
async (surface, { cli }) => {
const root = await createModuleLookupFixture();
const [esmControl, cjsControl] = await Promise.all([
execute(process.execPath, ["runtime-control.mjs"], {
cwd: root,
timeout: 5_000,
}),
execute(process.execPath, ["runtime-control.cjs"], {
cwd: root,
timeout: 5_000,
}),
]);
const esmResults = JSON.parse(esmControl.stdout) as Array<{
readonly specifier: string;
readonly ok: boolean;
readonly code?: string;
}>;
const cjsResults = JSON.parse(cjsControl.stdout) as Array<{
readonly specifier: string;
readonly ok: boolean;
readonly code?: string;
}>;
expect(esmResults).toEqual([
{
specifier: "./esm/extensionless",
ok: false,
code: "ERR_MODULE_NOT_FOUND",
},
{
specifier: "./esm/directory",
ok: false,
code: "ERR_UNSUPPORTED_DIR_IMPORT",
},
{
specifier: "./esm/explicit.js",
ok: false,
code: "ERR_MODULE_NOT_FOUND",
},
{ specifier: "./esm/exact.js", ok: true },
{ specifier: "empty-main", ok: true },
{ specifier: "nested-main", ok: false, code: "ERR_MODULE_NOT_FOUND" },
]);
expect(cjsResults).toEqual([
{
specifier: "./cjs/extensionless",
ok: false,
code: "MODULE_NOT_FOUND",
},
{
specifier: "./cjs/feature",
ok: false,
code: "MODULE_NOT_FOUND",
},
{ specifier: "./cjs/feature.cjs", ok: true },
{ specifier: "./cjs/default-js", ok: true },
{ specifier: "./cjs/default-json", ok: true },
{ specifier: "empty-main", ok: true },
{ specifier: "nested-main", ok: false, code: "MODULE_NOT_FOUND" },
]);
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
const expectations = [
["import", "./extensionless", null, "not-found"],
["import", "./directory", null, "not-found"],
["import", "./explicit.js", null, "not-found"],
["import", "./exact.js", "esm/exact.js", "resolved"],
["import", "empty-main", "node_modules/empty-main/index.js", "resolved"],
["import", "nested-main", null, "not-found"],
["require", "./extensionless", null, "not-found"],
["require", "./feature", null, "not-found"],
["require", "./feature.cjs", "cjs/feature.cjs", "resolved"],
["require", "./default-js", "cjs/default-js.js", "resolved"],
["require", "./default-json", "cjs/default-json.json", "resolved"],
["require", "./default-node", "cjs/default-node.node", "resolved"],
["require", "empty-main", "node_modules/empty-main/index.js", "resolved"],
["require", "nested-main", null, "not-found"],
] as const;
for (const [kind, specifier, resolvedPath, status] of expectations)
expect(graph.edges).toContainEqual(
expect.objectContaining({
relation: "imports",
properties: expect.objectContaining({
module_link_kind: kind,
specifier,
resolved_path: resolvedPath,
resolution_status: status,
}),
}),
);
},
);