import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
/**
* Tests for ToolRegistry system
*/
import { ToolRegistry } from '../../tools/registry.js';
import { GetStampTool } from '../../tools/stamps.js';
import { GetCollectionTool } from '../../tools/collections.js';
import { createMockToolContext } from '../utils/test-helpers.js';
describe('ToolRegistry', () => {
let registry: ToolRegistry;
let mockTool1: GetStampTool;
let mockTool2: GetCollectionTool;
beforeEach(() => {
registry = new ToolRegistry({
validateOnRegister: true,
allowDuplicateNames: false,
maxTools: 100,
});
mockTool1 = new GetStampTool();
mockTool2 = new GetCollectionTool();
});
describe('constructor', () => {
it('should create registry with default options', () => {
const defaultRegistry = new ToolRegistry();
expect(defaultRegistry).toBeInstanceOf(ToolRegistry);
});
it('should create registry with custom options', () => {
const customRegistry = new ToolRegistry({
validateOnRegister: false,
allowDuplicateNames: true,
maxTools: 50,
});
expect(customRegistry).toBeInstanceOf(ToolRegistry);
});
});
describe('register', () => {
it('should register a tool successfully', () => {
registry.register(mockTool1, {
category: 'stamps',
version: '1.0.0',
});
expect(registry.has('get_stamp')).toBe(true);
expect(registry.get('get_stamp')).toBe(mockTool1);
});
it('should register tool with minimal metadata', () => {
registry.register(mockTool1);
expect(registry.has('get_stamp')).toBe(true);
expect(registry.get('get_stamp')).toBe(mockTool1);
});
it('should reject duplicate tool names when not allowed', () => {
registry.register(mockTool1);
expect(() => {
registry.register(mockTool1, { category: 'duplicate' });
}).toThrow("Tool with name 'get_stamp' already registered");
});
it('should allow duplicate tool names when configured', () => {
const duplicateRegistry = new ToolRegistry({
allowDuplicateNames: true,
});
duplicateRegistry.register(mockTool1);
expect(() => {
duplicateRegistry.register(mockTool1, { category: 'duplicate' });
}).not.toThrow();
});
it('should enforce maximum tool limit', () => {
const limitedRegistry = new ToolRegistry({
maxTools: 1,
});
limitedRegistry.register(mockTool1);
expect(() => {
limitedRegistry.register(mockTool2);
}).toThrow('Maximum number of tools (1) reached');
});
it('should validate tool when enabled', () => {
// Mock a tool with invalid structure
const invalidTool = {} as any;
expect(() => {
registry.register(invalidTool);
}).toThrow();
});
it('should skip validation when disabled', () => {
const nonValidatingRegistry = new ToolRegistry({
validateOnRegister: false,
});
const invalidTool = { name: 'test', description: 'test' } as any;
expect(() => {
nonValidatingRegistry.register(invalidTool);
}).not.toThrow();
});
});
describe('get', () => {
beforeEach(() => {
registry.register(mockTool1, { category: 'stamps' });
registry.register(mockTool2, { category: 'collections' });
});
it('should retrieve registered tool', () => {
const tool = registry.get('get_stamp');
expect(tool).toBe(mockTool1);
});
it('should throw error for non-existent tool', () => {
expect(() => {
registry.get('non_existent_tool');
}).toThrow('Tool not found: non_existent_tool');
});
});
describe('has', () => {
beforeEach(() => {
registry.register(mockTool1);
});
it('should return true for registered tool', () => {
expect(registry.has('get_stamp')).toBe(true);
});
it('should return false for non-existent tool', () => {
expect(registry.has('non_existent_tool')).toBe(false);
});
});
describe('list', () => {
beforeEach(() => {
registry.register(mockTool1, { category: 'stamps' });
registry.register(mockTool2, { category: 'collections' });
});
it('should list all registered tool names', () => {
const tools = registry.list();
const toolNames = tools.map((tool) => tool.name);
expect(toolNames).toContain('get_stamp');
expect(toolNames).toContain('get_collection');
expect(tools).toHaveLength(2);
});
});
describe('remove', () => {
beforeEach(() => {
registry.register(mockTool1);
registry.register(mockTool2);
});
it('should remove registered tool', () => {
expect(registry.has('get_stamp')).toBe(true);
registry.remove('get_stamp');
expect(registry.has('get_stamp')).toBe(false);
});
it('should return false for non-existent tool', () => {
const result = registry.remove('non_existent_tool');
expect(result).toBe(false);
});
});
describe('clear', () => {
beforeEach(() => {
registry.register(mockTool1);
registry.register(mockTool2);
});
it('should remove all tools', () => {
expect(registry.list()).toHaveLength(2);
registry.clear();
expect(registry.list()).toHaveLength(0);
});
});
describe('getByCategory', () => {
beforeEach(() => {
registry.register(mockTool1, { category: 'stamps' });
registry.register(mockTool2, { category: 'collections' });
});
it('should return tools in specified category', () => {
const stampsTools = registry.getByCategory('stamps');
expect(stampsTools).toContain('get_stamp');
expect(stampsTools).toHaveLength(1);
});
it('should return empty array for non-existent category', () => {
const nonExistentTools = registry.getByCategory('non_existent');
expect(nonExistentTools).toEqual([]);
});
});
describe('getCategories', () => {
beforeEach(() => {
registry.register(mockTool1, { category: 'stamps' });
registry.register(mockTool2, { category: 'collections' });
});
it('should return all categories', () => {
const categories = registry.getCategories();
expect(categories).toContain('stamps');
expect(categories).toContain('collections');
expect(categories).toHaveLength(2);
});
});
describe('enable/disable', () => {
beforeEach(() => {
registry.register(mockTool1);
});
it('should disable tool', () => {
expect(registry.isEnabled('get_stamp')).toBe(true);
registry.disable('get_stamp');
expect(registry.isEnabled('get_stamp')).toBe(false);
});
it('should enable tool', () => {
registry.disable('get_stamp');
expect(registry.isEnabled('get_stamp')).toBe(false);
registry.enable('get_stamp');
expect(registry.isEnabled('get_stamp')).toBe(true);
});
it('should handle non-existent tools gracefully', () => {
expect(() => {
registry.disable('non_existent_tool');
}).not.toThrow();
expect(() => {
registry.enable('non_existent_tool');
}).not.toThrow();
});
});
describe('getStats', () => {
beforeEach(() => {
registry.register(mockTool1, { category: 'stamps' });
registry.register(mockTool2, { category: 'collections' });
registry.disable('get_stamp');
});
it('should return accurate statistics', () => {
const stats = registry.getStats();
expect(stats.totalTools).toBe(2);
expect(stats.enabledTools).toBe(1);
expect(stats.disabledTools).toBe(1);
expect(stats.categories).toBe(2);
});
});
describe('getMCPTools', () => {
beforeEach(() => {
registry.register(mockTool1, { category: 'stamps' });
registry.register(mockTool2, { category: 'collections' });
registry.disable('get_stamp');
});
it('should return only enabled tools in MCP format', () => {
const mcpTools = registry.getMCPTools();
expect(mcpTools).toHaveLength(1);
expect(mcpTools[0].name).toBe('get_collection');
expect(mcpTools[0].description).toBeDefined();
expect(mcpTools[0].inputSchema).toBeDefined();
});
it('should return all tools when includeDisabled is true', () => {
const mcpTools = registry.getMCPTools(true);
expect(mcpTools).toHaveLength(2);
expect(mcpTools.map((t) => t.name)).toContain('get_stamp');
expect(mcpTools.map((t) => t.name)).toContain('get_collection');
});
});
describe('export/import', () => {
beforeEach(() => {
registry.register(mockTool1, {
category: 'stamps',
version: '1.0.0',
description: 'Custom description',
});
registry.register(mockTool2, { category: 'collections' });
});
it('should export registry configuration', () => {
const exported = registry.export();
expect(exported.tools).toHaveLength(2);
expect(exported.tools[0].name).toBe('get_stamp');
expect(exported.tools[0].category).toBe('stamps');
expect(exported.options).toBeDefined();
});
it('should import registry configuration', () => {
const exported = registry.export();
const newRegistry = new ToolRegistry();
// Note: Import would need to be implemented to recreate tool instances
// This test validates the export structure
expect(exported.tools).toBeDefined();
expect(exported.options).toBeDefined();
});
});
describe('error handling', () => {
it('should handle tool execution errors gracefully', async () => {
const context = createMockToolContext();
registry.register(mockTool1);
// Mock tool execution to throw error
const originalExecute = mockTool1.execute;
mockTool1.execute = vi.fn().mockRejectedValueOnce(new Error('Tool execution failed'));
const tool = registry.get('get_stamp');
await expect(tool.execute({}, context)).rejects.toThrow('Tool execution failed');
// Restore original method
mockTool1.execute = originalExecute;
});
});
});