Require a caller-owned artifact resource scope for source traversal and Git metadata reads. Preserve cancellation, filesystem reasons, partial entries and bytes, and cleanup observations through import and the CLI. Transfer exact failed file owners at regular-file admission and close boundaries, including multiple failures encountered by Git reference resolution. Validation: 169 focused tests passed across 26 files; two Windows-only cases skipped. Build, typecheck, lint, formatting, knip, architecture guards, discovery and docs checks passed. BREAKING CHANGE: Reference source readers and imports require an ArtifactResourceScope. VCS metadata reads return Result, and CLI import failures include observed details and partial evidence.
146 lines
4.6 KiB
TypeScript
146 lines
4.6 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
|
|
import { expect, it } from "vitest";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { importReferenceSource } from "../../support/referenceSourceResourceScope.js";
|
|
|
|
it("distinguishes computed require member values from literal members in source evidence", async () => {
|
|
const root = await createTestTempDirectory("rea-reference-require-members-");
|
|
const literalCallees = [
|
|
"require",
|
|
"require.resolve",
|
|
'require["resolve"]',
|
|
"require.main",
|
|
'require["main"]',
|
|
'require[("resolve")]',
|
|
'require["res\\u006flve"]',
|
|
];
|
|
const ignoredSources = [
|
|
'require[resolve]("./dep.js");',
|
|
'require[main]("./dep.js");',
|
|
'const resolve = "toString"; require[resolve]("./dep.js");',
|
|
'const main = "toString"; require[main]("./dep.js");',
|
|
'const resolve = "resolve"; require[resolve]("./dep.js");',
|
|
'const main = "main"; require[main]("./dep.js");',
|
|
'const method = "resolve"; require[method]("./dep.js");',
|
|
'require[`resolve`]("./dep.js");',
|
|
'require["re" + "solve"]("./dep.js");',
|
|
...[
|
|
"require?.",
|
|
"require.resolve?.",
|
|
'require["resolve"]?.',
|
|
"require.main?.",
|
|
'require["main"]?.',
|
|
"require?.resolve",
|
|
'require?.["resolve"]',
|
|
"require?.[resolve]",
|
|
"require?.main",
|
|
'require?.["main"]',
|
|
"require?.[main]",
|
|
"(require?.resolve)",
|
|
"require.resolve.call",
|
|
"require.main.require",
|
|
"module.require",
|
|
"other.resolve",
|
|
"other.require",
|
|
"require.toString",
|
|
'require["toString"]',
|
|
].map((callee) => `${callee}("./dep.js");`),
|
|
];
|
|
const sources: Record<string, string> = {
|
|
"computed.cjs": [
|
|
'const resolve = "toString";',
|
|
'const main = "toString";',
|
|
'const value = require[resolve]("./dep.js");',
|
|
'require[main]("./dep.js");',
|
|
"console.log(typeof value);",
|
|
].join("\n"),
|
|
"literal.cjs": 'console.log(require["resolve"]("./dep.js"));\n',
|
|
"dep.js": 'throw new Error("The dependency must not execute");\n',
|
|
"ignored.cjs": ignoredSources.map((source) => `{ ${source} }`).join("\n"),
|
|
"unknown.cjs": [
|
|
"function load(target) {",
|
|
...literalCallees.flatMap((callee) => [
|
|
` ${callee}(target);`,
|
|
` ${callee}(\`./dep.js\`);`,
|
|
` ${callee}(getTarget());`,
|
|
` ${callee}();`,
|
|
]),
|
|
" return import(target);",
|
|
"}",
|
|
].join("\n"),
|
|
};
|
|
for (const [index, callee] of literalCallees.entries())
|
|
sources[`member-${index}.cjs`] = `${callee}("./dep.js");\n`;
|
|
await Promise.all(
|
|
Object.entries(sources).map(([path, source]) =>
|
|
writeFile(join(root, path), source),
|
|
),
|
|
);
|
|
|
|
// These owned producers demonstrate the member's runtime meaning without
|
|
// loading the throwing dependency. The importer itself never executes them.
|
|
const runNode = promisify(execFile);
|
|
for (const [path, stdout] of [
|
|
["computed.cjs", "string\n"],
|
|
["literal.cjs", `${join(root, "dep.js")}\n`],
|
|
] as const) {
|
|
const executed = await runNode(process.execPath, [join(root, path)], {
|
|
cwd: root,
|
|
timeout: 5_000,
|
|
});
|
|
expect(executed.stdout).toBe(stdout);
|
|
expect(executed.stderr).toBe("");
|
|
}
|
|
|
|
const result = await importReferenceSource({
|
|
root,
|
|
caller: "reference-require-members-test",
|
|
policy: { secretPatterns: [] },
|
|
});
|
|
if (!result.ok) throw result.error;
|
|
const graph = result.value;
|
|
expect(graph.parse_failures).toEqual([]);
|
|
expect(graph.relationships).toEqual([
|
|
{
|
|
from_path: "literal.cjs",
|
|
to: "dep.js",
|
|
kind: "requires",
|
|
resolution: "internal",
|
|
parse_state: "parsed",
|
|
},
|
|
...literalCallees.map((_, index) => ({
|
|
from_path: `member-${index}.cjs`,
|
|
to: "dep.js",
|
|
kind: "requires",
|
|
resolution: "internal",
|
|
parse_state: "parsed",
|
|
})),
|
|
{
|
|
from_path: "unknown.cjs",
|
|
to: "<dynamic-import>",
|
|
kind: "imports",
|
|
resolution: "unknown",
|
|
parse_state: "partial",
|
|
},
|
|
]);
|
|
expect(graph.entries).toHaveLength(Object.keys(sources).length);
|
|
for (const [path, source] of Object.entries(sources)) {
|
|
expect(graph.entries).toContainEqual({
|
|
path,
|
|
kind: "file",
|
|
sha256: createHash("sha256").update(source).digest("hex"),
|
|
size: Buffer.byteLength(source),
|
|
language: "JavaScript",
|
|
classifications: ["source"],
|
|
content_state: "hashed",
|
|
limitations: [],
|
|
});
|
|
}
|
|
});
|