Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
submitContentMethod.test.tsโ€ข12.1 kB
/** * Unit tests for submitContent method improvements * Tests parallel search, error handling, and logging enhancements */ import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals'; describe('submitContent method improvements', () => { describe('Parallel Search Implementation', () => { it('should demonstrate parallel search pattern', async () => { // This test validates the parallel search pattern used in submitContent // NOTE: Using example types here - the actual code uses Object.values(ElementType) // which dynamically includes ALL element types, whether there are 6, 10, or 100 const elementTypes = ['personas', 'skills', 'templates', 'agents', 'memories', 'ensembles']; const searchResults: { type: string; startTime: number; endTime: number }[] = []; // Simulate parallel searches const searchPromises = elementTypes.map(async (type) => { const startTime = Date.now(); // Simulate async file search with varying delays await new Promise(resolve => setTimeout(resolve, Math.random() * 100)); const endTime = Date.now(); searchResults.push({ type, startTime, endTime }); // Return result for 'skills' to simulate finding a match return type === 'skills' ? { type, file: `/path/to/${type}/sample.md` } : null; }); // Wait for all searches using Promise.allSettled const results = await Promise.allSettled(searchPromises); // Verify all searches completed (works with any number of element types) expect(results).toHaveLength(elementTypes.length); expect(results.every(r => r.status === 'fulfilled')).toBe(true); // Verify parallel execution - searches should overlap in time const sortedByStart = [...searchResults].sort((a, b) => a.startTime - b.startTime); const firstEnd = sortedByStart[0].endTime; const lastStart = sortedByStart[sortedByStart.length - 1].startTime; // In parallel execution, the last search should start before the first one ends expect(lastStart).toBeLessThanOrEqual(firstEnd + 10); // Allow 10ms tolerance // Find the match const match = results.find(r => r.status === 'fulfilled' && r.value); expect(match).toBeDefined(); expect((match as any).value.type).toBe('skills'); }); it('should handle any number of element types dynamically', async () => { // Test with different numbers of element types to prove no hardcoded assumptions const testCases = [ ['personas', 'skills'], // 2 types ['personas', 'skills', 'templates', 'agents'], // 4 types ['personas', 'skills', 'templates', 'agents', 'memories', 'ensembles'], // 6 types // Could have 20 types: Array.from({ length: 20 }, (_, i) => `type${i}`) // 20 types ]; for (const types of testCases) { const searchPromises = types.map(async (type) => { await new Promise(resolve => setTimeout(resolve, 5)); return type === types[0] ? { type, file: `/path/${type}/sample.md` } : null; }); const results = await Promise.allSettled(searchPromises); // Works correctly regardless of the number of types expect(results).toHaveLength(types.length); expect(results.every(r => r.status === 'fulfilled')).toBe(true); // Finds the match regardless of how many types exist const match = results.find(r => r.status === 'fulfilled' && r.value && (r.value as any).file ); expect(match).toBeDefined(); } }); it('should handle mixed success and failure in parallel searches', async () => { const searchPromises = [ Promise.resolve({ type: 'personas', file: null }), Promise.reject(new Error('Permission denied')), Promise.resolve({ type: 'templates', file: '/path/to/template.md' }), Promise.resolve({ type: 'agents', file: null }) ]; const results = await Promise.allSettled(searchPromises); // Should handle both successes and failures expect(results).toHaveLength(4); expect(results[0].status).toBe('fulfilled'); expect(results[1].status).toBe('rejected'); expect(results[2].status).toBe('fulfilled'); expect(results[3].status).toBe('fulfilled'); // Should still find the successful match const match = results.find(r => r.status === 'fulfilled' && r.value && (r.value as any).file ); expect(match).toBeDefined(); expect((match as any).value.file).toBe('/path/to/template.md'); }); }); describe('Error Handling Patterns', () => { it('should distinguish between expected and unexpected errors', () => { const errors = [ { code: 'ENOENT', message: 'No such file or directory' }, { code: 'ENOTDIR', message: 'Not a directory' }, { code: 'EACCES', message: 'Permission denied' }, { code: 'EIO', message: 'I/O error' }, { code: undefined, message: 'Unknown error' } ]; const categorized = errors.map(error => { const isExpected = error.code === 'ENOENT' || error.code === 'ENOTDIR'; return { ...error, logLevel: isExpected ? 'debug' : 'warn' }; }); // ENOENT and ENOTDIR should be debug level expect(categorized[0].logLevel).toBe('debug'); expect(categorized[1].logLevel).toBe('debug'); // Permission and I/O errors should be warnings expect(categorized[2].logLevel).toBe('warn'); expect(categorized[3].logLevel).toBe('warn'); // Unknown errors should be warnings expect(categorized[4].logLevel).toBe('warn'); }); it('should format error logs with appropriate context', () => { const error = { code: 'EACCES', message: 'Permission denied', path: '/home/user/.dollhouse/portfolio/agents' }; const logContext = { contentIdentifier: 'test-agent', type: 'agents', error: error.message, code: error.code }; // Verify all required context is included expect(logContext).toHaveProperty('contentIdentifier'); expect(logContext).toHaveProperty('type'); expect(logContext).toHaveProperty('error'); expect(logContext).toHaveProperty('code'); expect(logContext.code).toBe('EACCES'); }); }); describe('Logging Enhancements', () => { it('should use appropriate log levels', () => { const scenarios = [ { event: 'content_found', level: 'debug' }, { event: 'content_not_found', level: 'info' }, { event: 'directory_missing', level: 'debug' }, { event: 'permission_error', level: 'warn' }, { event: 'search_started', level: 'debug' } ]; // Verify each scenario uses the correct log level expect(scenarios[0].level).toBe('debug'); // Found content expect(scenarios[1].level).toBe('info'); // Not found (user should know) expect(scenarios[2].level).toBe('debug'); // Expected missing dir expect(scenarios[3].level).toBe('warn'); // Unexpected error expect(scenarios[4].level).toBe('debug'); // Internal operation }); it('should include searched types when content not found', () => { const elementTypes = ['personas', 'skills', 'templates', 'agents', 'memories', 'ensembles']; const notFoundLogData = { contentIdentifier: 'test-content', searchedTypes: elementTypes }; // Verify the log data includes all searched types expect(notFoundLogData.searchedTypes).toEqual(elementTypes); expect(notFoundLogData.searchedTypes).toHaveLength(6); expect(notFoundLogData.contentIdentifier).toBe('test-content'); }); }); describe('Performance Characteristics', () => { it('should complete faster with parallel search', async () => { const SEARCH_DELAY = 50; // ms per search const ELEMENT_COUNT = 6; // Could be any number - 6, 10, 20, etc. // Sequential search time const sequentialStart = Date.now(); for (let i = 0; i < ELEMENT_COUNT; i++) { await new Promise(resolve => setTimeout(resolve, SEARCH_DELAY)); } const sequentialTime = Date.now() - sequentialStart; // Parallel search time const parallelStart = Date.now(); const promises = new Array(ELEMENT_COUNT).fill(0).map(() => new Promise(resolve => setTimeout(resolve, SEARCH_DELAY)) ); await Promise.allSettled(promises); const parallelTime = Date.now() - parallelStart; // Parallel should be significantly faster expect(parallelTime).toBeLessThan(sequentialTime); // Parallel should be much faster than sequential (at least 2x faster) // This is a relative comparison, not absolute timing expect(parallelTime).toBeLessThan(sequentialTime / 2); // Sequential should take roughly the sum of all searches expect(sequentialTime).toBeGreaterThanOrEqual(SEARCH_DELAY * ELEMENT_COUNT * 0.9); // 90% minimum }); it('should stop searching after first match', async () => { let searchCount = 0; const searchPromises = ['personas', 'skills', 'templates'].map(async (type) => { searchCount++; await new Promise(resolve => setTimeout(resolve, 10)); return type === 'skills' ? { type, file: '/path/to/skill.md' } : null; }); const results = await Promise.allSettled(searchPromises); // All searches run in parallel expect(searchCount).toBe(3); // But we only use the first match let matchFound = false; for (const result of results) { if (result.status === 'fulfilled' && result.value && (result.value as any).file) { expect((result.value as any).type).toBe('skills'); matchFound = true; break; // Stop at first match } } expect(matchFound).toBe(true); }); }); describe('Edge Cases', () => { it('should handle all searches failing', async () => { const searchPromises = [ Promise.reject(new Error('Error 1')), Promise.reject(new Error('Error 2')), Promise.reject(new Error('Error 3')) ]; const results = await Promise.allSettled(searchPromises); // All should be rejected but handled gracefully expect(results.every(r => r.status === 'rejected')).toBe(true); // Should NOT fall back to default - this was the critical fix // The system should now return an error or ask user to specify type let selectedType = undefined; for (const result of results) { if (result.status === 'fulfilled' && result.value) { selectedType = (result.value as any).type; break; } } // No match found, should NOT default to any type expect(selectedType).toBeUndefined(); // The fix ensures we don't default to personas // Instead, the system will return an error message asking for --type parameter }); it('should handle empty search results', async () => { const searchPromises = [ Promise.resolve(null), Promise.resolve(null), Promise.resolve(null) ]; const results = await Promise.allSettled(searchPromises); // All successful but no matches expect(results.every(r => r.status === 'fulfilled')).toBe(true); expect(results.every(r => (r as any).value === null)).toBe(true); // Should NOT fall back to default - this was the critical fix // The system will return an error message listing available types const match = results.find(r => r.status === 'fulfilled' && r.value && (r.value as any).file ); expect(match).toBeUndefined(); }); }); });

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/DollhouseMCP/DollhouseMCP'

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