mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 00:30:19 +02:00
Recognize Slack's exact disabled-app response and return actionable setup instructions from catalog and health routes. Bound response parsing and keep unknown upstream errors reportable without exposing provider settings links. Verified 361 focused tests, server typecheck, and authenticated discovery. The three route regressions fail before this change and pass afterward. Co-Authored-By: Paperclip <noreply@paperclip.ing>
70 lines
3.1 KiB
TypeScript
70 lines
3.1 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { isSlackMcpAccessDisabledResponse } from "./slack-mcp-error.js";
|
|
|
|
const endpoint = "https://mcp.slack.com/mcp";
|
|
const message = "App is not enabled for Slack MCP server access. Please enable it here: https://api.slack.com/apps/fixture/mcp";
|
|
const payload = { jsonrpc: "2.0", id: null, error: { code: -32600, message } };
|
|
const response = (body: unknown = payload, status = 400, contentType = "application/json") =>
|
|
new Response(JSON.stringify(body), { status, headers: { "content-type": contentType } });
|
|
|
|
afterEach(() => vi.useRealTimers());
|
|
|
|
describe("Slack MCP app setup failure", () => {
|
|
it("recognizes Slack's explicit disabled-app response, including its null RPC id", async () => {
|
|
expect(await isSlackMcpAccessDisabledResponse(endpoint, response())).toBe(true);
|
|
});
|
|
|
|
it.each([
|
|
"https://other.example.test/mcp",
|
|
"https://mcp.slack.com.example.test/mcp",
|
|
"http://mcp.slack.com/mcp",
|
|
"https://mcp.slack.com:8443/mcp",
|
|
"https://mcp.slack.com/other",
|
|
])("does not classify matching text from %s", async (url) => {
|
|
const upstream = response();
|
|
expect(await isSlackMcpAccessDisabledResponse(url, upstream)).toBe(false);
|
|
expect(upstream.bodyUsed).toBe(false);
|
|
});
|
|
|
|
it.each([401, 403, 429, 500, 503])("does not hide HTTP %i behind a setup error", async (status) => {
|
|
expect(await isSlackMcpAccessDisabledResponse(endpoint, response(payload, status))).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
{ ...payload, error: { code: -32600, message: "Missing session ID" } },
|
|
{ ...payload, error: { code: -32603, message } },
|
|
{ ...payload, error: message },
|
|
{ ...payload, jsonrpc: "1.0" },
|
|
null,
|
|
])("keeps unrecognized errors reportable: %j", async (body) => {
|
|
expect(await isSlackMcpAccessDisabledResponse(endpoint, response(body))).toBe(false);
|
|
});
|
|
|
|
it("rejects non-JSON content and malformed JSON", async () => {
|
|
expect(await isSlackMcpAccessDisabledResponse(endpoint, response(payload, 400, "text/html"))).toBe(false);
|
|
expect(await isSlackMcpAccessDisabledResponse(endpoint,
|
|
new Response("{", { status: 400, headers: { "content-type": "application/json" } }))).toBe(false);
|
|
});
|
|
|
|
it("bounds and cancels oversized error responses", async () => {
|
|
const cancel = vi.fn();
|
|
const body = new ReadableStream({
|
|
start(controller) { controller.enqueue(new TextEncoder().encode(JSON.stringify({ ...payload, extra: "x".repeat(8192) }))); },
|
|
cancel,
|
|
});
|
|
expect(await isSlackMcpAccessDisabledResponse(endpoint,
|
|
new Response(body, { status: 400, headers: { "content-type": "application/json" } }))).toBe(false);
|
|
expect(cancel).toHaveBeenCalled();
|
|
});
|
|
|
|
it("bounds and cancels a stalled error body", async () => {
|
|
vi.useFakeTimers();
|
|
const cancel = vi.fn();
|
|
const result = isSlackMcpAccessDisabledResponse(endpoint,
|
|
new Response(new ReadableStream({ cancel }), { status: 400, headers: { "content-type": "application/json" } }));
|
|
await vi.advanceTimersByTimeAsync(2_000);
|
|
expect(await result).toBe(false);
|
|
expect(cancel).toHaveBeenCalled();
|
|
});
|
|
});
|