213 lines
6.8 KiB
TypeScript
213 lines
6.8 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { ToolRegistry } from './registry.js';
|
|
import type { Tool } from './types.js';
|
|
import type { ToolPolicy } from './policy.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((tools) => tools),
|
|
isAllowed: vi.fn(() => true),
|
|
resolveAllowedNames: vi.fn(() => new Set<string>()),
|
|
getEffectiveProfile: vi.fn<() => 'full'>(() => 'full'),
|
|
} as unknown as ToolPolicy;
|
|
reg.setPolicy(mockPolicy);
|
|
|
|
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);
|
|
const clonedTool = cloned.get('shell.exec');
|
|
const originalToolFromReg = reg.get('shell.exec');
|
|
if (!clonedTool || !originalToolFromReg) {
|
|
throw new Error('Expected shell.exec to exist in both registries');
|
|
}
|
|
expect(clonedTool.description).toBe('Sandboxed version');
|
|
expect(originalToolFromReg.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);
|
|
const replaced = reg.get('tool.a');
|
|
if (!replaced) {
|
|
throw new Error('Expected tool.a to be present');
|
|
}
|
|
expect(replaced.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');
|
|
});
|
|
});
|
|
});
|