Files
suaveplan/packages/devtools/tooling/mcp/src/transport/http-sse/http-sse.test.ts
2026-05-27 23:29:53 +01:00

124 lines
4.1 KiB
TypeScript

import { describe, expect, it } from "@suaveplan/testing/runner";
import { MCP_CODES } from "../../error-codes/error-codes.js";
import type { JsonRpcMessage } from "../../schemas/schemas.js";
import type {
EventSourceCtor,
EventSourceLike,
FetchLike,
} from "../../types/types.js";
import { createHttpSseTransport } from "./http-sse.js";
class FakeEventSource implements EventSourceLike {
public static instances: FakeEventSource[] = [];
private listeners: ((ev: { data: string }) => void)[] = [];
private _closed = false;
public constructor(public readonly url: string) {
FakeEventSource.instances.push(this);
}
public addEventListener(
_type: "message",
handler: (ev: { data: string }) => void
): void {
this.listeners.push(handler);
}
public close(): void {
this._closed = true;
}
public get closed(): boolean {
return this._closed;
}
public emit(data: string): void {
for (const l of this.listeners) l({ data });
}
}
const FakeEventSourceCtor = FakeEventSource as unknown as EventSourceCtor;
describe("HttpSseTransport", () => {
it("posts requests via fetchImpl", { timeout: 5000 }, async () => {
const calls: Array<{ url: string; body: string }> = [];
const fetchImpl: FetchLike = async (url, init) => {
calls.push({ url, body: init.body });
return {
ok: true,
status: 200,
text: async () => "",
};
};
FakeEventSource.instances = [];
const t = createHttpSseTransport({
endpoint: "http://x",
fetchImpl,
eventSourceImpl: FakeEventSourceCtor,
});
await t.send({ jsonrpc: "2.0", id: 1, method: "ping" });
expect(calls.length).toBe(1);
expect(calls[0]?.url).toBe("http://x");
await t.close();
});
it("dispatches messages from eventSourceImpl", {
timeout: 5000,
}, async () => {
FakeEventSource.instances = [];
const t = createHttpSseTransport({
endpoint: "http://x",
fetchImpl: async () => ({
ok: true,
status: 200,
text: async () => "",
}),
eventSourceImpl: FakeEventSourceCtor,
});
const received: JsonRpcMessage[] = [];
t.onMessage((m) => received.push(m));
const es = FakeEventSource.instances[0];
if (es === undefined) throw new Error("no es");
es.emit(
JSON.stringify({ jsonrpc: "2.0", id: 1, result: { ok: true } })
);
es.emit("garbage-not-json");
// JSON-valid but schema-invalid — exercises validated.success false.
es.emit(JSON.stringify({ not: "a jsonrpc message" }));
await Promise.resolve();
expect(received.length).toBe(1);
await t.close();
});
it("maps non-2xx responses to TRANSPORT_ERROR", {
timeout: 5000,
}, async () => {
FakeEventSource.instances = [];
const t = createHttpSseTransport({
endpoint: "http://x",
fetchImpl: async () => ({
ok: false,
status: 503,
text: async () => "boom",
}),
eventSourceImpl: FakeEventSourceCtor,
});
await expect(
t.send({ jsonrpc: "2.0", id: 1, method: "ping" })
).rejects.toMatchObject({ code: MCP_CODES.TRANSPORT_ERROR });
await t.close();
});
it("raises TRANSPORT_CLOSED after close", { timeout: 5000 }, async () => {
FakeEventSource.instances = [];
const t = createHttpSseTransport({
endpoint: "http://x",
fetchImpl: async () => ({
ok: true,
status: 200,
text: async () => "",
}),
eventSourceImpl: FakeEventSourceCtor,
});
await t.close();
await expect(
t.send({ jsonrpc: "2.0", id: 1, method: "ping" })
).rejects.toMatchObject({ code: MCP_CODES.TRANSPORT_CLOSED });
});
});