mirror of
https://github.com/morluto/rea.git
synced 2026-11-13 18:22:42 +00:00
144 lines
5.7 KiB
TypeScript
144 lines
5.7 KiB
TypeScript
import { expect } from "vitest";
|
|
import { createHash } from "node:crypto";
|
|
import { readFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
import {
|
|
analyzeJavaScriptDirectory,
|
|
runNodeJsonControl,
|
|
} from "../../support/cli/javascriptApplication.js";
|
|
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
|
|
import { resolveJavaScriptSemanticEvidence } from "../../support/javascriptSemanticGraphFixture.js";
|
|
|
|
cliTest.for(["CLI", "stdio MCP"] as const)(
|
|
"matches Node's fs function-valued argument rejection through %s",
|
|
async (surface, { cli }) => {
|
|
const root = await createTestTempDirectory("rea-fs-function-arguments-");
|
|
await writeFixtureFiles(root, {
|
|
input: "fixture",
|
|
"entry.mjs": `
|
|
import {open, openSync, closeSync} from "node:fs";
|
|
import {open as openPromise} from "node:fs/promises";
|
|
export const badSyncFlag = p => openSync(p, () => {});
|
|
export const badSyncMode = p => openSync(p, "r", () => {});
|
|
export const badSyncClassFlag = p => openSync(p, class {});
|
|
export const badSyncClassMode = p => openSync(p, "r", class {});
|
|
export const badPromiseFlag = p => openPromise(p, () => {});
|
|
export const badPromiseClassFlag = p => openPromise(p, class {});
|
|
export const badCallbackFlag = p => open(p, () => {}, () => {});
|
|
export const missingCallback = p => open(p);
|
|
export const missingShortCallback = p => open(p, "r");
|
|
export const nonfunctionCallback = p => open(p, "r", 0o644, 17);
|
|
export async function badPromiseMode(p) {
|
|
const h = await openPromise(p, "r", () => {}); h.close();
|
|
}
|
|
export async function badPromiseClassMode(p) {
|
|
const h = await openPromise(p, "r", class {}); h.close();
|
|
}
|
|
export const callbackOnly = p => new Promise((resolve, reject) =>
|
|
open(p, (error, fd) => {
|
|
if (error) reject(error);
|
|
else { closeSync(fd); resolve("opened"); }
|
|
}));
|
|
export const callbackMode = p => new Promise((resolve, reject) =>
|
|
open(p, "r", (error, fd) => {
|
|
if (error) reject(error);
|
|
else { closeSync(fd); resolve("opened"); }
|
|
}));
|
|
export const emptySpread = (p, ...args) => new Promise((resolve, reject) =>
|
|
open(p, (error, fd) => {
|
|
if (error) reject(error);
|
|
else { closeSync(fd); resolve("opened"); }
|
|
}, ...args));
|
|
`,
|
|
"control.mjs": `
|
|
import * as fixture from "./entry.mjs";
|
|
import {closeSync} from "node:fs";
|
|
const observed = {};
|
|
for (const [name, fn] of Object.entries(fixture)) {
|
|
try {
|
|
const value = await fn("input");
|
|
if (typeof value === "number") closeSync(value);
|
|
else if (typeof value?.close === "function") await value.close();
|
|
observed[name] = value === "opened" ? value : "unexpected success";
|
|
} catch (error) { observed[name] = error.code; }
|
|
}
|
|
console.log(JSON.stringify(observed));
|
|
`,
|
|
});
|
|
expect(await runNodeJsonControl(root, "control.mjs")).toEqual({
|
|
badSyncFlag: "ERR_INVALID_ARG_VALUE",
|
|
badSyncMode: "ERR_INVALID_ARG_TYPE",
|
|
badSyncClassFlag: "ERR_INVALID_ARG_VALUE",
|
|
badSyncClassMode: "ERR_INVALID_ARG_TYPE",
|
|
badPromiseFlag: "ERR_INVALID_ARG_VALUE",
|
|
badPromiseClassFlag: "ERR_INVALID_ARG_VALUE",
|
|
badCallbackFlag: "ERR_INVALID_ARG_VALUE",
|
|
missingCallback: "ERR_INVALID_ARG_TYPE",
|
|
missingShortCallback: "ERR_INVALID_ARG_TYPE",
|
|
nonfunctionCallback: "ERR_INVALID_ARG_TYPE",
|
|
badPromiseMode: "ERR_INVALID_ARG_TYPE",
|
|
badPromiseClassMode: "ERR_INVALID_ARG_TYPE",
|
|
callbackOnly: "opened",
|
|
callbackMode: "opened",
|
|
emptySpread: "opened",
|
|
});
|
|
|
|
const result = await analyzeJavaScriptDirectory(surface, cli, root, {
|
|
sourcePaths: ["entry.mjs"],
|
|
});
|
|
const resources = result.semantic_graph.nodes.filter(
|
|
({ kind }) => kind === "resource",
|
|
);
|
|
expect(resources).toHaveLength(3);
|
|
expect(resources.map(({ properties }) => properties.method)).toEqual([
|
|
"open",
|
|
"open",
|
|
"open",
|
|
]);
|
|
const source = await readFile(join(root, "entry.mjs"), "utf8");
|
|
const expectedLines = source
|
|
.split("\n")
|
|
.flatMap((line, index) =>
|
|
line.includes("open(p, (error, fd)") ||
|
|
line.includes('open(p, "r", (error, fd)')
|
|
? [index + 1]
|
|
: [],
|
|
);
|
|
const lines = resources.map(({ evidence: reference }) => {
|
|
const evidence = resolveJavaScriptSemanticEvidence(
|
|
result.semantic_graph,
|
|
reference,
|
|
);
|
|
expect(evidence).toMatchObject({
|
|
state: "observed",
|
|
authority: "ast-static-analysis",
|
|
artifact: {
|
|
available: true,
|
|
sha256: createHash("sha256").update(source).digest("hex"),
|
|
},
|
|
location: {
|
|
available: true,
|
|
value: { kind: "source-range", source: "entry.mjs" },
|
|
},
|
|
});
|
|
const location = evidence.location;
|
|
if (!location.available || location.value.kind !== "source-range")
|
|
throw new Error("Expected a source range for the accepted open");
|
|
return location.value.start.line;
|
|
});
|
|
expect(lines.sort((a, b) => a - b)).toEqual(expectedLines);
|
|
expect(
|
|
result.semantic_graph.relations.filter(
|
|
({ relation }) => relation === "acquires",
|
|
),
|
|
).toHaveLength(3);
|
|
expect(
|
|
result.semantic_graph.relations.filter(
|
|
({ relation }) => relation === "releases",
|
|
),
|
|
).toEqual([]);
|
|
},
|
|
);
|