/** * Integration tests for ToolSanitizationProxy * * Tests the full proxy flow: request sanitization → upstream forwarding → response restoration */ import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'bun:test'; import * as http from 'http'; import { ToolSanitizationProxy } from '../../../src/cliproxy/tool-sanitization-proxy'; // Mock upstream server that echoes requests let mockUpstream: http.Server; let mockUpstreamPort: number; let lastRequest: { body: unknown; headers: http.IncomingHttpHeaders } | null = null; // Track response to send back let mockResponse: { status: number; body: unknown; stream?: boolean } = { status: 200, body: { content: [] }, }; beforeAll(async () => { await new Promise((resolve) => { mockUpstream = http.createServer((req, res) => { const chunks: Buffer[] = []; req.on('data', (chunk) => chunks.push(chunk)); req.on('end', () => { const body = Buffer.concat(chunks).toString('utf8'); lastRequest = { body: body ? JSON.parse(body) : null, headers: req.headers, }; if (mockResponse.stream) { // SSE streaming response res.writeHead(mockResponse.status, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }); const events = mockResponse.body as string[]; for (const event of events) { res.write(event); } res.end(); } else { // Buffered JSON response res.writeHead(mockResponse.status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(mockResponse.body)); } }); }); mockUpstream.listen(0, '127.0.0.1', () => { const addr = mockUpstream.address(); mockUpstreamPort = typeof addr === 'object' && addr ? addr.port : 0; resolve(); }); }); }); afterAll(() => { mockUpstream?.close(); }); beforeEach(() => { lastRequest = null; mockResponse = { status: 200, body: { content: [] } }; }); describe('ToolSanitizationProxy Integration', () => { describe('Proxy Lifecycle', () => { it('starts on ephemeral port and stops cleanly', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); const port = await proxy.start(); expect(port).toBeGreaterThan(0); expect(proxy.getPort()).toBe(port); proxy.stop(); expect(proxy.getPort()).toBeNull(); }); it('returns same port on multiple start calls', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); const port1 = await proxy.start(); const port2 = await proxy.start(); expect(port1).toBe(port2); proxy.stop(); }); }); describe('Request Sanitization', () => { it('sanitizes duplicate segment tool names in request', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); const port = await proxy.start(); try { const response = await fetch(`http://127.0.0.1:${port}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'test-model', tools: [ { name: 'gitmcp__plus-pro-components__plus-pro-components', description: 'Test tool' }, { name: 'valid_tool', description: 'Valid tool' }, ], messages: [{ role: 'user', content: 'test' }], }), }); expect(response.ok).toBe(true); expect(lastRequest).not.toBeNull(); const sentTools = (lastRequest!.body as Record).tools as Array<{ name: string; }>; expect(sentTools[0].name).toBe('gitmcp__plus-pro-components'); expect(sentTools[1].name).toBe('valid_tool'); } finally { proxy.stop(); } }); it('truncates tool names exceeding 64 characters', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); const port = await proxy.start(); try { const longName = 'a'.repeat(100); await fetch(`http://127.0.0.1:${port}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tools: [{ name: longName }], }), }); const sentTools = (lastRequest!.body as Record).tools as Array<{ name: string; }>; expect(sentTools[0].name.length).toBe(64); expect(sentTools[0].name).toContain('_'); // Has hash separator } finally { proxy.stop(); } }); it('passes through requests without tools unchanged', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); const port = await proxy.start(); try { const originalBody = { model: 'test-model', messages: [{ role: 'user', content: 'Hello' }], }; await fetch(`http://127.0.0.1:${port}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(originalBody), }); expect(lastRequest!.body).toEqual(originalBody); } finally { proxy.stop(); } }); it('preserves other tool properties during sanitization', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); const port = await proxy.start(); try { await fetch(`http://127.0.0.1:${port}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tools: [ { name: 'foo__bar__bar', description: 'Test description', input_schema: { type: 'object', properties: { x: { type: 'string' } } }, }, ], }), }); const sentTools = (lastRequest!.body as Record).tools as Array<{ name: string; description: string; input_schema: object; }>; expect(sentTools[0].name).toBe('foo__bar'); expect(sentTools[0].description).toBe('Test description'); expect(sentTools[0].input_schema).toEqual({ type: 'object', properties: { x: { type: 'string' } }, }); } finally { proxy.stop(); } }); }); describe('Response Restoration (Buffered)', () => { it('restores original tool names in tool_use response blocks', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); const port = await proxy.start(); // Mock upstream returns sanitized name in tool_use mockResponse = { status: 200, body: { content: [ { type: 'text', text: 'Using tool...' }, { type: 'tool_use', id: 'call_123', name: 'foo__bar', input: { query: 'test' } }, ], }, }; try { const response = await fetch(`http://127.0.0.1:${port}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tools: [{ name: 'foo__bar__bar' }], }), }); const data = (await response.json()) as { content: Array<{ type: string; name?: string }> }; expect(data.content[0].type).toBe('text'); expect(data.content[1].type).toBe('tool_use'); expect(data.content[1].name).toBe('foo__bar__bar'); // Restored! } finally { proxy.stop(); } }); it('leaves unknown tool names unchanged in response', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); const port = await proxy.start(); mockResponse = { status: 200, body: { content: [{ type: 'tool_use', id: 'call_456', name: 'unknown_tool', input: {} }], }, }; try { const response = await fetch(`http://127.0.0.1:${port}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tools: [{ name: 'foo__bar__bar' }], }), }); const data = (await response.json()) as { content: Array<{ name: string }> }; expect(data.content[0].name).toBe('unknown_tool'); } finally { proxy.stop(); } }); }); describe('Response Restoration (Streaming)', () => { it('restores tool names in SSE streaming responses', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); const port = await proxy.start(); // Mock SSE events with sanitized tool name mockResponse = { status: 200, stream: true, body: [ 'event: content_block_start\n', `data: ${JSON.stringify({ type: 'content_block_start', content_block: { type: 'tool_use', id: 'call_789', name: 'foo__bar' } })}\n\n`, 'event: content_block_stop\n', 'data: {"type":"content_block_stop"}\n\n', ], }; try { const response = await fetch(`http://127.0.0.1:${port}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stream: true, tools: [{ name: 'foo__bar__bar' }], }), }); const text = await response.text(); expect(text).toContain('foo__bar__bar'); // Restored in SSE expect(text).not.toContain('"name":"foo__bar"'); // Original sanitized name replaced } finally { proxy.stop(); } }); it('passes through SSE events when no sanitization occurred', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); const port = await proxy.start(); mockResponse = { status: 200, stream: true, body: [ 'event: message_start\n', 'data: {"type":"message_start"}\n\n', 'event: content_block_delta\n', 'data: {"type":"content_block_delta","delta":{"text":"Hello"}}\n\n', ], }; try { const response = await fetch(`http://127.0.0.1:${port}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stream: true, tools: [{ name: 'valid_tool' }], // No sanitization needed }), }); const text = await response.text(); expect(text).toContain('message_start'); expect(text).toContain('content_block_delta'); } finally { proxy.stop(); } }); }); describe('Error Handling', () => { it('returns 400 for invalid JSON request body', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); const port = await proxy.start(); try { const response = await fetch(`http://127.0.0.1:${port}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: 'not valid json{', }); expect(response.status).toBe(400); const data = (await response.json()) as { error: string }; expect(data.error).toContain('Invalid JSON'); } finally { proxy.stop(); } }); it('forwards upstream errors correctly', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); const port = await proxy.start(); mockResponse = { status: 429, body: { error: { type: 'rate_limit_error', message: 'Too many requests' } }, }; try { const response = await fetch(`http://127.0.0.1:${port}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tools: [] }), }); expect(response.status).toBe(429); } finally { proxy.stop(); } }); it('handles upstream connection errors gracefully', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: 'http://127.0.0.1:1', // Invalid port timeoutMs: 1000, }); const port = await proxy.start(); try { const response = await fetch(`http://127.0.0.1:${port}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tools: [] }), }); expect(response.status).toBe(502); } finally { proxy.stop(); } }); }); describe('Non-JSON Passthrough', () => { it('forwards GET requests without modification', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); const port = await proxy.start(); mockResponse = { status: 200, body: { status: 'ok' } }; try { const response = await fetch(`http://127.0.0.1:${port}/health`); expect(response.ok).toBe(true); } finally { proxy.stop(); } }); }); describe('Multiple Tools Sanitization', () => { it('sanitizes multiple tools and tracks all mappings', async () => { const proxy = new ToolSanitizationProxy({ upstreamBaseUrl: `http://127.0.0.1:${mockUpstreamPort}`, }); const port = await proxy.start(); mockResponse = { status: 200, body: { content: [ { type: 'tool_use', id: '1', name: 'tool_a__x', input: {} }, { type: 'tool_use', id: '2', name: 'tool_b__y', input: {} }, ], }, }; try { const response = await fetch(`http://127.0.0.1:${port}/v1/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tools: [ { name: 'tool_a__x__x' }, { name: 'tool_b__y__y' }, { name: 'tool_c_valid' }, ], }), }); // Verify request was sanitized const sentTools = (lastRequest!.body as Record).tools as Array<{ name: string; }>; expect(sentTools[0].name).toBe('tool_a__x'); expect(sentTools[1].name).toBe('tool_b__y'); expect(sentTools[2].name).toBe('tool_c_valid'); // Verify response was restored const data = (await response.json()) as { content: Array<{ name: string }> }; expect(data.content[0].name).toBe('tool_a__x__x'); expect(data.content[1].name).toBe('tool_b__y__y'); } finally { proxy.stop(); } }); }); });