Files
flynn/src/tools/registry.test.ts
T
William Valentin 6bb424cddc feat: add agent tools and sanitize tool names for Anthropic API
Add 8 new agent-callable tools (sessions.list/history/create/delete,
agents.list, message.send, cron.list/trigger) and sanitize tool names
at the API boundary (dots → underscores) to comply with Anthropic's
`^[a-zA-Z0-9_-]{1,128}` requirement. Reverse-maps sanitized names
back to internal names for hook callbacks and tool execution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 12:23:09 -08:00

198 lines
6.3 KiB
TypeScript

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');
});
});
});