feat(FN-2938): merge fusion/fn-2938
- fix(FN-2938): satisfy lint for control-request debug logging - test(FN-2938): complete Step 4 — update protocol regression tests - feat(FN-2938): complete Step 3 — remove stdin control request routing - feat(FN-2938): complete Step 2 — make control handler pure - feat(FN-2938): complete Step 1 — fix stdin EOF and spawn flags - feat(FN-2929): merge fusion/fn-2929 Fusion-Task-Id: FN-2938
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { PassThrough } from "node:stream";
|
||||
import type { ClaudeControlRequest } from "../types";
|
||||
import {
|
||||
handleControlRequest,
|
||||
@@ -7,13 +6,6 @@ import {
|
||||
MCP_PREFIX,
|
||||
} from "../control-handler";
|
||||
|
||||
function createMockStdin() {
|
||||
const stream = new PassThrough();
|
||||
const chunks: string[] = [];
|
||||
stream.on("data", (data: Buffer) => chunks.push(data.toString()));
|
||||
return { stream, chunks };
|
||||
}
|
||||
|
||||
function makeControlRequest(
|
||||
toolName: string,
|
||||
requestId = "req-test-001",
|
||||
@@ -44,147 +36,128 @@ describe("control-handler", () => {
|
||||
});
|
||||
|
||||
describe("denies custom MCP tools (mcp__custom-tools__*)", () => {
|
||||
it("denies mcp__custom-tools__weather and returns false", () => {
|
||||
const { stream, chunks } = createMockStdin();
|
||||
it("denies mcp__custom-tools__weather", () => {
|
||||
const msg = makeControlRequest("mcp__custom-tools__weather");
|
||||
|
||||
const result = handleControlRequest(msg, stream);
|
||||
const result = handleControlRequest(msg);
|
||||
|
||||
expect(result).toBe(false);
|
||||
const response = JSON.parse(chunks[0].trim());
|
||||
expect(response.response.response.behavior).toBe("deny");
|
||||
expect(response.response.response.message).toBe(
|
||||
expect(result.allowed).toBe(false);
|
||||
expect(result.response.response.response.behavior).toBe("deny");
|
||||
expect(result.response.response.response.message).toBe(
|
||||
TOOL_EXECUTION_DENIED_MESSAGE,
|
||||
);
|
||||
});
|
||||
|
||||
it("denies mcp__custom-tools__deploy", () => {
|
||||
const { stream, chunks } = createMockStdin();
|
||||
const msg = makeControlRequest("mcp__custom-tools__deploy");
|
||||
|
||||
const result = handleControlRequest(msg, stream);
|
||||
const result = handleControlRequest(msg);
|
||||
|
||||
expect(result).toBe(false);
|
||||
const response = JSON.parse(chunks[0].trim());
|
||||
expect(response.response.response.behavior).toBe("deny");
|
||||
expect(result.allowed).toBe(false);
|
||||
expect(result.response.response.response.behavior).toBe("deny");
|
||||
});
|
||||
});
|
||||
|
||||
describe("allows user MCP tools and other tools", () => {
|
||||
it("allows user MCP tool mcp__database__query and returns true", () => {
|
||||
const { stream, chunks } = createMockStdin();
|
||||
it("allows user MCP tool mcp__database__query", () => {
|
||||
const msg = makeControlRequest("mcp__database__query");
|
||||
|
||||
const result = handleControlRequest(msg, stream);
|
||||
const result = handleControlRequest(msg);
|
||||
|
||||
expect(result).toBe(true);
|
||||
const response = JSON.parse(chunks[0].trim());
|
||||
expect(response.response.response.behavior).toBe("allow");
|
||||
expect(result.allowed).toBe(true);
|
||||
expect(result.response.response.response.behavior).toBe("allow");
|
||||
});
|
||||
|
||||
it("allows built-in tool Read", () => {
|
||||
const { stream, chunks } = createMockStdin();
|
||||
const msg = makeControlRequest("Read");
|
||||
|
||||
const result = handleControlRequest(msg, stream);
|
||||
const result = handleControlRequest(msg);
|
||||
|
||||
expect(result).toBe(true);
|
||||
const response = JSON.parse(chunks[0].trim());
|
||||
expect(response.response.response.behavior).toBe("allow");
|
||||
expect(result.allowed).toBe(true);
|
||||
expect(result.response.response.response.behavior).toBe("allow");
|
||||
});
|
||||
|
||||
it("allows internal tools like ToolSearch", () => {
|
||||
const { stream, chunks } = createMockStdin();
|
||||
const msg = makeControlRequest("ToolSearch");
|
||||
|
||||
const result = handleControlRequest(msg, stream);
|
||||
const result = handleControlRequest(msg);
|
||||
|
||||
expect(result).toBe(true);
|
||||
const response = JSON.parse(chunks[0].trim());
|
||||
expect(response.response.response.behavior).toBe("allow");
|
||||
expect(result.allowed).toBe(true);
|
||||
expect(result.response.response.response.behavior).toBe("allow");
|
||||
});
|
||||
|
||||
it("allows unknown tools", () => {
|
||||
const { stream, chunks } = createMockStdin();
|
||||
const msg = makeControlRequest("SomeUnknownTool");
|
||||
|
||||
const result = handleControlRequest(msg, stream);
|
||||
const result = handleControlRequest(msg);
|
||||
|
||||
expect(result).toBe(true);
|
||||
const response = JSON.parse(chunks[0].trim());
|
||||
expect(response.response.response.behavior).toBe("allow");
|
||||
expect(result.allowed).toBe(true);
|
||||
expect(result.response.response.response.behavior).toBe("allow");
|
||||
});
|
||||
});
|
||||
|
||||
describe("response format", () => {
|
||||
it("includes matching request_id", () => {
|
||||
const { stream, chunks } = createMockStdin();
|
||||
const msg = makeControlRequest("Read", "custom-req-id-42");
|
||||
|
||||
handleControlRequest(msg, stream);
|
||||
const result = handleControlRequest(msg);
|
||||
|
||||
const response = JSON.parse(chunks[0].trim());
|
||||
expect(response.request_id).toBe("custom-req-id-42");
|
||||
expect(result.response.request_id).toBe("custom-req-id-42");
|
||||
});
|
||||
|
||||
it("writes response as NDJSON (JSON + newline)", () => {
|
||||
const { stream, chunks } = createMockStdin();
|
||||
it("returns a JSON-serializable response object", () => {
|
||||
const msg = makeControlRequest("Read");
|
||||
|
||||
handleControlRequest(msg, stream);
|
||||
const result = handleControlRequest(msg);
|
||||
const serialized = JSON.stringify(result.response);
|
||||
|
||||
expect(chunks[0].endsWith("\n")).toBe(true);
|
||||
expect(() => JSON.parse(chunks[0].trim())).not.toThrow();
|
||||
expect(() => JSON.parse(serialized)).not.toThrow();
|
||||
});
|
||||
|
||||
it("deny response includes message field", () => {
|
||||
const { stream, chunks } = createMockStdin();
|
||||
const msg = makeControlRequest("mcp__custom-tools__foo");
|
||||
|
||||
handleControlRequest(msg, stream);
|
||||
const result = handleControlRequest(msg);
|
||||
|
||||
const response = JSON.parse(chunks[0].trim());
|
||||
expect(response.response.response.message).toBe(
|
||||
expect(result.response.response.response.message).toBe(
|
||||
TOOL_EXECUTION_DENIED_MESSAGE,
|
||||
);
|
||||
});
|
||||
|
||||
it("allow response does not include a message field", () => {
|
||||
const { stream, chunks } = createMockStdin();
|
||||
const msg = makeControlRequest("mcp__database__query");
|
||||
|
||||
handleControlRequest(msg, stream);
|
||||
const result = handleControlRequest(msg);
|
||||
|
||||
const response = JSON.parse(chunks[0].trim());
|
||||
expect(response.response.response.message).toBeUndefined();
|
||||
expect(result.response.response.response.message).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("malformed input", () => {
|
||||
it("returns false for missing request_id", () => {
|
||||
const { stream } = createMockStdin();
|
||||
it("returns denied decision object for missing request_id", () => {
|
||||
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
const msg = {
|
||||
type: "control_request",
|
||||
} as unknown as ClaudeControlRequest;
|
||||
const result = handleControlRequest(msg, stream);
|
||||
const result = handleControlRequest(msg);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(result.allowed).toBe(false);
|
||||
expect(result.response.response.response.behavior).toBe("deny");
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("returns false for missing request object", () => {
|
||||
const { stream } = createMockStdin();
|
||||
it("returns denied decision object for missing request object", () => {
|
||||
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
const msg = {
|
||||
type: "control_request",
|
||||
request_id: "req-001",
|
||||
} as unknown as ClaudeControlRequest;
|
||||
const result = handleControlRequest(msg, stream);
|
||||
const result = handleControlRequest(msg);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(result.allowed).toBe(false);
|
||||
expect(result.response.response.response.behavior).toBe("deny");
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user