/**
* Extended Tool Registry Tests
*
* Comprehensive tests for tool registration, lookup, and execution
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { ToolRegistry } from '../../src/application/tools/tool-registry.js';
import type { ToolDefinition, ToolResult } from '../../src/domain/types.js';
import { ConsoleLogger } from '../../src/infrastructure/logging/logger.js';
describe('Tool Registry - Extended Coverage', () => {
let registry: ToolRegistry;
let logger: ConsoleLogger;
const mockTool: ToolDefinition = {
name: 'test-tool',
description: 'A test tool',
inputSchema: {
type: 'object',
properties: {
input: {
type: 'string'
}
},
required: ['input']
}
};
const mockHandler = vi.fn(async (args: unknown): Promise<ToolResult> => ({
content: [{ type: 'text', text: 'success' }],
isError: false
}));
beforeEach(() => {
logger = new ConsoleLogger('test');
registry = new ToolRegistry(logger);
vi.clearAllMocks();
});
describe('register', () => {
it('should register a new tool', () => {
registry.register(mockTool, mockHandler);
const tools = registry.getToolDefinitions();
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe('test-tool');
});
it('should register multiple tools', () => {
const tool1 = { ...mockTool, name: 'tool-1' };
const tool2 = { ...mockTool, name: 'tool-2' };
const tool3 = { ...mockTool, name: 'tool-3' };
registry.register(tool1, mockHandler);
registry.register(tool2, mockHandler);
registry.register(tool3, mockHandler);
const tools = registry.getToolDefinitions();
expect(tools).toHaveLength(3);
});
it('should overwrite existing tool with same name', () => {
const handler1 = vi.fn(async () => ({ content: [], isError: false }));
const handler2 = vi.fn(async () => ({ content: [], isError: false }));
registry.register(mockTool, handler1);
registry.register(mockTool, handler2);
const tools = registry.getToolDefinitions();
expect(tools).toHaveLength(1);
});
it('should preserve tool definition when registering', () => {
registry.register(mockTool, mockHandler);
const tools = registry.getToolDefinitions();
expect(tools[0]).toEqual(mockTool);
});
it('should register tool with complex schema', () => {
const complexTool: ToolDefinition = {
name: 'complex-tool',
description: 'Complex tool',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
active: { type: 'boolean' },
tags: {
type: 'array',
items: { type: 'string' }
}
},
required: ['name', 'age']
}
};
registry.register(complexTool, mockHandler);
const tools = registry.getToolDefinitions();
expect(tools[0].inputSchema.properties).toHaveProperty('tags');
});
});
describe('unregister', () => {
beforeEach(() => {
registry.register(mockTool, mockHandler);
});
it('should unregister existing tool', () => {
const removed = registry.unregister('test-tool');
expect(removed).toBe(true);
expect(registry.getToolDefinitions()).toHaveLength(0);
});
it('should return false for non-existent tool', () => {
const removed = registry.unregister('non-existent');
expect(removed).toBe(false);
});
it('should not affect other tools when unregistering', () => {
const tool2 = { ...mockTool, name: 'tool-2' };
registry.register(tool2, mockHandler);
registry.unregister('test-tool');
const tools = registry.getToolDefinitions();
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe('tool-2');
});
it('should allow re-registering after unregister', () => {
registry.unregister('test-tool');
registry.register(mockTool, mockHandler);
const tools = registry.getToolDefinitions();
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe('test-tool');
});
});
describe('listTools', () => {
it('should return empty array when no tools registered', () => {
const tools = registry.getToolDefinitions();
expect(tools).toEqual([]);
});
it('should return all registered tools', () => {
const tool1 = { ...mockTool, name: 'tool-1' };
const tool2 = { ...mockTool, name: 'tool-2' };
registry.register(tool1, mockHandler);
registry.register(tool2, mockHandler);
const tools = registry.getToolDefinitions();
expect(tools).toHaveLength(2);
expect(tools.map(t => t.name)).toContain('tool-1');
expect(tools.map(t => t.name)).toContain('tool-2');
});
it('should return readonly array', () => {
registry.register(mockTool, mockHandler);
const tools = registry.getToolDefinitions();
expect(Array.isArray(tools)).toBe(true);
});
});
describe('has', () => {
beforeEach(() => {
registry.register(mockTool, mockHandler);
});
it('should return true for existing tool', () => {
const exists = registry.has('test-tool');
expect(exists).toBe(true);
});
it('should return false for non-existent tool', () => {
const exists = registry.has('non-existent');
expect(exists).toBe(false);
});
it('should return false after unregistering', () => {
registry.unregister('test-tool');
const exists = registry.has('test-tool');
expect(exists).toBe(false);
});
});
describe('getHandler', () => {
beforeEach(() => {
registry.register(mockTool, mockHandler);
});
it('should get handler for existing tool', () => {
const handler = registry.getHandler('test-tool');
expect(handler).toBeDefined();
expect(typeof handler).toBe('function');
});
it('should return undefined for non-existent tool', () => {
const handler = registry.getHandler('non-existent');
expect(handler).toBeUndefined();
});
});
describe('execute', () => {
beforeEach(() => {
registry.register(mockTool, mockHandler);
});
it('should execute registered tool', async () => {
const args = { input: 'test' };
const result = await registry.execute('test-tool', args);
expect(result.content[0].text).toBe('success');
expect(result.isError).toBe(false);
expect(mockHandler).toHaveBeenCalledWith(args);
});
it('should throw error for non-existent tool', async () => {
await expect(registry.execute('non-existent', {})).rejects.toThrow('Tool not found');
});
it('should pass arguments to handler', async () => {
const args = { input: 'test-value', extra: 123 };
await registry.execute('test-tool', args);
expect(mockHandler).toHaveBeenCalledWith(args);
});
it('should throw handler errors', async () => {
const errorHandler = vi.fn(async () => {
throw new Error('Handler failed');
});
registry.register({ ...mockTool, name: 'error-tool' }, errorHandler);
await expect(registry.execute('error-tool', {})).rejects.toThrow('Handler failed');
});
it('should handle handler that returns error', async () => {
const errorHandler = vi.fn(async (): Promise<ToolResult> => ({
content: [{ type: 'text', text: 'Custom error' }],
isError: true
}));
registry.register({ ...mockTool, name: 'error-tool' }, errorHandler);
const result = await registry.execute('error-tool', {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('Custom error');
});
it('should execute multiple different tools', async () => {
const handler1 = vi.fn(async () => ({
content: [{ type: 'text', text: 'result1' }],
isError: false
}));
const handler2 = vi.fn(async () => ({
content: [{ type: 'text', text: 'result2' }],
isError: false
}));
registry.register({ ...mockTool, name: 'tool-1' }, handler1);
registry.register({ ...mockTool, name: 'tool-2' }, handler2);
const result1 = await registry.execute('tool-1', {});
const result2 = await registry.execute('tool-2', {});
expect(result1.content[0].text).toBe('result1');
expect(result2.content[0].text).toBe('result2');
expect(handler1).toHaveBeenCalledTimes(1);
expect(handler2).toHaveBeenCalledTimes(1);
});
it('should execute same tool multiple times', async () => {
await registry.execute('test-tool', { input: 'first' });
await registry.execute('test-tool', { input: 'second' });
expect(mockHandler).toHaveBeenCalledTimes(2);
});
});
describe('count', () => {
it('should return 0 for empty registry', () => {
expect(registry.count()).toBe(0);
});
it('should return correct count after registrations', () => {
registry.register({ ...mockTool, name: 'tool-1' }, mockHandler);
registry.register({ ...mockTool, name: 'tool-2' }, mockHandler);
registry.register({ ...mockTool, name: 'tool-3' }, mockHandler);
expect(registry.count()).toBe(3);
});
it('should update count after unregister', () => {
registry.register({ ...mockTool, name: 'tool-1' }, mockHandler);
registry.register({ ...mockTool, name: 'tool-2' }, mockHandler);
registry.unregister('tool-1');
expect(registry.count()).toBe(1);
});
it('should not change count when overwriting tool', () => {
registry.register(mockTool, mockHandler);
registry.register(mockTool, mockHandler);
expect(registry.count()).toBe(1);
});
});
});