import { describe, it, expect, vi } from 'vitest'; import { ToolRegistry } from './registry.js'; import type { Tool } from './types.js'; const echoTool: Tool = { name: 'test.echo', description: 'Echoes input back', inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'Text to echo' } }, required: ['text'], }, execute: async (args) => ({ success: true, output: String((args as { text: string }).text) }), }; const greetTool: Tool = { name: 'test.greet', description: 'Greets someone', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'], }, execute: async (args) => ({ success: true, output: `Hello ${(args as { name: string }).name}` }), }; describe('ToolRegistry', () => { it('registers and retrieves tools by name', () => { const registry = new ToolRegistry(); registry.register(echoTool); expect(registry.get('test.echo')).toBe(echoTool); expect(registry.get('nonexistent')).toBeUndefined(); }); it('lists all registered tools', () => { const registry = new ToolRegistry(); registry.register(echoTool); registry.register(greetTool); const tools = registry.list(); expect(tools).toHaveLength(2); expect(tools.map(t => t.name)).toContain('test.echo'); expect(tools.map(t => t.name)).toContain('test.greet'); }); it('throws on duplicate registration', () => { const registry = new ToolRegistry(); registry.register(echoTool); expect(() => registry.register(echoTool)).toThrow('already registered'); }); it('serializes to Anthropic format', () => { const registry = new ToolRegistry(); registry.register(echoTool); const anthropicTools = registry.toAnthropicFormat(); expect(anthropicTools).toEqual([{ name: 'test_echo', description: 'Echoes input back', input_schema: echoTool.inputSchema, }]); }); it('unregisters a tool by name', () => { const registry = new ToolRegistry(); registry.register(echoTool); registry.register(greetTool); expect(registry.unregister('test.echo')).toBe(true); expect(registry.get('test.echo')).toBeUndefined(); expect(registry.list()).toHaveLength(1); expect(registry.list()[0].name).toBe('test.greet'); }); it('returns false when unregistering a nonexistent tool', () => { const registry = new ToolRegistry(); expect(registry.unregister('nonexistent')).toBe(false); }); it('serializes to OpenAI format', () => { const registry = new ToolRegistry(); registry.register(echoTool); const openaiTools = registry.toOpenAIFormat(); expect(openaiTools).toEqual([{ type: 'function', function: { name: 'test_echo', description: 'Echoes input back', parameters: echoTool.inputSchema, }, }]); }); describe('ToolRegistry — clone()', () => { function makeTool(name: string): Tool { return { name, description: `Mock ${name}`, inputSchema: { type: 'object', properties: {} }, execute: async () => ({ success: true, output: '' }), }; } it('creates a copy with all tools', () => { const reg = new ToolRegistry(); reg.register(makeTool('tool.a')); reg.register(makeTool('tool.b')); const cloned = reg.clone(); expect(cloned.list().map(t => t.name).sort()).toEqual(['tool.a', 'tool.b']); }); it('inherits the policy from original', () => { const reg = new ToolRegistry(); const mockPolicy = { filterTools: vi.fn(), isAllowed: vi.fn(), resolveAllowedNames: vi.fn(), getEffectiveProfile: vi.fn() }; reg.setPolicy(mockPolicy as any); const cloned = reg.clone(); expect(cloned.getPolicy()).toBe(mockPolicy); }); it('allows replacing tools in clone without affecting original', () => { const reg = new ToolRegistry(); const originalTool = makeTool('shell.exec'); reg.register(originalTool); const cloned = reg.clone(); const replacementTool = makeTool('shell.exec'); replacementTool.description = 'Sandboxed version'; cloned.replace(replacementTool); expect(cloned.get('shell.exec')!.description).toBe('Sandboxed version'); expect(reg.get('shell.exec')!.description).toBe('Mock shell.exec'); }); }); describe('ToolRegistry — replace()', () => { function makeTool(name: string): Tool { return { name, description: `Mock ${name}`, inputSchema: { type: 'object', properties: {} }, execute: async () => ({ success: true, output: '' }), }; } it('replaces an existing tool', () => { const reg = new ToolRegistry(); reg.register(makeTool('tool.a')); const replacement = makeTool('tool.a'); replacement.description = 'New description'; reg.replace(replacement); expect(reg.get('tool.a')!.description).toBe('New description'); }); it('throws if tool does not exist', () => { const reg = new ToolRegistry(); expect(() => reg.replace(makeTool('nonexistent'))).toThrow('not registered'); }); }); describe('ToolRegistry — API name sanitization', () => { it('sanitizeToolName converts dots to underscores', () => { expect(ToolRegistry.sanitizeToolName('shell.exec')).toBe('shell_exec'); expect(ToolRegistry.sanitizeToolName('file.read')).toBe('file_read'); expect(ToolRegistry.sanitizeToolName('no_dots')).toBe('no_dots'); }); it('getByApiName resolves sanitized names back to internal tools', () => { const registry = new ToolRegistry(); registry.register(echoTool); // name is 'test.echo' expect(registry.getByApiName('test_echo')).toBe(echoTool); expect(registry.getByApiName('test.echo')).toBe(echoTool); expect(registry.getByApiName('nonexistent')).toBeUndefined(); }); it('toAnthropicFormat outputs sanitized names', () => { const registry = new ToolRegistry(); registry.register(echoTool); const tools = registry.toAnthropicFormat(); expect(tools[0].name).toBe('test_echo'); }); it('toOpenAIFormat outputs sanitized names', () => { const registry = new ToolRegistry(); registry.register(echoTool); const tools = registry.toOpenAIFormat(); expect(tools[0].function.name).toBe('test_echo'); }); }); });