2026-04-10 18:28:03 +01:00
|
|
|
import { describe, it, expect, afterAll, afterEach } from 'vitest';
|
|
|
|
|
import http from 'node:http';
|
fix(mcplocal): stop the 30s proxy timeout killing agent turns
`mcpctl chat <agent>` failed with
HTTP 503 {"error":"service_unavailable","message":"Cannot reach mcpd daemon. Is it running?"}
while mcpd was answering /healthz in 32ms. The message was wrong in a way that
cost real debugging time: mcplocal was reaching mcpd fine and giving up after
30s. journalctl shows the signature plainly — statusCode 503 with
responseTime 30003.87 on POST /api/v1/agents/reviewer/chat.
This blocks the agentic-teams epic outright. An agent turn is a multi-turn
tool-use loop that runs for minutes by design, so a 30s ceiling on the chat path
is not a safety net, it is a guaranteed failure for every non-trivial turn.
Three defects, all in the same path:
1. One blanket budget for every forwarded route. DEFAULT_TIMEOUT_MS = 30_000 is
right for CRUD and wrong for chat. Chat, project chat, llm infer and
inference-task streams now get LONG_RUNNING_TIMEOUT_MS (600_000, override
with MCPLOCAL_LONG_TIMEOUT_MS) — matching STREAM_TIMEOUT_MS, which the CLI
already allowed. mcplocal in the middle was the binding constraint.
2. Timeouts were reported as connection failures. Split UpstreamTimeoutError
out of ConnectionError and map it to 504 with an accurate message that says
the daemon IS reachable. ConnectionError still means unreachable and still
returns 503. Verified nothing else branches on ConnectionError.
3. SSE was buffered. `forward()` reads the whole body through res.text(), so
even turns that finished in time arrived as one blob and the CLI's live
token output never appeared. Streaming routes now use forwardStream() and
pipe the body straight through, preserving content-type and
x-accel-buffering (dropping the latter lets intermediaries re-buffer and
reintroduces the stall).
Also closes the escape that produced the sibling `500 code:23` failure: the body
read in forward() was outside the try, so when mcpd had already written SSE
headers the raw DOMException reached Fastify unhandled.
Tests: 9 new proxy tests. The two that matter — "does not abort an agent chat
that outlives the CRUD budget" and "streams SSE through instead of buffering" —
were confirmed to FAIL against the pre-fix behaviour and pass after. Three
existing mcpd-client tests asserted the old taxonomy and were updated to assert
the new one deliberately.
Local: build clean, workspace 2375 passed, lint unchanged at 869.
NOT YET LIVE: mcplocal runs from the installed RPM, so this needs a package
rebuild + `systemctl --user restart mcplocal` to take effect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
2026-08-08 11:35:16 +01:00
|
|
|
import { McpdClient, ConnectionError, UpstreamTimeoutError } from '../src/http/mcpd-client.js';
|
2026-04-10 18:28:03 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a local HTTP server for testing McpdClient behavior.
|
|
|
|
|
* Returns the server and its URL.
|
|
|
|
|
*/
|
|
|
|
|
function createTestServer(
|
|
|
|
|
handler: (req: http.IncomingMessage, res: http.ServerResponse) => void,
|
|
|
|
|
): Promise<{ server: http.Server; url: string }> {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const server = http.createServer(handler);
|
|
|
|
|
server.listen(0, '127.0.0.1', () => {
|
|
|
|
|
const addr = server.address() as { port: number };
|
|
|
|
|
resolve({ server, url: `http://127.0.0.1:${addr.port}` });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('McpdClient', () => {
|
|
|
|
|
const servers: http.Server[] = [];
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
for (const s of servers) s.close();
|
|
|
|
|
servers.length = 0;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterAll(() => {
|
|
|
|
|
for (const s of servers) s.close();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('makes GET requests with auth header', async () => {
|
|
|
|
|
let capturedAuth = '';
|
|
|
|
|
const { server, url } = await createTestServer((req, res) => {
|
|
|
|
|
capturedAuth = req.headers['authorization'] ?? '';
|
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
|
|
|
res.end(JSON.stringify({ ok: true }));
|
|
|
|
|
});
|
|
|
|
|
servers.push(server);
|
|
|
|
|
|
|
|
|
|
const client = new McpdClient(url, 'my-token');
|
|
|
|
|
const result = await client.get<{ ok: boolean }>('/api/v1/test');
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual({ ok: true });
|
|
|
|
|
expect(capturedAuth).toBe('Bearer my-token');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('makes POST requests with JSON body', async () => {
|
|
|
|
|
let capturedBody = '';
|
|
|
|
|
const { server, url } = await createTestServer((req, res) => {
|
|
|
|
|
const chunks: Buffer[] = [];
|
|
|
|
|
req.on('data', (c: Buffer) => chunks.push(c));
|
|
|
|
|
req.on('end', () => {
|
|
|
|
|
capturedBody = Buffer.concat(chunks).toString();
|
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
|
|
|
res.end(JSON.stringify({ received: true }));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
servers.push(server);
|
|
|
|
|
|
|
|
|
|
const client = new McpdClient(url, 'tok');
|
|
|
|
|
const result = await client.post<{ received: boolean }>('/api/v1/proxy', { serverId: 's1' });
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual({ received: true });
|
|
|
|
|
expect(JSON.parse(capturedBody)).toEqual({ serverId: 's1' });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws ConnectionError on connection refused', async () => {
|
|
|
|
|
const client = new McpdClient('http://127.0.0.1:1', 'tok');
|
|
|
|
|
|
|
|
|
|
await expect(client.get('/test')).rejects.toThrow(ConnectionError);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws on 4xx/5xx responses', async () => {
|
|
|
|
|
const { server, url } = await createTestServer((_req, res) => {
|
|
|
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
|
|
|
res.end(JSON.stringify({ error: 'internal' }));
|
|
|
|
|
});
|
|
|
|
|
servers.push(server);
|
|
|
|
|
|
|
|
|
|
const client = new McpdClient(url, 'tok');
|
|
|
|
|
await expect(client.get('/test')).rejects.toThrow(/mcpd returned 500/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Timeout behavior ──
|
|
|
|
|
|
fix(mcplocal): stop the 30s proxy timeout killing agent turns
`mcpctl chat <agent>` failed with
HTTP 503 {"error":"service_unavailable","message":"Cannot reach mcpd daemon. Is it running?"}
while mcpd was answering /healthz in 32ms. The message was wrong in a way that
cost real debugging time: mcplocal was reaching mcpd fine and giving up after
30s. journalctl shows the signature plainly — statusCode 503 with
responseTime 30003.87 on POST /api/v1/agents/reviewer/chat.
This blocks the agentic-teams epic outright. An agent turn is a multi-turn
tool-use loop that runs for minutes by design, so a 30s ceiling on the chat path
is not a safety net, it is a guaranteed failure for every non-trivial turn.
Three defects, all in the same path:
1. One blanket budget for every forwarded route. DEFAULT_TIMEOUT_MS = 30_000 is
right for CRUD and wrong for chat. Chat, project chat, llm infer and
inference-task streams now get LONG_RUNNING_TIMEOUT_MS (600_000, override
with MCPLOCAL_LONG_TIMEOUT_MS) — matching STREAM_TIMEOUT_MS, which the CLI
already allowed. mcplocal in the middle was the binding constraint.
2. Timeouts were reported as connection failures. Split UpstreamTimeoutError
out of ConnectionError and map it to 504 with an accurate message that says
the daemon IS reachable. ConnectionError still means unreachable and still
returns 503. Verified nothing else branches on ConnectionError.
3. SSE was buffered. `forward()` reads the whole body through res.text(), so
even turns that finished in time arrived as one blob and the CLI's live
token output never appeared. Streaming routes now use forwardStream() and
pipe the body straight through, preserving content-type and
x-accel-buffering (dropping the latter lets intermediaries re-buffer and
reintroduces the stall).
Also closes the escape that produced the sibling `500 code:23` failure: the body
read in forward() was outside the try, so when mcpd had already written SSE
headers the raw DOMException reached Fastify unhandled.
Tests: 9 new proxy tests. The two that matter — "does not abort an agent chat
that outlives the CRUD budget" and "streams SSE through instead of buffering" —
were confirmed to FAIL against the pre-fix behaviour and pass after. Three
existing mcpd-client tests asserted the old taxonomy and were updated to assert
the new one deliberately.
Local: build clean, workspace 2375 passed, lint unchanged at 869.
NOT YET LIVE: mcplocal runs from the installed RPM, so this needs a package
rebuild + `systemctl --user restart mcplocal` to take effect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
2026-08-08 11:35:16 +01:00
|
|
|
it('times out on slow responses and throws UpstreamTimeoutError', async () => {
|
2026-04-10 18:28:03 +01:00
|
|
|
const { server, url } = await createTestServer((_req, _res) => {
|
|
|
|
|
// Never respond — simulates a hanging upstream tool call
|
|
|
|
|
});
|
|
|
|
|
servers.push(server);
|
|
|
|
|
|
|
|
|
|
// Use a very short timeout for the test
|
|
|
|
|
const client = new McpdClient(url, 'tok', undefined, 500);
|
|
|
|
|
|
|
|
|
|
const start = Date.now();
|
|
|
|
|
await expect(client.post('/api/v1/mcp/proxy', { serverId: 's1' })).rejects.toThrow(
|
fix(mcplocal): stop the 30s proxy timeout killing agent turns
`mcpctl chat <agent>` failed with
HTTP 503 {"error":"service_unavailable","message":"Cannot reach mcpd daemon. Is it running?"}
while mcpd was answering /healthz in 32ms. The message was wrong in a way that
cost real debugging time: mcplocal was reaching mcpd fine and giving up after
30s. journalctl shows the signature plainly — statusCode 503 with
responseTime 30003.87 on POST /api/v1/agents/reviewer/chat.
This blocks the agentic-teams epic outright. An agent turn is a multi-turn
tool-use loop that runs for minutes by design, so a 30s ceiling on the chat path
is not a safety net, it is a guaranteed failure for every non-trivial turn.
Three defects, all in the same path:
1. One blanket budget for every forwarded route. DEFAULT_TIMEOUT_MS = 30_000 is
right for CRUD and wrong for chat. Chat, project chat, llm infer and
inference-task streams now get LONG_RUNNING_TIMEOUT_MS (600_000, override
with MCPLOCAL_LONG_TIMEOUT_MS) — matching STREAM_TIMEOUT_MS, which the CLI
already allowed. mcplocal in the middle was the binding constraint.
2. Timeouts were reported as connection failures. Split UpstreamTimeoutError
out of ConnectionError and map it to 504 with an accurate message that says
the daemon IS reachable. ConnectionError still means unreachable and still
returns 503. Verified nothing else branches on ConnectionError.
3. SSE was buffered. `forward()` reads the whole body through res.text(), so
even turns that finished in time arrived as one blob and the CLI's live
token output never appeared. Streaming routes now use forwardStream() and
pipe the body straight through, preserving content-type and
x-accel-buffering (dropping the latter lets intermediaries re-buffer and
reintroduces the stall).
Also closes the escape that produced the sibling `500 code:23` failure: the body
read in forward() was outside the try, so when mcpd had already written SSE
headers the raw DOMException reached Fastify unhandled.
Tests: 9 new proxy tests. The two that matter — "does not abort an agent chat
that outlives the CRUD budget" and "streams SSE through instead of buffering" —
were confirmed to FAIL against the pre-fix behaviour and pass after. Three
existing mcpd-client tests asserted the old taxonomy and were updated to assert
the new one deliberately.
Local: build clean, workspace 2375 passed, lint unchanged at 869.
NOT YET LIVE: mcplocal runs from the installed RPM, so this needs a package
rebuild + `systemctl --user restart mcplocal` to take effect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
2026-08-08 11:35:16 +01:00
|
|
|
/did not respond within/,
|
2026-04-10 18:28:03 +01:00
|
|
|
);
|
|
|
|
|
const elapsed = Date.now() - start;
|
|
|
|
|
|
|
|
|
|
// Should have timed out around 500ms, not hung for seconds
|
|
|
|
|
expect(elapsed).toBeGreaterThanOrEqual(450);
|
|
|
|
|
expect(elapsed).toBeLessThan(3000);
|
|
|
|
|
});
|
|
|
|
|
|
fix(mcplocal): stop the 30s proxy timeout killing agent turns
`mcpctl chat <agent>` failed with
HTTP 503 {"error":"service_unavailable","message":"Cannot reach mcpd daemon. Is it running?"}
while mcpd was answering /healthz in 32ms. The message was wrong in a way that
cost real debugging time: mcplocal was reaching mcpd fine and giving up after
30s. journalctl shows the signature plainly — statusCode 503 with
responseTime 30003.87 on POST /api/v1/agents/reviewer/chat.
This blocks the agentic-teams epic outright. An agent turn is a multi-turn
tool-use loop that runs for minutes by design, so a 30s ceiling on the chat path
is not a safety net, it is a guaranteed failure for every non-trivial turn.
Three defects, all in the same path:
1. One blanket budget for every forwarded route. DEFAULT_TIMEOUT_MS = 30_000 is
right for CRUD and wrong for chat. Chat, project chat, llm infer and
inference-task streams now get LONG_RUNNING_TIMEOUT_MS (600_000, override
with MCPLOCAL_LONG_TIMEOUT_MS) — matching STREAM_TIMEOUT_MS, which the CLI
already allowed. mcplocal in the middle was the binding constraint.
2. Timeouts were reported as connection failures. Split UpstreamTimeoutError
out of ConnectionError and map it to 504 with an accurate message that says
the daemon IS reachable. ConnectionError still means unreachable and still
returns 503. Verified nothing else branches on ConnectionError.
3. SSE was buffered. `forward()` reads the whole body through res.text(), so
even turns that finished in time arrived as one blob and the CLI's live
token output never appeared. Streaming routes now use forwardStream() and
pipe the body straight through, preserving content-type and
x-accel-buffering (dropping the latter lets intermediaries re-buffer and
reintroduces the stall).
Also closes the escape that produced the sibling `500 code:23` failure: the body
read in forward() was outside the try, so when mcpd had already written SSE
headers the raw DOMException reached Fastify unhandled.
Tests: 9 new proxy tests. The two that matter — "does not abort an agent chat
that outlives the CRUD budget" and "streams SSE through instead of buffering" —
were confirmed to FAIL against the pre-fix behaviour and pass after. Three
existing mcpd-client tests asserted the old taxonomy and were updated to assert
the new one deliberately.
Local: build clean, workspace 2375 passed, lint unchanged at 869.
NOT YET LIVE: mcplocal runs from the installed RPM, so this needs a package
rebuild + `systemctl --user restart mcplocal` to take effect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
2026-08-08 11:35:16 +01:00
|
|
|
it('timeout is NOT a ConnectionError — a slow daemon is not an absent one', async () => {
|
2026-04-10 18:28:03 +01:00
|
|
|
const { server, url } = await createTestServer((_req, _res) => {
|
|
|
|
|
// Never respond
|
|
|
|
|
});
|
|
|
|
|
servers.push(server);
|
|
|
|
|
|
|
|
|
|
const client = new McpdClient(url, 'tok', undefined, 200);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await client.get('/test');
|
|
|
|
|
expect.unreachable('Should have thrown');
|
|
|
|
|
} catch (err) {
|
fix(mcplocal): stop the 30s proxy timeout killing agent turns
`mcpctl chat <agent>` failed with
HTTP 503 {"error":"service_unavailable","message":"Cannot reach mcpd daemon. Is it running?"}
while mcpd was answering /healthz in 32ms. The message was wrong in a way that
cost real debugging time: mcplocal was reaching mcpd fine and giving up after
30s. journalctl shows the signature plainly — statusCode 503 with
responseTime 30003.87 on POST /api/v1/agents/reviewer/chat.
This blocks the agentic-teams epic outright. An agent turn is a multi-turn
tool-use loop that runs for minutes by design, so a 30s ceiling on the chat path
is not a safety net, it is a guaranteed failure for every non-trivial turn.
Three defects, all in the same path:
1. One blanket budget for every forwarded route. DEFAULT_TIMEOUT_MS = 30_000 is
right for CRUD and wrong for chat. Chat, project chat, llm infer and
inference-task streams now get LONG_RUNNING_TIMEOUT_MS (600_000, override
with MCPLOCAL_LONG_TIMEOUT_MS) — matching STREAM_TIMEOUT_MS, which the CLI
already allowed. mcplocal in the middle was the binding constraint.
2. Timeouts were reported as connection failures. Split UpstreamTimeoutError
out of ConnectionError and map it to 504 with an accurate message that says
the daemon IS reachable. ConnectionError still means unreachable and still
returns 503. Verified nothing else branches on ConnectionError.
3. SSE was buffered. `forward()` reads the whole body through res.text(), so
even turns that finished in time arrived as one blob and the CLI's live
token output never appeared. Streaming routes now use forwardStream() and
pipe the body straight through, preserving content-type and
x-accel-buffering (dropping the latter lets intermediaries re-buffer and
reintroduces the stall).
Also closes the escape that produced the sibling `500 code:23` failure: the body
read in forward() was outside the try, so when mcpd had already written SSE
headers the raw DOMException reached Fastify unhandled.
Tests: 9 new proxy tests. The two that matter — "does not abort an agent chat
that outlives the CRUD budget" and "streams SSE through instead of buffering" —
were confirmed to FAIL against the pre-fix behaviour and pass after. Three
existing mcpd-client tests asserted the old taxonomy and were updated to assert
the new one deliberately.
Local: build clean, workspace 2375 passed, lint unchanged at 869.
NOT YET LIVE: mcplocal runs from the installed RPM, so this needs a package
rebuild + `systemctl --user restart mcplocal` to take effect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
2026-08-08 11:35:16 +01:00
|
|
|
// Reporting a timeout as "cannot connect" is what sent a previous
|
|
|
|
|
// debugging session chasing a network fault that did not exist.
|
|
|
|
|
expect(err).toBeInstanceOf(UpstreamTimeoutError);
|
|
|
|
|
expect(err).not.toBeInstanceOf(ConnectionError);
|
|
|
|
|
expect((err as UpstreamTimeoutError).timeoutMs).toBe(200);
|
|
|
|
|
expect((err as Error).message).toContain('did not respond within 200ms');
|
2026-04-10 18:28:03 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('fast responses succeed within the timeout window', async () => {
|
|
|
|
|
const { server, url } = await createTestServer((_req, res) => {
|
|
|
|
|
// Respond immediately
|
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
|
|
|
res.end(JSON.stringify({ fast: true }));
|
|
|
|
|
});
|
|
|
|
|
servers.push(server);
|
|
|
|
|
|
|
|
|
|
// Short timeout, but response is immediate — should work
|
|
|
|
|
const client = new McpdClient(url, 'tok', undefined, 500);
|
|
|
|
|
const result = await client.get<{ fast: boolean }>('/test');
|
|
|
|
|
expect(result).toEqual({ fast: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('withHeaders preserves timeout', async () => {
|
|
|
|
|
const { server, url } = await createTestServer((_req, _res) => {
|
|
|
|
|
// Never respond
|
|
|
|
|
});
|
|
|
|
|
servers.push(server);
|
|
|
|
|
|
|
|
|
|
const client = new McpdClient(url, 'tok', undefined, 300);
|
|
|
|
|
const derived = client.withHeaders({ 'X-Custom': 'val' });
|
|
|
|
|
|
|
|
|
|
const start = Date.now();
|
fix(mcplocal): stop the 30s proxy timeout killing agent turns
`mcpctl chat <agent>` failed with
HTTP 503 {"error":"service_unavailable","message":"Cannot reach mcpd daemon. Is it running?"}
while mcpd was answering /healthz in 32ms. The message was wrong in a way that
cost real debugging time: mcplocal was reaching mcpd fine and giving up after
30s. journalctl shows the signature plainly — statusCode 503 with
responseTime 30003.87 on POST /api/v1/agents/reviewer/chat.
This blocks the agentic-teams epic outright. An agent turn is a multi-turn
tool-use loop that runs for minutes by design, so a 30s ceiling on the chat path
is not a safety net, it is a guaranteed failure for every non-trivial turn.
Three defects, all in the same path:
1. One blanket budget for every forwarded route. DEFAULT_TIMEOUT_MS = 30_000 is
right for CRUD and wrong for chat. Chat, project chat, llm infer and
inference-task streams now get LONG_RUNNING_TIMEOUT_MS (600_000, override
with MCPLOCAL_LONG_TIMEOUT_MS) — matching STREAM_TIMEOUT_MS, which the CLI
already allowed. mcplocal in the middle was the binding constraint.
2. Timeouts were reported as connection failures. Split UpstreamTimeoutError
out of ConnectionError and map it to 504 with an accurate message that says
the daemon IS reachable. ConnectionError still means unreachable and still
returns 503. Verified nothing else branches on ConnectionError.
3. SSE was buffered. `forward()` reads the whole body through res.text(), so
even turns that finished in time arrived as one blob and the CLI's live
token output never appeared. Streaming routes now use forwardStream() and
pipe the body straight through, preserving content-type and
x-accel-buffering (dropping the latter lets intermediaries re-buffer and
reintroduces the stall).
Also closes the escape that produced the sibling `500 code:23` failure: the body
read in forward() was outside the try, so when mcpd had already written SSE
headers the raw DOMException reached Fastify unhandled.
Tests: 9 new proxy tests. The two that matter — "does not abort an agent chat
that outlives the CRUD budget" and "streams SSE through instead of buffering" —
were confirmed to FAIL against the pre-fix behaviour and pass after. Three
existing mcpd-client tests asserted the old taxonomy and were updated to assert
the new one deliberately.
Local: build clean, workspace 2375 passed, lint unchanged at 869.
NOT YET LIVE: mcplocal runs from the installed RPM, so this needs a package
rebuild + `systemctl --user restart mcplocal` to take effect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N4wNHWf7xSwnZCWpJcyv9p
2026-08-08 11:35:16 +01:00
|
|
|
await expect(derived.get('/test')).rejects.toThrow(/did not respond within/);
|
2026-04-10 18:28:03 +01:00
|
|
|
const elapsed = Date.now() - start;
|
|
|
|
|
expect(elapsed).toBeLessThan(2000);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('default timeout is 30 seconds', async () => {
|
|
|
|
|
// We can't wait 30s in a test, but we can verify the error message format
|
|
|
|
|
// when a custom timeout is not set. Use a fast-failing server instead.
|
|
|
|
|
const { server, url } = await createTestServer((_req, res) => {
|
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
|
|
|
res.end(JSON.stringify({ ok: true }));
|
|
|
|
|
});
|
|
|
|
|
servers.push(server);
|
|
|
|
|
|
|
|
|
|
// Default constructor — should work for fast responses
|
|
|
|
|
const client = new McpdClient(url, 'tok');
|
|
|
|
|
const result = await client.get<{ ok: boolean }>('/test');
|
|
|
|
|
expect(result).toEqual({ ok: true });
|
|
|
|
|
});
|
|
|
|
|
});
|