SysDeck/klanker-gate/packages/mcp/src/monitor_test.ts

98 lines
3.5 KiB
TypeScript
Executable File

import { assertEquals } from "@std/assert";
import { MCPRegistry } from "./registry.ts";
import { MCPHealthMonitor } from "./monitor.ts";
/**
* Scripted streamable-http server whose `initialize` fails for the first
* `failInitUntil` attempts, then succeeds. Counts every initialize attempt so
* tests can prove a reconnect (an extra sync) was triggered.
*/
function countingFetch(failInitUntil: number): {
fetch: typeof fetch;
initCalls: () => number;
} {
let initCalls = 0;
const reply = (id: unknown, result: unknown): Promise<Response> =>
Promise.resolve(
new Response(
JSON.stringify({ jsonrpc: "2.0", id, result }),
{ headers: { "Content-Type": "application/json" } },
),
);
const impl: typeof fetch = (_input, init) => {
const body = JSON.parse(String(init?.body));
if (body.method === "initialize") {
initCalls++;
if (initCalls <= failInitUntil) {
return Promise.reject(new TypeError("connection refused"));
}
return reply(body.id, { protocolVersion: "2025-06-18" });
}
if (body.method === "tools/list") {
return reply(body.id, {
tools: [{ name: "ping", annotations: { readOnlyHint: true } }],
});
}
return reply(body.id ?? 0, {});
};
return { fetch: impl, initCalls: () => initCalls };
}
function flakyRegistry(fetchImpl: typeof fetch): MCPRegistry {
return new MCPRegistry(
[{
id: "flaky",
url: "http://flaky.test/rpc",
transport: "streamable-http",
enabled: true,
requestTimeoutMs: 500,
}],
fetchImpl,
);
}
Deno.test("consecutive failures below the threshold stay unhealthy (no reconnect)", async () => {
const counting = countingFetch(Number.POSITIVE_INFINITY);
const monitor = new MCPHealthMonitor(flakyRegistry(counting.fetch), 3);
await monitor.checkAll();
const health = monitor.statuses()[0];
assertEquals(health.status, "unhealthy");
assertEquals(health.consecutiveFailures, 1);
// One sync attempt only: the threshold gate did NOT fire a reconnect.
assertEquals(counting.initCalls(), 1);
});
Deno.test("threshold breach marks disconnected and attempts a reconnect", async () => {
const counting = countingFetch(Number.POSITIVE_INFINITY); // server never recovers
const monitor = new MCPHealthMonitor(flakyRegistry(counting.fetch), 2);
await monitor.checkAll(); // failure 1 -> unhealthy
assertEquals(monitor.statuses()[0].status, "unhealthy");
assertEquals(counting.initCalls(), 1);
await monitor.checkAll(); // failure 2 -> threshold -> reconnect (also fails)
const health = monitor.statuses()[0];
assertEquals(health.status, "disconnected");
assertEquals(health.consecutiveFailures, 2);
// Primary sync (#2) plus the reconnect sync = one extra initialize attempt.
assertEquals(counting.initCalls(), 3);
});
Deno.test("a reconnect that succeeds heals the client back to healthy", async () => {
// initialize fails on attempts 1 and 2, then succeeds from attempt 3 on.
const counting = countingFetch(2);
const monitor = new MCPHealthMonitor(flakyRegistry(counting.fetch), 2);
await monitor.checkAll(); // failure 1 -> unhealthy
assertEquals(monitor.statuses()[0].status, "unhealthy");
await monitor.checkAll(); // failure 2 -> threshold -> reconnect succeeds
const health = monitor.statuses()[0];
assertEquals(health.status, "healthy");
assertEquals(health.consecutiveFailures, 0);
assertEquals(health.toolCount, 1);
// The 3rd initialize attempt is the reconnect that brought the server back.
assertEquals(counting.initCalls(), 3);
});