mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
* fix(web): refuse an existing script-export destination as invalid input export_web_scripts refused an existing output directory or link correctly, but reported it as artifact_operation_failed / execution_failure with no field issue or recovery. SafeOutputTree.create raises that refusal as a path failure before any output tree exists, and the export mapper wrapped it as an artifact failure. Attribute pre-admission path refusals to output_directory as an AnalysisInputError, as environment export already does for its directory. Both now share one helper owned beside SafeOutputTree; failures after the tree is admitted keep their artifact semantics. Fixes #6029 * fix(artifacts): type SafeOutputTree destination refusals Admission conversion treated any cleanup-free path failure raised before admission as a refusal of the selected directory, so an acquired-tree setup failure, such as being unable to restrict a created root to 0700, blamed valid output_directory or directory input. SafeOutputTree.create now raises SafeOutputTreeDestinationRefusal for the refusals of the selection itself: a relative or reserved root, an existing root, and an accepted existing root that is not an empty directory. The shared conversion accepts only that type, directly or wrapped by SafeOutputTreeCreationFailure, so script and environment export no longer track admission separately.
139 lines
5.0 KiB
TypeScript
139 lines
5.0 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { access, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
|
|
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
|
|
import { expect, it, onTestFinished } from "vitest";
|
|
|
|
import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js";
|
|
import { silentLogger } from "../../../src/logger.js";
|
|
import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js";
|
|
import { registerWebScriptTool } from "../../../src/server/registerWebScriptTool.js";
|
|
import { ToolResultDelivery } from "../../../src/server/toolResult.js";
|
|
import { readWithoutFifoWriter } from "../../fixtures/fifoInput.js";
|
|
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { scriptScenarioFixture } from "../../fixtures/webScriptCapture.js";
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"returns a capture_path invalid_request for a named pipe without waiting or publishing output",
|
|
async () => {
|
|
const root = await createTestTempDirectory("rea-script-selection-mcp-");
|
|
const capturePath = join(root, "capture.pipe");
|
|
const outputDirectory = join(root, "export");
|
|
await promisify(execFile)("mkfifo", [capturePath]);
|
|
|
|
const server = new EvidenceMcpServer(
|
|
{ name: "script-selection", version: "1" },
|
|
{},
|
|
undefined,
|
|
new ToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE),
|
|
);
|
|
const artifactResources = new ArtifactResourceScope();
|
|
registerWebScriptTool(server, {
|
|
logger: silentLogger,
|
|
recordEvidence: undefined,
|
|
artifactResources: artifactResources,
|
|
});
|
|
const client = new Client({ name: "script-selection-test", version: "1" });
|
|
onTestFinished(async () => {
|
|
await client.close();
|
|
await server.close();
|
|
await artifactResources.close();
|
|
});
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
|
|
const outcome = await readWithoutFifoWriter(capturePath, () =>
|
|
client.callTool({
|
|
name: "export_web_scripts",
|
|
arguments: {
|
|
capture_path: capturePath,
|
|
output_directory: outputDirectory,
|
|
},
|
|
}),
|
|
);
|
|
expect(outcome.state).toBe("completed");
|
|
if (outcome.state !== "completed")
|
|
throw new Error("MCP capture selection waited for a FIFO writer");
|
|
expect(parseMcpToolError(outcome.result)).toMatchObject({
|
|
error: {
|
|
code: "invalid_request",
|
|
details: {
|
|
operation: "export_web_scripts",
|
|
issues: [
|
|
{
|
|
path: ["capture_path"],
|
|
reason: "invalid_value",
|
|
message: expect.stringContaining("regular file"),
|
|
},
|
|
],
|
|
},
|
|
},
|
|
});
|
|
expect(JSON.stringify(outcome.result)).toContain(capturePath);
|
|
await expect(access(outputDirectory)).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
},
|
|
);
|
|
|
|
it("refuses an existing output directory as invalid output_directory input", async () => {
|
|
const root = await createTestTempDirectory("rea-script-export-existing-mcp-");
|
|
const capturePath = join(root, "capture.json");
|
|
await writeFile(capturePath, JSON.stringify(scriptScenarioFixture()));
|
|
const outputDirectory = join(root, "retained");
|
|
await mkdir(outputDirectory);
|
|
await writeFile(join(outputDirectory, "marker.txt"), "keep");
|
|
const server = new EvidenceMcpServer(
|
|
{ name: "script-export-existing", version: "1" },
|
|
{},
|
|
undefined,
|
|
new ToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE),
|
|
);
|
|
const artifactResources = new ArtifactResourceScope();
|
|
registerWebScriptTool(server, {
|
|
logger: silentLogger,
|
|
recordEvidence: undefined,
|
|
artifactResources,
|
|
});
|
|
const client = new Client({ name: "script-export-existing", version: "1" });
|
|
onTestFinished(async () => {
|
|
await client.close();
|
|
await server.close();
|
|
await artifactResources.close();
|
|
});
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
|
|
const result = await client.callTool({
|
|
name: "export_web_scripts",
|
|
arguments: { capture_path: capturePath, output_directory: outputDirectory },
|
|
});
|
|
expect(parseMcpToolError(result).error).toMatchObject({
|
|
code: "invalid_request",
|
|
category: "invalid_input",
|
|
recovery: {
|
|
kind: "inspect_input_contract",
|
|
operation: "export_web_scripts",
|
|
},
|
|
details: {
|
|
operation: "export_web_scripts",
|
|
issues: [
|
|
{
|
|
path: ["output_directory"],
|
|
reason: "invalid_value",
|
|
message: "Extraction output root already exists",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(await readdir(outputDirectory)).toEqual(["marker.txt"]);
|
|
});
|