mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 20:06:14 +00:00
123 lines
4.5 KiB
TypeScript
123 lines
4.5 KiB
TypeScript
import { readdir, readFile } from "node:fs/promises";
|
|
import { join, relative } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { parseConfig } from "../src/config.js";
|
|
import { HopperProvider } from "../src/hopper/HopperProvider.js";
|
|
import { GhidraProvider } from "../src/ghidra/GhidraProvider.js";
|
|
import { GHIDRA_INVENTORY_OPERATIONS } from "../src/ghidra/GhidraInventoryValues.js";
|
|
import { GHIDRA_FUNCTION_OPERATIONS } from "../src/ghidra/GhidraFunctionValues.js";
|
|
import { silentLogger } from "../src/logger.js";
|
|
|
|
const sourceRoot = new URL("../src/", import.meta.url);
|
|
|
|
describe("provider-neutral architecture", () => {
|
|
it("keeps deep-provider imports behind adapters and the composition root", async () => {
|
|
const forbiddenRoots = ["domain", "contracts", "server", "application"];
|
|
const inspected = await Promise.all(
|
|
forbiddenRoots.map(async (root) => {
|
|
const files = await typescriptFiles(
|
|
fileURLToPath(new URL(`${root}/`, sourceRoot)),
|
|
);
|
|
return Promise.all(
|
|
files.map(async (file) => {
|
|
if (root === "application" && file.endsWith("/runtime.ts"))
|
|
return undefined;
|
|
const source = await readFile(file, "utf8");
|
|
return /from\s+["'][^"']*\/(?:hopper|ghidra)\//u.test(source)
|
|
? relative(fileURLToPath(sourceRoot), file)
|
|
: undefined;
|
|
}),
|
|
);
|
|
}),
|
|
);
|
|
const violations = inspected
|
|
.flat()
|
|
.filter((violation) => violation !== undefined);
|
|
expect(violations).toEqual([]);
|
|
});
|
|
|
|
it("publishes only admitted immutable Ghidra read-only capabilities", () => {
|
|
const config = parseConfig({});
|
|
expect(config.ok).toBe(true);
|
|
if (!config.ok) return;
|
|
const provider = new GhidraProvider(config.value, silentLogger, {
|
|
readText: () => undefined,
|
|
executable: () => false,
|
|
probeJava: () => undefined,
|
|
});
|
|
const capabilities = provider.capabilities();
|
|
expect(capabilities.map(({ operation }) => operation).sort()).toEqual(
|
|
[...GHIDRA_INVENTORY_OPERATIONS, ...GHIDRA_FUNCTION_OPERATIONS].sort(),
|
|
);
|
|
expect(Object.isFrozen(capabilities)).toBe(true);
|
|
for (const descriptor of capabilities) {
|
|
expect(descriptor).toMatchObject({
|
|
available: true,
|
|
reason: null,
|
|
effects: {
|
|
mutatesArtifact: false,
|
|
mayShowUi: false,
|
|
mayAccessNetwork: false,
|
|
changesPermissions: false,
|
|
requiresRoot: false,
|
|
},
|
|
});
|
|
expect(Object.isFrozen(descriptor)).toBe(true);
|
|
expect(Object.isFrozen(descriptor.effects)).toBe(true);
|
|
expect(Object.isFrozen(descriptor.limits)).toBe(true);
|
|
expect(Object.isFrozen(descriptor.limitations)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("publishes deterministic immutable Hopper capability descriptors", () => {
|
|
const config = parseConfig({});
|
|
expect(config.ok).toBe(true);
|
|
if (!config.ok) return;
|
|
const provider = new HopperProvider(config.value, silentLogger);
|
|
const capabilities = provider.capabilities();
|
|
expect(capabilities).toHaveLength(34);
|
|
expect(new Set(capabilities.map(({ operation }) => operation)).size).toBe(
|
|
capabilities.length,
|
|
);
|
|
expect(Object.isFrozen(capabilities)).toBe(true);
|
|
for (const descriptor of capabilities) {
|
|
expect(descriptor.provider).toEqual(provider.identity());
|
|
expect(descriptor).toMatchObject({
|
|
inputContractVersion: 1,
|
|
outputContractVersion: 1,
|
|
available: true,
|
|
reason: null,
|
|
});
|
|
expect(Object.isFrozen(descriptor)).toBe(true);
|
|
expect(Object.isFrozen(descriptor.effects)).toBe(true);
|
|
expect(Object.isFrozen(descriptor.limits)).toBe(true);
|
|
expect(Object.isFrozen(descriptor.limitations)).toBe(true);
|
|
}
|
|
expect(
|
|
capabilities.find(({ operation }) => operation === "list_procedures"),
|
|
).toMatchObject({ pagination: "offset" });
|
|
expect(
|
|
capabilities.find(({ operation }) => operation === "set_comment"),
|
|
).toMatchObject({
|
|
effects: { mutatesArtifact: true, mayWriteFilesystem: true },
|
|
});
|
|
});
|
|
});
|
|
|
|
const typescriptFiles = async (
|
|
directory: string,
|
|
): Promise<readonly string[]> => {
|
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
const nested = await Promise.all(
|
|
entries.map(async (entry) => {
|
|
const path = join(directory, entry.name);
|
|
if (entry.isDirectory()) return typescriptFiles(path);
|
|
return entry.isFile() && entry.name.endsWith(".ts") ? [path] : [];
|
|
}),
|
|
);
|
|
return nested.flat();
|
|
};
|