mirror of
https://github.com/morluto/rea.git
synced 2026-10-31 03:45:39 +00:00
136 lines
4.7 KiB
TypeScript
136 lines
4.7 KiB
TypeScript
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
import { chmod, mkdir, rm, symlink, writeFile } from "node:fs/promises";
|
|
import { join, resolve } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { parseEvidence } from "../../../src/domain/evidence.js";
|
|
import { nativeDispatchMetadataResultSchema } from "../../../src/domain/native/objcSwiftMetadata.js";
|
|
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
|
import { objcRecordBudgetMacho } from "../../fixtures/objcRecordBudgetMacho.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
type Replacement = "missing" | "directory" | "loop" | "unreadable";
|
|
|
|
// Real native-macos dispatch acquisition, through the compiled stdio server.
|
|
// The self-authored Mach-O fixture is only read, never executed.
|
|
const inspectReplacements = async (replacements: readonly Replacement[]) => {
|
|
const root = await createTestTempDirectory("rea-dispatch-bound-target-");
|
|
const path = join(root, "fixture.macho");
|
|
const bytes = objcRecordBudgetMacho("class");
|
|
await writeFile(path, bytes);
|
|
const env: Record<string, string> = {
|
|
PATH: process.env.PATH ?? "",
|
|
REA_LOG_LEVEL: "silent",
|
|
};
|
|
for (const key of [
|
|
"HOME",
|
|
"XDG_CONFIG_HOME",
|
|
"XDG_DATA_HOME",
|
|
"XDG_CACHE_HOME",
|
|
"XDG_STATE_HOME",
|
|
"TMPDIR",
|
|
]) {
|
|
const directory = join(root, key.toLowerCase());
|
|
env[key] = directory;
|
|
await mkdir(directory);
|
|
}
|
|
const client = new Client({ name: "dispatch-bound-target", version: "1" });
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [resolve("scripts/rea.mjs"), "mcp"],
|
|
cwd: process.cwd(),
|
|
env,
|
|
stderr: "pipe",
|
|
});
|
|
let stderr = "";
|
|
transport.stderr?.on("data", (value) => {
|
|
stderr += value;
|
|
});
|
|
const inspect = () =>
|
|
client.callTool({
|
|
name: "inspect_native_dispatch_metadata",
|
|
arguments: { max_records: 100 },
|
|
});
|
|
const successfulRead = async () => {
|
|
const reply = await inspect();
|
|
expect(reply.isError).not.toBe(true);
|
|
return nativeDispatchMetadataResultSchema.parse(
|
|
parseEvidence(reply.structuredContent).normalized_result,
|
|
);
|
|
};
|
|
try {
|
|
await client.connect(transport);
|
|
const opened = await client.callTool({
|
|
name: "open_binary",
|
|
arguments: { path },
|
|
});
|
|
expect(opened.isError).not.toBe(true);
|
|
const control = await successfulRead();
|
|
for (const replacement of replacements) {
|
|
await rm(path, { recursive: true, force: true });
|
|
if (replacement === "directory") await mkdir(path);
|
|
if (replacement === "loop") await symlink("fixture.macho", path);
|
|
if (replacement === "unreadable") {
|
|
await writeFile(path, bytes);
|
|
await chmod(path, 0);
|
|
}
|
|
try {
|
|
const { error } = parseMcpToolError(await inspect());
|
|
const denied = replacement === "unreadable";
|
|
expect(error).toMatchObject({
|
|
code: denied ? "access_denied" : "artifact_changed",
|
|
category: denied ? "unavailable" : "integrity_mismatch",
|
|
details: {
|
|
operation: "inspect_native_dispatch_metadata",
|
|
path,
|
|
boundary: denied ? "filesystem-read" : "stable-artifact-read",
|
|
},
|
|
});
|
|
if (replacement === "unreadable")
|
|
expect(error.details?.system_code).toBe("EACCES");
|
|
else {
|
|
const code = {
|
|
missing: "ENOENT",
|
|
directory: "EISDIR",
|
|
loop: "ELOOP",
|
|
}[replacement];
|
|
expect(error.details?.reason).toContain(code);
|
|
}
|
|
expect(error.remediation.action).not.toContain("rea doctor");
|
|
} finally {
|
|
if (replacement === "unreadable") await chmod(path, 0o600);
|
|
}
|
|
await rm(path, { recursive: true, force: true });
|
|
await writeFile(path, bytes);
|
|
expect(await successfulRead()).toEqual(control);
|
|
}
|
|
const closed = await client.callTool({
|
|
name: "close_binary",
|
|
arguments: {},
|
|
});
|
|
expect(closed.isError).not.toBe(true);
|
|
expect(stderr).toBe("");
|
|
} finally {
|
|
await client.close();
|
|
await transport.close();
|
|
}
|
|
};
|
|
|
|
describe.skipIf(process.platform !== "darwin")(
|
|
"bound dispatch target read refusals",
|
|
() => {
|
|
it("reports missing, directory and symlink-loop replacements as artifact changes and recovers in the same session", async () => {
|
|
await inspectReplacements(["missing", "directory", "loop"]);
|
|
}, 30_000);
|
|
|
|
it.skipIf(process.getuid?.() === 0)(
|
|
"keeps permission denial distinct and recovers after restoring read access",
|
|
async () => {
|
|
await inspectReplacements(["unreadable"]);
|
|
},
|
|
30_000,
|
|
);
|
|
},
|
|
);
|