Skip to main content
Glama
tool-config-interface.test.tsβ€’14.2 kB
/** * Tests for the refactored UniversalToolConfig interface * Validates the new generic type system from Phase 1 */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import type { UniversalToolConfig } from '@handlers/tool-configs/universal/types.js'; import { AttioRecord } from '@shared-types/attio.js'; describe('UniversalToolConfig Interface', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('Generic Type Safety', () => { it('should enforce parameter type safety', async () => { interface TestParams { query: string; limit: number; } interface TestResult { data: string[]; count: number; } const testConfig: UniversalToolConfig<TestParams, TestResult> = { name: 'test-tool', handler: async (params: TestParams): Promise<TestResult> => { // TypeScript should enforce that params is TestParams expect(params.query).toBeDefined(); expect(params.limit).toBeDefined(); return { data: [params.query], count: params.limit, }; }, formatResult: (results: TestResult, ...args: unknown[]): string => { // TypeScript should enforce that results is TestResult expect(results.data).toBeDefined(); expect(results.count).toBeDefined(); return `Found ${results.count} results: ${results.data.join(', ')}`; }, }; const params: TestParams = { query: 'test', limit: 5 }; const result = await testConfig.handler(params); const formatted = testConfig.formatResult(result); expect(result.data).toEqual(['test']); expect(result.count).toBe(5); expect(formatted).toBe('Found 5 results: test'); }); it('should work with AttioRecord array results', async () => { interface SearchParams { resource_type: string; query: string; } const searchConfig: UniversalToolConfig<SearchParams, AttioRecord[]> = { name: 'search-records', handler: async (params: SearchParams): Promise<AttioRecord[]> => { return [ { id: { record_id: 'record-1' }, values: { name: [{ value: 'Test Record' }] }, created_at: '2023-01-01T00:00:00Z', updated_at: '2023-01-01T00:00:00Z', }, ]; }, formatResult: (results: AttioRecord[], ...args: unknown[]): string => { const resourceType = args[0] as string | undefined; if (!Array.isArray(results)) { return 'No records found'; } return `Found ${results.length} ${resourceType || 'record'}(s)`; }, }; const params: SearchParams = { resource_type: 'companies', query: 'test', }; const result = await searchConfig.handler(params); const formatted = searchConfig.formatResult(result, 'companies'); expect(result).toHaveLength(1); expect(result[0].id.record_id).toBe('record-1'); expect(formatted).toBe('Found 1 companies(s)'); }); it('should handle default generic types', async () => { // Test with default types (Record<string, unknown> and unknown) const defaultConfig: UniversalToolConfig = { name: 'default-tool', handler: async (params: Record<string, unknown>) => { return { message: 'success', data: params }; }, formatResult: (results: unknown): string => { const typedResults = results as { message: string; data: Record<string, unknown>; }; return `Result: ${typedResults.message}`; }, }; const params = { test: 'value' }; const result = await defaultConfig.handler(params); const formatted = defaultConfig.formatResult(result); expect(result).toEqual({ message: 'success', data: params }); expect(formatted).toBe('Result: success'); }); }); describe('Handler Function Signatures', () => { it('should accept async handlers that return promises', async () => { interface SimpleParams { id: string; } const asyncConfig: UniversalToolConfig<SimpleParams, string> = { name: 'async-tool', handler: async (params: SimpleParams): Promise<string> => { // Simulate async operation await new Promise((resolve) => setTimeout(resolve, 1)); return `Processed ${params.id}`; }, formatResult: (results: string): string => { return `Output: ${results}`; }, }; const result = await asyncConfig.handler({ id: 'test-123' }); expect(result).toBe('Processed test-123'); const formatted = asyncConfig.formatResult(result); expect(formatted).toBe('Output: Processed test-123'); }); it('should handle error cases in handlers', async () => { interface ErrorParams { shouldFail: boolean; } const errorConfig: UniversalToolConfig<ErrorParams, string> = { name: 'error-tool', handler: async (params: ErrorParams): Promise<string> => { if (params.shouldFail) { throw new Error('Intentional test error'); } return 'success'; }, formatResult: (results: string): string => { return results; }, }; // Test success case const successResult = await errorConfig.handler({ shouldFail: false }); expect(successResult).toBe('success'); // Test error case await expect(errorConfig.handler({ shouldFail: true })).rejects.toThrow( 'Intentional test error' ); }); }); describe('FormatResult Function Signatures', () => { it('should handle multiple arguments through rest parameters', () => { interface MultiArgParams { type: string; } interface MultiArgResult { items: string[]; } const multiArgConfig: UniversalToolConfig< MultiArgParams, MultiArgResult > = { name: 'multi-arg-tool', handler: async (params: MultiArgParams): Promise<MultiArgResult> => { return { items: [`item-${params.type}`] }; }, formatResult: (results: MultiArgResult, ...args: unknown[]): string => { const prefix = args[0] as string | undefined; const suffix = args[1] as string | undefined; const includeCount = args[2] as boolean | undefined; let output = results.items.join(', '); if (prefix) output = `${prefix}: ${output}`; if (suffix) output = `${output} ${suffix}`; if (includeCount) output = `${output} (${results.items.length} items)`; return output; }, }; const result = { items: ['test-item'] }; // Test with no extra args expect(multiArgConfig.formatResult(result)).toBe('test-item'); // Test with one arg expect(multiArgConfig.formatResult(result, 'Items')).toBe( 'Items: test-item' ); // Test with multiple args expect(multiArgConfig.formatResult(result, 'Items', '!', true)).toBe( 'Items: test-item ! (1 items)' ); }); it('should always return string type', () => { interface StringTestParams { value: number; } const stringConfig: UniversalToolConfig<StringTestParams, number> = { name: 'string-test', handler: async (params: StringTestParams): Promise<number> => { return params.value * 2; }, formatResult: (results: number): string => { // Must return string, not number return `Result: ${results}`; }, }; const formatted = stringConfig.formatResult(42); expect(typeof formatted).toBe('string'); expect(formatted).toBe('Result: 42'); }); it('should handle complex nested result types', () => { interface ComplexResult { metadata: { total: number; page: number; }; records: Array<{ id: string; data: Record<string, unknown>; }>; } const complexConfig: UniversalToolConfig< Record<string, unknown>, ComplexResult > = { name: 'complex-tool', handler: async (): Promise<ComplexResult> => { return { metadata: { total: 2, page: 1 }, records: [ { id: '1', data: { name: 'Record 1' } }, { id: '2', data: { name: 'Record 2' } }, ], }; }, formatResult: (results: ComplexResult, ...args: unknown[]): string => { const showMetadata = args[0] as boolean | undefined; let output = `Found ${results.records.length} records:\n`; output += results.records .map((r) => `- ${r.id}: ${JSON.stringify(r.data)}`) .join('\n'); if (showMetadata) { output += `\nMetadata: Total ${results.metadata.total}, Page ${results.metadata.page}`; } return output; }, }; const result: ComplexResult = { metadata: { total: 2, page: 1 }, records: [ { id: '1', data: { name: 'Record 1' } }, { id: '2', data: { name: 'Record 2' } }, ], }; const formattedWithoutMeta = complexConfig.formatResult(result); expect(formattedWithoutMeta).toContain('Found 2 records:'); expect(formattedWithoutMeta).not.toContain('Metadata:'); const formattedWithMeta = complexConfig.formatResult(result, true); expect(formattedWithMeta).toContain('Found 2 records:'); expect(formattedWithMeta).toContain('Metadata: Total 2, Page 1'); }); }); describe('Type Inheritance and Compatibility', () => { it('should be compatible with ToolConfig base interface', () => { // This test ensures UniversalToolConfig properly extends ToolConfig interface BasicParams { id: string; } const config: UniversalToolConfig<BasicParams, string> = { name: 'inheritance-test', handler: async (params: BasicParams): Promise<string> => { return `Handled ${params.id}`; }, formatResult: (results: string): string => { return results; }, }; // Should have the required ToolConfig properties expect(config.name).toBe('inheritance-test'); expect(typeof config.handler).toBe('function'); expect(typeof config.formatResult).toBe('function'); }); it('should work with union types', () => { type UnionResult = string | number | boolean; const unionConfig: UniversalToolConfig< Record<string, unknown>, UnionResult > = { name: 'union-test', handler: async ( params: Record<string, unknown> ): Promise<UnionResult> => { const type = params.type as string; switch (type) { case 'string': return 'text'; case 'number': return 42; case 'boolean': return true; default: return 'unknown'; } }, formatResult: (results: UnionResult): string => { return `Value: ${results} (type: ${typeof results})`; }, }; const stringResult = unionConfig.formatResult('text'); expect(stringResult).toBe('Value: text (type: string)'); const numberResult = unionConfig.formatResult(42); expect(numberResult).toBe('Value: 42 (type: number)'); const booleanResult = unionConfig.formatResult(true); expect(booleanResult).toBe('Value: true (type: boolean)'); }); }); describe('Edge Cases and Error Handling', () => { it('should handle null and undefined results gracefully', () => { const nullConfig: UniversalToolConfig<Record<string, unknown>, null> = { name: 'null-test', handler: async (): Promise<null> => { return null; }, formatResult: (results: null): string => { return results === null ? 'No data' : 'Unexpected data'; }, }; const formatted = nullConfig.formatResult(null); expect(formatted).toBe('No data'); }); it('should handle empty arrays and objects', () => { interface EmptyTestResult { items: never[]; metadata: Record<string, never>; } const emptyConfig: UniversalToolConfig< Record<string, unknown>, EmptyTestResult > = { name: 'empty-test', handler: async (): Promise<EmptyTestResult> => { return { items: [], metadata: {}, }; }, formatResult: (results: EmptyTestResult): string => { if (results.items.length === 0) { return 'No items found'; } return `Found ${results.items.length} items`; }, }; const result = { items: [], metadata: {} }; const formatted = emptyConfig.formatResult(result); expect(formatted).toBe('No items found'); }); it('should handle large result sets without performance issues', () => { interface LargeResult { data: string[]; } const largeConfig: UniversalToolConfig< Record<string, unknown>, LargeResult > = { name: 'large-test', handler: async (): Promise<LargeResult> => { // Create a large array const data = Array.from({ length: 1000 }, (_, i) => `item-${i}`); return { data }; }, formatResult: (results: LargeResult): string => { // Should handle large arrays efficiently return `Found ${results.data.length} items (showing first 3): ${results.data.slice(0, 3).join(', ')}...`; }, }; const result = { data: Array.from({ length: 1000 }, (_, i) => `item-${i}`), }; const formatted = largeConfig.formatResult(result); expect(formatted).toBe( 'Found 1000 items (showing first 3): item-0, item-1, item-2...' ); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kesslerio/attio-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server