UNPKG

bc-symbols-mcp

Version:

MCP server for analyzing Business Central .app files and BC object structures

335 lines 15.4 kB
import { describe, test, expect, beforeEach, afterEach, jest } from '@jest/globals'; import { StreamingSymbolParser } from '../processors/streaming-parser.js'; import { LazyMemoryCache } from '../cache/lazy-cache.js'; import { MemoryOptimizedCache } from '../cache/memory-optimized-cache.js'; import { ProgressiveAppLoader } from '../processors/progressive-loader.js'; import { ObjectIndex } from '../indexing/object-index.js'; describe('Symbol Optimization Tests', () => { let streamingParser; let lazyCache; let optimizedCache; let progressiveLoader; let objectIndex; beforeEach(() => { streamingParser = new StreamingSymbolParser(); lazyCache = new LazyMemoryCache(60, 256); // 60 min, 256MB limit optimizedCache = new MemoryOptimizedCache(60, 256, true, 'lru'); progressiveLoader = new ProgressiveAppLoader(); objectIndex = new ObjectIndex(); }); afterEach(() => { lazyCache.clear(); optimizedCache.clear(); objectIndex.clear(); progressiveLoader.cleanup(); }); describe('StreamingSymbolParser', () => { test('should create object index from buffer', async () => { const mockSymbolData = { RuntimeVersion: '1.0.0', Tables: [ { Id: 18, Name: 'Customer', Properties: [] }, { Id: 23, Name: 'Vendor', Properties: [] } ], Codeunits: [ { Id: 12, Name: 'Gen. Jnl.-Post Line', Properties: [] } ] }; const buffer = Buffer.from(JSON.stringify(mockSymbolData)); const index = await streamingParser.parseSymbolsProgressive(buffer); expect(index.runtimeVersion).toBe('1.0.0'); expect(index.totalObjects).toBe(3); expect(index.loadedObjects).toBe(0); expect(index.objectIndex.size).toBe(3); }); test('should provide object metadata without loading', () => { const mockSymbolData = { RuntimeVersion: '1.0.0', Tables: [ { Id: 18, Name: 'Customer', Properties: [] } ] }; const buffer = Buffer.from(JSON.stringify(mockSymbolData)); streamingParser.parseSymbolsProgressive(buffer).then(() => { const metadata = streamingParser.getObjectMetadata('table', 18, 'Customer'); expect(metadata).toHaveLength(1); expect(metadata[0].objectType).toBe('table'); expect(metadata[0].objectId).toBe(18); expect(metadata[0].name).toBe('Customer'); }); }); test('should track loading statistics', () => { const stats = streamingParser.getLoadingStats(); expect(stats).toHaveProperty('total'); expect(stats).toHaveProperty('loaded'); expect(stats).toHaveProperty('percentage'); }); }); describe('LazyMemoryCache', () => { test('should store and retrieve apps lazily', async () => { const mockApp = { id: 'test-app', name: 'Test App', publisher: 'Test Publisher', version: '1.0.0', fileHash: 'hash123', filePath: '/test/app.app' }; const mockSymbolsBuffer = Buffer.from('{"RuntimeVersion":"1.0.0"}'); await lazyCache.setLazy('/test/app.app', mockApp, mockSymbolsBuffer); const retrievedApp = lazyCache.getAppMetadata('/test/app.app', 'hash123'); expect(retrievedApp).toBeTruthy(); expect(retrievedApp?.id).toBe('test-app'); }); test('should provide cache statistics', async () => { const stats = lazyCache.getCacheStats(); expect(stats).toHaveProperty('totalApps'); expect(stats).toHaveProperty('totalObjects'); expect(stats).toHaveProperty('loadedObjects'); expect(stats).toHaveProperty('memoryUsage'); expect(stats).toHaveProperty('cacheHitRate'); }); test('should enforce memory limits', async () => { const smallCache = new LazyMemoryCache(60, 5); // 5MB limit to allow for some overhead const mockApp = { id: 'large-app', fileHash: 'hash123', filePath: '/large/app.app' }; // Create a buffer that's within reasonable limits for testing const symbolsBuffer = Buffer.from(JSON.stringify({ RuntimeVersion: '1.0.0', Tables: Array.from({ length: 10 }, (_, i) => ({ Id: i, Name: `Table${i}` })) })); await smallCache.setLazy('/large/app.app', mockApp, symbolsBuffer); const stats = smallCache.getCacheStats(); expect(stats.memoryUsage).toBeLessThanOrEqual(5); }); }); describe('MemoryOptimizedCache', () => { test('should store apps in partitioned structure', () => { const mockApp = { id: 'test-app', fileHash: 'hash123', symbols: { runtimeVersion: '1.0.0', namespaces: [] } }; optimizedCache.setApp('/test/app.app', mockApp); const retrievedApp = optimizedCache.getApp('/test/app.app', 'hash123'); expect(retrievedApp).toBeTruthy(); expect(retrievedApp?.id).toBe('test-app'); }); test('should store objects in type-specific partitions', () => { const mockTables = [ { id: 18, name: 'Customer' }, { id: 23, name: 'Vendor' } ]; optimizedCache.setObjects('app1', 'table', mockTables); const retrievedTables = optimizedCache.getObjects('app1', 'table'); expect(retrievedTables).toHaveLength(2); expect(retrievedTables?.[0].name).toBe('Customer'); }); test('should provide detailed memory statistics', () => { const stats = optimizedCache.getMemoryStats(); expect(stats).toHaveProperty('totalMemoryMB'); expect(stats).toHaveProperty('partitionSizes'); expect(stats).toHaveProperty('largestObjects'); expect(stats).toHaveProperty('compressionRatio'); }); test('should handle different eviction strategies', () => { const lruCache = new MemoryOptimizedCache(60, 256, false, 'lru'); const lfuCache = new MemoryOptimizedCache(60, 256, false, 'lfu'); const sizeCache = new MemoryOptimizedCache(60, 256, false, 'size-based'); expect(lruCache).toBeTruthy(); expect(lfuCache).toBeTruthy(); expect(sizeCache).toBeTruthy(); }); }); describe('ProgressiveAppLoader', () => { test('should provide loading progress updates', async () => { const progressUpdates = []; const mockOptions = { batchSize: 10, delayBetweenBatches: 1, priorityObjectTypes: ['table'], backgroundLoadingEnabled: false, progressCallback: (progress) => progressUpdates.push(progress) }; // Test that progress callback is called if (mockOptions.progressCallback) { mockOptions.progressCallback({ phase: 'metadata', percentage: 10, message: 'Loading metadata...', objectsLoaded: 0, totalObjects: 0 }); } expect(progressUpdates.length).toBeGreaterThan(0); expect(progressUpdates[0].phase).toBe('metadata'); }); test('should handle background loading', async () => { const activeTasks = progressiveLoader.getActiveLoadingTasks(); expect(Array.isArray(activeTasks)).toBe(true); }); test('should support preloading specific object types', async () => { const mockObjectTypes = ['table', 'codeunit']; // Test method exists and can be called expect(typeof progressiveLoader.preloadObjectTypes).toBe('function'); // Test that it accepts the expected parameters const result = progressiveLoader.preloadObjectTypes('/test/app.app', mockObjectTypes); expect(result).toBeInstanceOf(Promise); try { await result; } catch (error) { // Expected to fail since we don't have a real app file, but method should exist expect(error).toBeDefined(); } }); }); describe('ObjectIndex', () => { test('should index objects with metadata', async () => { // Clear the index first objectIndex.clear(); const mockApp = { id: 'test-app', filePath: '/test/app.app' }; const mockObjectLoader = { getObjectMetadata: (filePath, objectType) => { if (objectType === 'table') { return [ { objectType: 'table', objectId: 18, name: 'Customer', namespace: 'Root', properties: [] } ]; } return []; // Return empty array for other object types } }; await objectIndex.indexApp(mockApp, mockObjectLoader); const stats = objectIndex.getStats(); expect(stats.totalObjects).toBe(1); expect(stats.objectsByType['table']).toBe(1); }); test('should support complex searches', () => { // Test empty search const results = objectIndex.search({ objectTypes: ['table'], keywords: ['customer'] }); // Should return empty array since no objects are indexed expect(Array.isArray(results)).toBe(true); expect(results).toHaveLength(0); }); test('should find objects by partial name', () => { const results = objectIndex.findByPartialName('Cust'); expect(Array.isArray(results)).toBe(true); }); test('should provide index statistics', () => { const stats = objectIndex.getStats(); expect(stats).toHaveProperty('totalObjects'); expect(stats).toHaveProperty('objectsByType'); expect(stats).toHaveProperty('objectsByApp'); expect(stats).toHaveProperty('memoryUsage'); expect(stats).toHaveProperty('indexingTime'); }); }); describe('Performance Tests', () => { test('should handle large symbol files efficiently', async () => { const startTime = Date.now(); // Create a mock large symbol file const largeSymbolData = { RuntimeVersion: '1.0.0', Tables: Array.from({ length: 1000 }, (_, i) => ({ Id: i + 1, Name: `Table${i + 1}`, Properties: [] })) }; const buffer = Buffer.from(JSON.stringify(largeSymbolData)); const index = await streamingParser.parseSymbolsProgressive(buffer); const processingTime = Date.now() - startTime; expect(index.totalObjects).toBe(1000); expect(processingTime).toBeLessThan(5000); // Should complete within 5 seconds }); test('should maintain reasonable memory usage', async () => { const initialMemory = process.memoryUsage().heapUsed; // Load multiple apps for (let i = 0; i < 10; i++) { const mockApp = { id: `app-${i}`, fileHash: `hash-${i}`, filePath: `/test/app-${i}.app` }; const mockBuffer = Buffer.from(`{"RuntimeVersion":"1.0.0","Tables":[{"Id":${i},"Name":"Table${i}"}]}`); await lazyCache.setLazy(`/test/app-${i}.app`, mockApp, mockBuffer); } const finalMemory = process.memoryUsage().heapUsed; const memoryIncrease = (finalMemory - initialMemory) / (1024 * 1024); // MB expect(memoryIncrease).toBeLessThan(100); // Should not increase by more than 100MB }); test('should provide fast object lookups', async () => { // Build index with many objects const mockApp = { id: 'perf-app', filePath: '/perf/app.app' }; const mockLoader = { getObjectMetadata: jest.fn().mockReturnValue(Array.from({ length: 1000 }, (_, i) => ({ objectType: 'table', objectId: i + 1, name: `Table${i + 1}`, namespace: 'Root' }))) }; await objectIndex.indexApp(mockApp, mockLoader); const startTime = Date.now(); // Perform multiple searches for (let i = 0; i < 100; i++) { objectIndex.search({ objectTypes: ['table'] }); } const searchTime = Date.now() - startTime; expect(searchTime).toBeLessThan(1000); // 100 searches should complete within 1 second }); }); describe('Integration Tests', () => { test('should work together as a complete optimization system', async () => { // Test basic integration without complex operations const mockApp = { id: 'integration-app', name: 'Integration Test App', publisher: 'Test Publisher', version: '1.0.0', fileHash: 'integration-hash', filePath: '/integration/app.app', symbols: { runtimeVersion: '1.0.0', namespaces: [] } }; // 1. Set up lazy cache const symbolsBuffer = Buffer.from(JSON.stringify({ RuntimeVersion: '1.0.0', Tables: [{ Id: 18, Name: 'Customer', Properties: [] }] })); await lazyCache.setLazy(mockApp.filePath, mockApp, symbolsBuffer); // 2. Index objects const mockLoader = { getObjectMetadata: () => [{ objectType: 'table', objectId: 18, name: 'Customer', namespace: 'Root' }] }; await objectIndex.indexApp(mockApp, mockLoader); // 3. Verify components work together const cacheStats = lazyCache.getCacheStats(); const indexStats = objectIndex.getStats(); expect(cacheStats.totalApps).toBeGreaterThan(0); expect(indexStats.totalObjects).toBeGreaterThan(0); // Test progressive loader exists expect(typeof progressiveLoader.getActiveLoadingTasks).toBe('function'); }); }); }); //# sourceMappingURL=optimization.test.js.map