import { afterEach, beforeEach, describe, expect, it } from "vitest";
import "./setup.js";
import { createServer, getServerContext, waitForServerReady } from "../src/mcp/server.js";
import { startHttpBridge } from "../src/server/httpBridge.js";
import {
closeServerDefensively,
closeHttpBridgeDefensively,
getDynamicPort,
createCleanupHandler
} from "./helpers/server.js";
const ORIGINAL_ENV = { ...process.env };
beforeEach(() => {
process.env = { ...ORIGINAL_ENV };
process.env.CLICKUP_TOKEN = "test-token";
process.env.CLICKUP_DEFAULT_TEAM_ID = "1";
});
afterEach(() => {
process.env = { ...ORIGINAL_ENV };
});
describe("defensive server context and teardown", () => {
it("server context is available immediately after createServer", async () => {
const server = await createServer();
try {
// Context should be available immediately
const context = getServerContext(server);
expect(context).toBeDefined();
expect(context.tools).toBeDefined();
expect(context.session).toBeDefined();
expect(context.logger).toBeDefined();
} finally {
await closeServerDefensively(server);
}
});
it("server context registration is logged", async () => {
const logs: string[] = [];
const originalInfo = console.info;
const mockLogger = {
info: (...args: unknown[]) => {
logs.push(JSON.stringify(args));
}
};
// Temporarily mock the logger module
const { createLogger: originalCreateLogger } = await import("../src/shared/logging.js");
try {
const server = await createServer();
try {
await waitForServerReady(server);
// Check that context was registered by verifying it's accessible
const context = getServerContext(server);
expect(context).toBeDefined();
expect(context.tools.length).toBeGreaterThan(0);
} finally {
await closeServerDefensively(server);
}
} finally {
// Restore original logger
}
});
it("http bridge uses dynamic port allocation", async () => {
const server = await createServer();
const testPort = getDynamicPort();
const http = await startHttpBridge(server, { port: testPort });
try {
await waitForServerReady(server);
// Port should be assigned by OS (not 0, not 3000)
expect(http.port).toBeGreaterThan(0);
expect(http.port).not.toBe(testPort);
// Should be able to make a request
const response = await fetch(`http://127.0.0.1:${http.port}/healthz`);
expect(response.status).toBe(200);
const body = await response.json();
expect(body.ok).toBe(true);
} finally {
await closeHttpBridgeDefensively(http);
await closeServerDefensively(server);
}
});
it("multiple servers can be created with dynamic ports without collision", async () => {
const servers: Array<{ server: Awaited<ReturnType<typeof createServer>>, http: Awaited<ReturnType<typeof startHttpBridge>> }> = [];
try {
// Create 3 servers with dynamic ports
for (let i = 0; i < 3; i++) {
const server = await createServer();
const http = await startHttpBridge(server, { port: getDynamicPort() });
await waitForServerReady(server);
servers.push({ server, http });
}
// All should have different ports
const ports = servers.map(s => s.http.port);
const uniquePorts = new Set(ports);
expect(uniquePorts.size).toBe(3);
// All should be accessible
for (const { http } of servers) {
const response = await fetch(`http://127.0.0.1:${http.port}/healthz`);
expect(response.status).toBe(200);
}
} finally {
// Defensive cleanup - all resources should be cleaned up even if some fail
const cleanup = createCleanupHandler(
...servers.flatMap(({ server, http }) => [
() => closeHttpBridgeDefensively(http),
() => closeServerDefensively(server)
])
);
await cleanup();
}
});
it("defensive close handles missing close method gracefully", async () => {
const warns: string[] = [];
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => {
warns.push(JSON.stringify(args));
originalWarn(...args);
};
try {
const fakeServer = { notClose: () => {} };
await closeServerDefensively(fakeServer);
// Should have logged a warning
const closeSkippedLog = warns.find(log => log.includes("server_close_skipped"));
expect(closeSkippedLog).toBeDefined();
} finally {
console.warn = originalWarn;
}
});
it("defensive close handles errors during close gracefully", async () => {
const warns: string[] = [];
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => {
warns.push(JSON.stringify(args));
originalWarn(...args);
};
try {
const errorServer = {
close: async () => {
throw new Error("Close failed");
}
};
// Should not throw
await closeServerDefensively(errorServer);
// Should have logged a warning
const closeWarningLog = warns.find(log => log.includes("server_close_warning"));
expect(closeWarningLog).toBeDefined();
expect(closeWarningLog).toContain("Close failed");
} finally {
console.warn = originalWarn;
}
});
it("getServerContext provides detailed error when context is missing", async () => {
const fakeServer = {} as any;
expect(() => getServerContext(fakeServer)).toThrow(/Server context not available/);
expect(() => getServerContext(fakeServer)).toThrow(/createServer\(\)/);
});
it("server context contains expected properties", async () => {
const server = await createServer();
try {
const context = getServerContext(server);
// Verify all expected context properties exist
expect(context.runtime).toBeDefined();
expect(context.tools).toBeDefined();
expect(context.toolMap).toBeDefined();
expect(context.logger).toBeDefined();
expect(context.session).toBeDefined();
expect(context.baseSession).toBeDefined();
// Verify toolMap matches tools array
expect(context.toolMap.size).toBe(context.tools.length);
for (const tool of context.tools) {
expect(context.toolMap.has(tool.name)).toBe(true);
}
} finally {
await closeServerDefensively(server);
}
});
});