Skip to main content
Glama
BaseAdapter.test.ts7.83 kB
/** * Tests for BaseAdapter */ import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; import { BaseAdapter } from '../../src/adapters/BaseAdapter'; import type { HookContext, HookResponse } from '../../src/types'; import { Readable } from 'node:stream'; /** * Mock adapter implementation for testing */ class MockAdapter extends BaseAdapter { parseInput(stdin: string): HookContext { const data = JSON.parse(stdin); return { toolName: data.tool_name, toolInput: data.tool_input, filePath: data.file_path, operation: data.operation, cwd: data.cwd, sessionId: data.session_id, llmTool: data.llm_tool, }; } formatOutput(response: HookResponse): string { return JSON.stringify({ decision: response.decision, message: response.message, userMessage: response.userMessage, updatedInput: response.updatedInput, }); } } describe('BaseAdapter', () => { let adapter: MockAdapter; let originalExit: typeof process.exit; let exitCode: number | undefined; beforeEach(() => { adapter = new MockAdapter(); originalExit = process.exit; // Mock process.exit to capture exit code exitCode = undefined; process.exit = vi.fn((code?: number) => { exitCode = code ?? 0; throw new Error('EXIT'); }) as any; }); afterEach(() => { process.exit = originalExit; vi.restoreAllMocks(); }); /** * Helper to mock stdin with data */ function mockStdin(data: string): Readable { const readable = new Readable(); readable.push(data); readable.push(null); // Mock process.stdin methods vi.spyOn(process.stdin, 'on').mockImplementation((event: string, handler: any) => { if (event === 'data') { setImmediate(() => handler(Buffer.from(data))); } else if (event === 'end') { setImmediate(() => handler()); } return process.stdin; }); return readable; } describe('execute', () => { test('reads stdin, executes callback, and writes output', async () => { const input = { tool_name: 'Read', tool_input: { file_path: '/test/file.ts' }, file_path: '/test/file.ts', operation: 'read', cwd: '/test', session_id: 'session-123', llm_tool: 'claude-code', }; mockStdin(JSON.stringify(input)); const callback = vi.fn(async (context: HookContext) => ({ decision: 'allow' as const, message: 'Test message', })); const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); try { await adapter.execute(callback); } catch (error: any) { if (error.message !== 'EXIT') throw error; } expect(callback).toHaveBeenCalledWith({ toolName: 'Read', toolInput: { file_path: '/test/file.ts' }, filePath: '/test/file.ts', operation: 'read', cwd: '/test', sessionId: 'session-123', llmTool: 'claude-code', }); expect(consoleLogSpy).toHaveBeenCalledWith( JSON.stringify({ decision: 'allow', message: 'Test message', userMessage: undefined, updatedInput: undefined, }), ); expect(exitCode).toBe(0); }); test('handles skip decision without outputting anything', async () => { const input = { tool_name: 'Read', tool_input: { file_path: '/test/file.ts' }, file_path: '/test/file.ts', operation: 'read', cwd: '/test', session_id: 'session-123', }; mockStdin(JSON.stringify(input)); const callback = vi.fn(async () => ({ decision: 'skip' as const, message: '', })); const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); // For skip decision, process.exit should be called without logging // Re-mock process.exit to not throw so we can verify no logging happens let skipExitCalled = false; process.exit = vi.fn((code?: number) => { skipExitCalled = true; exitCode = code ?? 0; // Don't throw for this test - we want to verify no console.log return undefined as never; }) as any; await adapter.execute(callback); expect(callback).toHaveBeenCalled(); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(skipExitCalled).toBe(true); expect(exitCode).toBe(0); }); test('handles callback with updatedInput', async () => { const input = { tool_name: 'Write', tool_input: { file_path: '/test/file.ts', content: 'test' }, file_path: '/test/file.ts', operation: 'write', cwd: '/test', session_id: 'session-123', }; mockStdin(JSON.stringify(input)); const updatedInput = { file_path: '/test/file.ts', content: 'updated test' }; const callback = vi.fn(async () => ({ decision: 'allow' as const, message: 'Updated content', updatedInput, })); const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); try { await adapter.execute(callback); } catch (error: any) { if (error.message !== 'EXIT') throw error; } expect(consoleLogSpy).toHaveBeenCalledWith( JSON.stringify({ decision: 'allow', message: 'Updated content', userMessage: undefined, updatedInput, }), ); }); test('handles errors with fail-open behavior', async () => { const input = { tool_name: 'Read', tool_input: {}, cwd: '/test', session_id: 'session-123', }; mockStdin(JSON.stringify(input)); const callback = vi.fn(async () => { throw new Error('Callback error'); }); const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); try { await adapter.execute(callback); } catch (error: any) { if (error.message !== 'EXIT') throw error; } expect(consoleLogSpy).toHaveBeenCalledWith( JSON.stringify({ decision: 'allow', message: '⚠️ Hook error: Callback error', userMessage: undefined, updatedInput: undefined, }), ); expect(exitCode).toBe(0); }); test('handles JSON parse errors with fail-open behavior', async () => { mockStdin('invalid json'); const callback = vi.fn(); const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); try { await adapter.execute(callback); } catch (error: any) { if (error.message !== 'EXIT') throw error; } expect(callback).not.toHaveBeenCalled(); expect(consoleLogSpy).toHaveBeenCalled(); const output = consoleLogSpy.mock.calls[0][0]; expect(output).toContain('⚠️ Hook error:'); expect(exitCode).toBe(0); }); test('handles non-Error exceptions', async () => { const input = { tool_name: 'Read', tool_input: {}, cwd: '/test', session_id: 'session-123', }; mockStdin(JSON.stringify(input)); const callback = vi.fn(async () => { throw 'String error'; }); const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); try { await adapter.execute(callback); } catch (error: any) { if (error.message !== 'EXIT') throw error; } expect(consoleLogSpy).toHaveBeenCalledWith( JSON.stringify({ decision: 'allow', message: '⚠️ Hook error: String error', userMessage: undefined, updatedInput: undefined, }), ); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AgiFlow/aicode-toolkit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server