Skip to main content
Glama
EnhancedMCPServerFixed.test.ts48.3 kB
/** * Core Tests for EnhancedMCPServerFixed * * Tests tool execution paths, state management, and error handling * to increase coverage from 35% to 70%+ */ import { EnhancedMCPServerFixed } from '../server/EnhancedMCPServerFixed'; import { StrudelController } from '../StrudelController'; import { PatternStore } from '../PatternStore'; import { MusicTheory } from '../services/MusicTheory'; import { PatternGenerator } from '../services/PatternGenerator'; // Mock all dependencies jest.mock('../StrudelController'); jest.mock('../PatternStore'); jest.mock('../services/MusicTheory'); jest.mock('../services/PatternGenerator'); jest.mock('fs', () => ({ readFileSync: jest.fn().mockReturnValue('{"headless": true}'), existsSync: jest.fn().mockReturnValue(true) })); describe('EnhancedMCPServerFixed', () => { let server: EnhancedMCPServerFixed; let mockController: jest.Mocked<StrudelController>; let mockStore: jest.Mocked<PatternStore>; let mockTheory: jest.Mocked<MusicTheory>; let mockGenerator: jest.Mocked<PatternGenerator>; beforeEach(() => { // Setup mocks mockController = { initialize: jest.fn().mockResolvedValue('Strudel initialized'), getCurrentPattern: jest.fn().mockResolvedValue('s("bd*4")'), writePattern: jest.fn().mockResolvedValue('Pattern written: 7 chars'), play: jest.fn().mockResolvedValue('Playing'), stop: jest.fn().mockResolvedValue('Stopped'), analyzeAudio: jest.fn().mockResolvedValue({ connected: true, features: { bass: 0.5, mid: 0.5, treble: 0.5, isPlaying: true, isSilent: false } }), detectKey: jest.fn().mockResolvedValue({ key: 'C', scale: 'major', confidence: 0.85, alternatives: [] }), detectTempo: jest.fn().mockResolvedValue({ bpm: 120, confidence: 0.85, method: 'autocorrelation' }), analyzer: { detectTempo: jest.fn().mockResolvedValue({ bpm: 120, confidence: 0.85, method: 'autocorrelation' }) }, validatePatternRuntime: jest.fn().mockResolvedValue({ valid: true, errors: [], warnings: [] }), validatePattern: jest.fn().mockResolvedValue({ valid: true, errors: [], warnings: [], suggestions: [] }), showBrowser: jest.fn().mockResolvedValue('Browser window brought to foreground'), takeScreenshot: jest.fn().mockResolvedValue('Screenshot saved to strudel-screenshot.png'), getStatus: jest.fn().mockReturnValue({ initialized: true, playing: false, patternLength: 10, cacheValid: true, errorCount: 0, warningCount: 0 }), getDiagnostics: jest.fn().mockResolvedValue({ browserConnected: true, pageLoaded: true, editorReady: true, audioConnected: false, cacheStatus: { hasCache: true, cacheAge: 100 }, errorStats: {} }), getConsoleErrors: jest.fn().mockReturnValue([]), getConsoleWarnings: jest.fn().mockReturnValue([]), page: {} } as any; mockStore = { save: jest.fn().mockResolvedValue(undefined), load: jest.fn().mockResolvedValue({ name: 'test-pattern', content: 's("bd cp")', tags: ['test'], timestamp: new Date().toISOString() }), list: jest.fn().mockResolvedValue([ { name: 'pattern1', tags: ['techno'], timestamp: new Date().toISOString() } ]) } as any; mockTheory = { generateScale: jest.fn().mockReturnValue(['C', 'D', 'E', 'F', 'G', 'A', 'B']), generateChordProgression: jest.fn().mockReturnValue('I-V-vi-IV') } as any; mockGenerator = { generateCompletePattern: jest.fn().mockReturnValue('s("bd*4 cp*2")'), generateDrumPattern: jest.fn().mockReturnValue('s("bd*4")'), generateBassline: jest.fn().mockReturnValue('s("c2 e2 g2")'), generateMelody: jest.fn().mockReturnValue('note("c4 d4 e4")'), generateChords: jest.fn().mockReturnValue('s("C Em Am F")'), generateEuclideanPattern: jest.fn().mockReturnValue('s("bd").euclidean(3, 8)'), generatePolyrhythm: jest.fn().mockReturnValue('s("bd").euclidean(3, 8)'), generateFill: jest.fn().mockReturnValue('s("bd*16")'), generateVariation: jest.fn().mockReturnValue('s("bd*4 cp*2").fast(2)') } as any; // Mock constructors (StrudelController as jest.Mock).mockReturnValue(mockController); (PatternStore as jest.Mock).mockReturnValue(mockStore); (MusicTheory as jest.Mock).mockReturnValue(mockTheory); (PatternGenerator as jest.Mock).mockReturnValue(mockGenerator); server = new EnhancedMCPServerFixed(); }); afterEach(() => { jest.clearAllMocks(); }); describe('Core Control Tools', () => { describe('init', () => { test('should initialize controller', async () => { const result = await (server as any).executeTool('init', {}); expect(mockController.initialize).toHaveBeenCalled(); expect(result).toContain('initialized'); }); test('should load pending patterns after init', async () => { // Generate a pattern before init await (server as any).executeTool('generate_pattern', { style: 'techno' }); const result = await (server as any).executeTool('init', {}); expect(result).toContain('Loaded generated pattern'); expect(mockController.writePattern).toHaveBeenCalled(); }); }); describe('write', () => { beforeEach(async () => { await (server as any).executeTool('init', {}); }); test('should write pattern', async () => { const result = await (server as any).executeTool('write', { pattern: 's("bd*4")' }); expect(mockController.writePattern).toHaveBeenCalledWith('s("bd*4")'); expect(result).toContain('Pattern written'); }); test('should validate pattern length', async () => { const longPattern = 'a'.repeat(10001); await expect( (server as any).executeTool('write', { pattern: longPattern }) ).rejects.toThrow('pattern too long'); }); test('should save to undo stack', async () => { await (server as any).executeTool('write', { pattern: 's("bd*4")' }); const undoResult = await (server as any).executeTool('undo', {}); expect(undoResult).toBe('Undone'); }); test('should require initialization for write', async () => { const uninitServer = new EnhancedMCPServerFixed(); const result = await (uninitServer as any).executeTool('write', { pattern: 's("bd*4")' }); expect(result).toContain('not initialized'); }); }); describe('append', () => { beforeEach(async () => { await (server as any).executeTool('init', {}); }); test('should append to current pattern', async () => { mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); await (server as any).executeTool('append', { code: 's("cp*2")' }); expect(mockController.writePattern).toHaveBeenCalledWith('s("bd*4")\ns("cp*2")'); }); test('should validate code length', async () => { const longCode = 'a'.repeat(10001); await expect( (server as any).executeTool('append', { code: longCode }) ).rejects.toThrow('code too long'); }); }); describe('insert', () => { beforeEach(async () => { await (server as any).executeTool('init', {}); }); test('should insert at specific line', async () => { mockController.getCurrentPattern.mockResolvedValue('line1\nline3'); await (server as any).executeTool('insert', { position: 1, code: 'line2' }); expect(mockController.writePattern).toHaveBeenCalledWith('line1\nline2\nline3'); }); test('should validate position is positive integer', async () => { await expect( (server as any).executeTool('insert', { position: -1, code: 'test' }) ).rejects.toThrow(); }); test('should validate position is integer', async () => { await expect( (server as any).executeTool('insert', { position: 1.5, code: 'test' }) ).rejects.toThrow('integer'); }); }); describe('replace', () => { beforeEach(async () => { await (server as any).executeTool('init', {}); }); test('should replace text in pattern', async () => { mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); await (server as any).executeTool('replace', { search: 'bd', replace: 'cp' }); expect(mockController.writePattern).toHaveBeenCalledWith('s("cp*4")'); }); test('should validate search string', async () => { const longSearch = 'a'.repeat(1001); await expect( (server as any).executeTool('replace', { search: longSearch, replace: 'test' }) ).rejects.toThrow(); }); }); describe('play/pause/stop', () => { beforeEach(async () => { await (server as any).executeTool('init', {}); }); test('should start playback', async () => { const result = await (server as any).executeTool('play', {}); expect(mockController.play).toHaveBeenCalled(); expect(result).toBe('Playing'); }); test('should stop playback with stop', async () => { const result = await (server as any).executeTool('stop', {}); expect(mockController.stop).toHaveBeenCalled(); expect(result).toBe('Stopped'); }); test('should stop playback with pause', async () => { const result = await (server as any).executeTool('pause', {}); expect(mockController.stop).toHaveBeenCalled(); expect(result).toBe('Stopped'); }); }); describe('clear', () => { beforeEach(async () => { await (server as any).executeTool('init', {}); }); test('should clear editor', async () => { await (server as any).executeTool('clear', {}); expect(mockController.writePattern).toHaveBeenCalledWith(''); }); }); describe('get_pattern', () => { beforeEach(async () => { await (server as any).executeTool('init', {}); }); test('should get current pattern', async () => { mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); const result = await (server as any).executeTool('get_pattern', {}); expect(result).toBe('s("bd*4")'); }); }); }); describe('Pattern Generation Tools', () => { describe('generate_pattern', () => { test('should generate pattern without initialization', async () => { const result = await (server as any).executeTool('generate_pattern', { style: 'techno' }); expect(mockGenerator.generateCompletePattern).toHaveBeenCalledWith('techno', 'C', 120); expect(result).toContain('Generated techno pattern'); }); test('should validate style', async () => { await expect( (server as any).executeTool('generate_pattern', { style: '' }) ).rejects.toThrow('cannot be empty'); }); test('should validate BPM when provided', async () => { await expect( (server as any).executeTool('generate_pattern', { style: 'techno', bpm: 500 }) ).rejects.toThrow('BPM'); }); test('should validate key when provided', async () => { await expect( (server as any).executeTool('generate_pattern', { style: 'techno', key: 'X' }) ).rejects.toThrow('Invalid root note'); }); test('should use default values', async () => { await (server as any).executeTool('generate_pattern', { style: 'house' }); expect(mockGenerator.generateCompletePattern).toHaveBeenCalledWith('house', 'C', 120); }); }); describe('generate_drums', () => { test('should generate drums', async () => { await (server as any).executeTool('generate_drums', { style: 'techno' }); expect(mockGenerator.generateDrumPattern).toHaveBeenCalledWith('techno', 0.5); }); test('should validate complexity range', async () => { await expect( (server as any).executeTool('generate_drums', { style: 'techno', complexity: 1.5 }) ).rejects.toThrow('between 0 and 1'); }); test('should use provided complexity', async () => { await (server as any).executeTool('generate_drums', { style: 'techno', complexity: 0.8 }); expect(mockGenerator.generateDrumPattern).toHaveBeenCalledWith('techno', 0.8); }); }); describe('generate_bassline', () => { test('should generate bassline', async () => { const result = await (server as any).executeTool('generate_bassline', { key: 'C', style: 'acid' }); expect(mockGenerator.generateBassline).toHaveBeenCalledWith('C', 'acid'); expect(result).toContain('Generated acid bassline in C'); }); test('should validate key', async () => { await expect( (server as any).executeTool('generate_bassline', { key: 'X', style: 'acid' }) ).rejects.toThrow('Invalid root note'); }); }); describe('generate_melody', () => { test('should generate melody', async () => { await (server as any).executeTool('generate_melody', { root: 'C', scale: 'major', length: 16 }); expect(mockTheory.generateScale).toHaveBeenCalledWith('C', 'major'); expect(mockGenerator.generateMelody).toHaveBeenCalledWith( ['C', 'D', 'E', 'F', 'G', 'A', 'B'], 16 ); }); test('should use default length', async () => { await (server as any).executeTool('generate_melody', { root: 'D', scale: 'minor' }); expect(mockGenerator.generateMelody).toHaveBeenCalledWith(expect.any(Array), 8); }); test('should validate length is positive', async () => { await expect( (server as any).executeTool('generate_melody', { root: 'C', scale: 'major', length: 0 }) ).rejects.toThrow('positive'); }); }); describe('generate_scale', () => { test('should return scale notes', async () => { const result = await (server as any).executeTool('generate_scale', { root: 'C', scale: 'major' }); expect(result).toContain('C major scale'); expect(result).toContain('C, D, E, F, G, A, B'); }); test('should validate scale name', async () => { await expect( (server as any).executeTool('generate_scale', { root: 'C', scale: 'invalid' }) ).rejects.toThrow('Invalid scale name'); }); }); describe('generate_chord_progression', () => { test('should generate chord progression', async () => { const result = await (server as any).executeTool('generate_chord_progression', { key: 'C', style: 'pop' }); expect(mockTheory.generateChordProgression).toHaveBeenCalledWith('C', 'pop'); expect(result).toContain('Generated pop progression in C'); }); test('should validate chord style', async () => { await expect( (server as any).executeTool('generate_chord_progression', { key: 'C', style: 'invalid' }) ).rejects.toThrow('Invalid chord style'); }); }); describe('generate_euclidean', () => { test('should generate euclidean pattern', async () => { const result = await (server as any).executeTool('generate_euclidean', { hits: 3, steps: 8, sound: 'bd' }); expect(mockGenerator.generateEuclideanPattern).toHaveBeenCalledWith(3, 8, 'bd'); expect(result).toContain('Euclidean rhythm (3/8)'); }); test('should use default sound', async () => { await (server as any).executeTool('generate_euclidean', { hits: 5, steps: 16 }); expect(mockGenerator.generateEuclideanPattern).toHaveBeenCalledWith(5, 16, 'bd'); }); test('should validate hits <= steps', async () => { await expect( (server as any).executeTool('generate_euclidean', { hits: 10, steps: 8 }) ).rejects.toThrow('cannot exceed steps'); }); }); describe('generate_polyrhythm', () => { test('should generate polyrhythm', async () => { await (server as any).executeTool('generate_polyrhythm', { sounds: ['bd', 'cp', 'hh'], patterns: [3, 5, 7] }); expect(mockGenerator.generatePolyrhythm).toHaveBeenCalledWith( ['bd', 'cp', 'hh'], [3, 5, 7] ); }); test('should validate pattern numbers are positive', async () => { await expect( (server as any).executeTool('generate_polyrhythm', { sounds: ['bd'], patterns: [0] }) ).rejects.toThrow('positive'); }); }); describe('generate_fill', () => { test('should generate fill', async () => { const result = await (server as any).executeTool('generate_fill', { style: 'techno', bars: 2 }); expect(mockGenerator.generateFill).toHaveBeenCalledWith('techno', 2); expect(result).toContain('2 bar fill'); }); test('should use default bars', async () => { await (server as any).executeTool('generate_fill', { style: 'house' }); expect(mockGenerator.generateFill).toHaveBeenCalledWith('house', 1); }); }); }); describe('Pattern Manipulation Tools', () => { beforeEach(async () => { await (server as any).executeTool('init', {}); }); describe('transpose', () => { test('should transpose pattern', async () => { mockController.getCurrentPattern.mockResolvedValue('note("c4 d4 e4")'); const result = await (server as any).executeTool('transpose', { semitones: 5 }); expect(result).toContain('Transposed 5 semitones'); }); test('should accept negative semitones', async () => { const result = await (server as any).executeTool('transpose', { semitones: -3 }); expect(result).toContain('Transposed -3 semitones'); }); test('should validate semitones is integer', async () => { await expect( (server as any).executeTool('transpose', { semitones: 2.5 }) ).rejects.toThrow('integer'); }); }); describe('reverse', () => { test('should add reverse modifier', async () => { mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); await (server as any).executeTool('reverse', {}); expect(mockController.writePattern).toHaveBeenCalledWith('s("bd*4").rev'); }); }); describe('stretch', () => { test('should add slow modifier', async () => { mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); await (server as any).executeTool('stretch', { factor: 2 }); expect(mockController.writePattern).toHaveBeenCalledWith('s("bd*4").slow(2)'); }); test('should validate factor is positive', async () => { await expect( (server as any).executeTool('stretch', { factor: -1 }) ).rejects.toThrow(); }); }); describe('humanize', () => { test('should add humanize effect', async () => { mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); const result = await (server as any).executeTool('humanize', { amount: 0.05 }); expect(mockController.writePattern).toHaveBeenCalled(); expect(result).toBe('Added human timing'); }); test('should use default amount', async () => { const result = await (server as any).executeTool('humanize', {}); expect(result).toBe('Added human timing'); }); }); describe('generate_variation', () => { test('should generate variation', async () => { mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); const result = await (server as any).executeTool('generate_variation', { type: 'extreme' }); expect(mockGenerator.generateVariation).toHaveBeenCalledWith('s("bd*4")', 'extreme'); expect(result).toContain('extreme variation'); }); test('should use default type', async () => { await (server as any).executeTool('generate_variation', {}); expect(mockGenerator.generateVariation).toHaveBeenCalledWith(expect.any(String), 'subtle'); }); }); }); describe('Effects and Tempo Tools', () => { beforeEach(async () => { await (server as any).executeTool('init', {}); }); describe('add_effect', () => { test('should add effect with params', async () => { mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); const result = await (server as any).executeTool('add_effect', { effect: 'reverb', params: '0.5' }); expect(mockController.writePattern).toHaveBeenCalledWith('s("bd*4").reverb(0.5)'); expect(result).toContain('Added reverb effect'); }); test('should add effect without params', async () => { mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); await (server as any).executeTool('add_effect', { effect: 'distortion' }); expect(mockController.writePattern).toHaveBeenCalledWith('s("bd*4").distortion()'); }); }); describe('add_swing', () => { test('should add swing', async () => { mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); const result = await (server as any).executeTool('add_swing', { amount: 0.6 }); expect(mockController.writePattern).toHaveBeenCalledWith('s("bd*4").swing(0.6)'); expect(result).toContain('Added swing: 0.6'); }); test('should validate amount range', async () => { await expect( (server as any).executeTool('add_swing', { amount: 1.5 }) ).rejects.toThrow('between 0 and 1'); }); }); describe('set_tempo', () => { test('should set tempo', async () => { mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); const result = await (server as any).executeTool('set_tempo', { bpm: 140 }); expect(mockController.writePattern).toHaveBeenCalledWith('setcpm(140)\ns("bd*4")'); expect(result).toContain('Set tempo to 140 BPM'); }); test('should validate BPM range', async () => { await expect( (server as any).executeTool('set_tempo', { bpm: 500 }) ).rejects.toThrow('BPM must be between'); }); }); }); describe('Audio Analysis Tools', () => { beforeEach(async () => { await (server as any).executeTool('init', {}); }); describe('analyze', () => { test('should analyze audio', async () => { const result = await (server as any).executeTool('analyze', {}); expect(mockController.analyzeAudio).toHaveBeenCalled(); expect(result.connected).toBe(true); }); test('should require initialization', async () => { const uninitServer = new EnhancedMCPServerFixed(); const result = await (uninitServer as any).executeTool('analyze', {}); expect(result).toContain('not initialized'); }); }); describe('analyze_spectrum', () => { test('should return spectrum features', async () => { const result = await (server as any).executeTool('analyze_spectrum', {}); expect(result).toHaveProperty('bass'); expect(result).toHaveProperty('mid'); expect(result).toHaveProperty('treble'); }); }); describe('analyze_rhythm', () => { test('should return rhythm analysis', async () => { const result = await (server as any).executeTool('analyze_rhythm', {}); expect(result).toHaveProperty('isPlaying'); expect(result).toHaveProperty('tempo'); }); }); describe('detect_tempo', () => { test('should detect tempo successfully', async () => { const result = await (server as any).executeTool('detect_tempo', {}); expect(result.bpm).toBe(120); expect(result.confidence).toBe(0.85); expect(result.method).toBe('autocorrelation'); expect(result.message).toContain('120 BPM'); }); test('should handle no tempo detected', async () => { mockController.detectTempo.mockResolvedValue({ bpm: 0, confidence: 0 }); const result = await (server as any).executeTool('detect_tempo', {}); expect(result.bpm).toBe(0); expect(result.message).toContain('No tempo detected'); }); test('should handle null tempo result', async () => { mockController.detectTempo.mockResolvedValue(null); const result = await (server as any).executeTool('detect_tempo', {}); expect(result.bpm).toBe(0); expect(result.message).toContain('No tempo detected'); }); test('should handle detection error', async () => { mockController.detectTempo.mockRejectedValue(new Error('Detection failed')); const result = await (server as any).executeTool('detect_tempo', {}); expect(result.bpm).toBe(0); expect(result.error).toContain('Detection failed'); }); test('should require initialization', async () => { const uninitServer = new EnhancedMCPServerFixed(); const result = await (uninitServer as any).executeTool('detect_tempo', {}); expect(result).toContain('not initialized'); }); }); describe('detect_key', () => { test('should detect key successfully', async () => { const result = await (server as any).executeTool('detect_key', {}); expect(result.key).toBe('C'); expect(result.scale).toBe('major'); expect(result.confidence).toBe(0.85); }); test('should handle no key detected', async () => { mockController.detectKey.mockResolvedValue({ key: 'Unknown', scale: 'unknown', confidence: 0.05 }); const result = await (server as any).executeTool('detect_key', {}); expect(result.key).toBe('Unknown'); expect(result.message).toContain('No clear key detected'); }); test('should handle detection error', async () => { mockController.detectKey.mockRejectedValue(new Error('Key detection failed')); const result = await (server as any).executeTool('detect_key', {}); expect(result.key).toBe('Unknown'); expect(result.error).toContain('Key detection failed'); }); test('should include alternatives when available', async () => { mockController.detectKey.mockResolvedValue({ key: 'C', scale: 'major', confidence: 0.75, alternatives: [{ key: 'A', scale: 'minor', confidence: 0.65 }] }); const result = await (server as any).executeTool('detect_key', {}); expect(result.alternatives).toBeDefined(); expect(result.alternatives[0].key).toBe('A'); }); test('should require initialization', async () => { const uninitServer = new EnhancedMCPServerFixed(); const result = await (uninitServer as any).executeTool('detect_key', {}); expect(result).toContain('not initialized'); }); }); }); describe('Session Management Tools', () => { beforeEach(async () => { await (server as any).executeTool('init', {}); }); describe('save', () => { test('should save pattern', async () => { mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); const result = await (server as any).executeTool('save', { name: 'my-pattern', tags: ['techno', 'drums'] }); expect(mockStore.save).toHaveBeenCalledWith( 'my-pattern', 's("bd*4")', ['techno', 'drums'] ); expect(result).toContain('Pattern saved as "my-pattern"'); }); test('should handle empty pattern', async () => { mockController.getCurrentPattern.mockResolvedValue(''); const result = await (server as any).executeTool('save', { name: 'test' }); expect(result).toBe('No pattern to save'); }); test('should validate name length', async () => { const longName = 'a'.repeat(256); await expect( (server as any).executeTool('save', { name: longName }) ).rejects.toThrow(); }); }); describe('load', () => { test('should load pattern', async () => { const result = await (server as any).executeTool('load', { name: 'test-pattern' }); expect(mockStore.load).toHaveBeenCalledWith('test-pattern'); expect(mockController.writePattern).toHaveBeenCalledWith('s("bd cp")'); expect(result).toContain('Loaded pattern "test-pattern"'); }); test('should handle pattern not found', async () => { mockStore.load.mockResolvedValue(null); const result = await (server as any).executeTool('load', { name: 'nonexistent' }); expect(result).toContain('not found'); }); }); describe('list', () => { test('should list patterns', async () => { const result = await (server as any).executeTool('list', {}); expect(mockStore.list).toHaveBeenCalledWith(undefined); expect(result).toContain('pattern1'); }); test('should filter by tag', async () => { await (server as any).executeTool('list', { tag: 'techno' }); expect(mockStore.list).toHaveBeenCalledWith('techno'); }); test('should handle empty list', async () => { mockStore.list.mockResolvedValue([]); const result = await (server as any).executeTool('list', {}); expect(result).toContain('No patterns found'); }); }); describe('undo', () => { test('should undo changes', async () => { // First make a change await (server as any).executeTool('write', { pattern: 's("bd*4")' }); mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); const result = await (server as any).executeTool('undo', {}); expect(result).toBe('Undone'); }); test('should handle empty undo stack', async () => { const result = await (server as any).executeTool('undo', {}); expect(result).toBe('Nothing to undo'); }); test('should require initialization', async () => { const uninitServer = new EnhancedMCPServerFixed(); const result = await (uninitServer as any).executeTool('undo', {}); expect(result).toContain('not initialized'); }); }); describe('redo', () => { test('should redo changes', async () => { // Setup: write, undo, then redo await (server as any).executeTool('write', { pattern: 's("bd*4")' }); await (server as any).executeTool('undo', {}); const result = await (server as any).executeTool('redo', {}); expect(result).toBe('Redone'); }); test('should handle empty redo stack', async () => { const result = await (server as any).executeTool('redo', {}); expect(result).toBe('Nothing to redo'); }); }); }); describe('Performance Monitoring Tools', () => { describe('performance_report', () => { test('should return performance report', async () => { const result = await (server as any).executeTool('performance_report', {}); expect(result).toContain('Bottlenecks'); }); }); describe('memory_usage', () => { test('should return memory usage', async () => { const result = await (server as any).executeTool('memory_usage', {}); // Result depends on PerformanceMonitor implementation expect(result).toBeDefined(); }); }); }); describe('Pattern Validation', () => { beforeEach(async () => { await (server as any).executeTool('init', {}); }); test('should validate valid pattern', async () => { const result = await (server as any).executeTool('validate_pattern_runtime', { pattern: 's("bd*4")', waitMs: 500 }); expect(result).toContain('Pattern valid'); expect(mockController.validatePatternRuntime).toHaveBeenCalledWith('s("bd*4")', 500); }); test('should detect invalid pattern', async () => { mockController.validatePatternRuntime.mockResolvedValue({ valid: false, errors: ['Syntax error'], warnings: ['Missing semicolon'] }); const result = await (server as any).executeTool('validate_pattern_runtime', { pattern: 's("bd*4)' }); expect(result).toContain('runtime errors'); expect(result).toContain('Syntax error'); }); test('should use default waitMs', async () => { await (server as any).executeTool('validate_pattern_runtime', { pattern: 's("bd*4")' }); expect(mockController.validatePatternRuntime).toHaveBeenCalledWith('s("bd*4")', 500); }); test('should require initialization', async () => { const uninitServer = new EnhancedMCPServerFixed(); const result = await (uninitServer as any).executeTool('validate_pattern_runtime', { pattern: 's("bd*4")' }); expect(result).toContain('not initialized'); }); }); describe('Error Handling', () => { test('should handle unknown tool', async () => { await expect( (server as any).executeTool('unknown_tool', {}) ).rejects.toThrow('Unknown tool'); }); test('should require initialization for browser tools', async () => { const uninitServer = new EnhancedMCPServerFixed(); const result = await (uninitServer as any).executeTool('play', {}); expect(result).toContain('not initialized'); }); test('should handle controller errors gracefully', async () => { await (server as any).executeTool('init', {}); mockController.play.mockRejectedValue(new Error('Playback failed')); await expect( (server as any).executeTool('play', {}) ).rejects.toThrow('Playback failed'); }); }); describe('State Management', () => { beforeEach(async () => { await (server as any).executeTool('init', {}); }); test('should maintain undo/redo stack', async () => { // Write pattern 1 await (server as any).executeTool('write', { pattern: 's("bd*4")' }); // Write pattern 2 mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); await (server as any).executeTool('write', { pattern: 's("cp*2")' }); // Undo should restore pattern 1 const undoResult = await (server as any).executeTool('undo', {}); expect(undoResult).toBe('Undone'); // Redo should restore pattern 2 const redoResult = await (server as any).executeTool('redo', {}); expect(redoResult).toBe('Redone'); }); test('should clear redo stack on new write', async () => { await (server as any).executeTool('write', { pattern: 's("bd*4")' }); await (server as any).executeTool('undo', {}); // New write should clear redo stack await (server as any).executeTool('write', { pattern: 's("cp*2")' }); const redoResult = await (server as any).executeTool('redo', {}); expect(redoResult).toBe('Nothing to redo'); }); }); describe('Tool Integration', () => { test('should chain operations correctly', async () => { await (server as any).executeTool('init', {}); // Generate pattern await (server as any).executeTool('generate_pattern', { style: 'techno' }); // Add effect mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); await (server as any).executeTool('add_effect', { effect: 'reverb', params: '0.5' }); // Save const saveResult = await (server as any).executeTool('save', { name: 'techno-reverb', tags: ['techno'] }); expect(saveResult).toContain('Pattern saved'); }); }); // UX Tools Tests - Issue #37, #38, #39, #40, #42 describe('UX Tools', () => { describe('Browser Control (#37)', () => { test('show_browser should bring window to foreground', async () => { await (server as any).executeTool('init', {}); const result = await (server as any).executeTool('show_browser', {}); expect(result).toContain('foreground'); expect(mockController.showBrowser).toHaveBeenCalled(); }); test('show_browser should require initialization', async () => { const result = await (server as any).executeTool('show_browser', {}); expect(result).toContain('not initialized'); }); test('screenshot should save browser state', async () => { await (server as any).executeTool('init', {}); const result = await (server as any).executeTool('screenshot', { filename: 'test.png' }); expect(result).toContain('Screenshot'); expect(mockController.takeScreenshot).toHaveBeenCalledWith('test.png'); }); test('screenshot should require initialization', async () => { const result = await (server as any).executeTool('screenshot', {}); expect(result).toContain('not initialized'); }); }); describe('Status & Diagnostics (#39)', () => { test('status should return current state', async () => { const result = await (server as any).executeTool('status', {}); expect(result).toHaveProperty('initialized'); expect(result).toHaveProperty('playing'); expect(result).toHaveProperty('errorCount'); }); test('diagnostics should return detailed info when initialized', async () => { await (server as any).executeTool('init', {}); const result = await (server as any).executeTool('diagnostics', {}); expect(result).toHaveProperty('browserConnected'); expect(result).toHaveProperty('editorReady'); expect(result).toHaveProperty('cacheStatus'); }); test('diagnostics should indicate not initialized', async () => { const result = await (server as any).executeTool('diagnostics', {}); expect(result.initialized).toBe(false); expect(result.message).toContain('not initialized'); }); test('show_errors should display captured errors', async () => { mockController.getConsoleErrors.mockReturnValue(['Error 1', 'Error 2']); mockController.getConsoleWarnings.mockReturnValue(['Warning 1']); const result = await (server as any).executeTool('show_errors', {}); expect(result).toContain('Errors'); expect(result).toContain('Error 1'); expect(result).toContain('Warning 1'); }); test('show_errors should indicate no errors when empty', async () => { mockController.getConsoleErrors.mockReturnValue([]); mockController.getConsoleWarnings.mockReturnValue([]); const result = await (server as any).executeTool('show_errors', {}); expect(result).toContain('No errors'); }); }); describe('Auto-play (#38)', () => { test('write with auto_play should start playback', async () => { await (server as any).executeTool('init', {}); const result = await (server as any).executeTool('write', { pattern: 's("bd*4")', auto_play: true }); expect(result).toContain('Playing'); expect(mockController.play).toHaveBeenCalled(); }); test('write without auto_play should not start playback', async () => { await (server as any).executeTool('init', {}); mockController.play.mockClear(); const result = await (server as any).executeTool('write', { pattern: 's("bd*4")', auto_play: false }); expect(result).not.toContain('Playing'); expect(mockController.play).not.toHaveBeenCalled(); }); test('generate_pattern with auto_play should start playback', async () => { await (server as any).executeTool('init', {}); mockController.play.mockClear(); const result = await (server as any).executeTool('generate_pattern', { style: 'techno', auto_play: true }); expect(result).toContain('Playing'); expect(mockController.play).toHaveBeenCalled(); }); }); describe('Pattern Validation (#40)', () => { test('write with validate=false should skip validation', async () => { await (server as any).executeTool('init', {}); mockController.validatePattern.mockClear(); await (server as any).executeTool('write', { pattern: 's("bd*4")', validate: false }); expect(mockController.validatePattern).not.toHaveBeenCalled(); }); test('write should validate by default', async () => { await (server as any).executeTool('init', {}); mockController.validatePattern.mockClear(); await (server as any).executeTool('write', { pattern: 's("bd*4")' }); expect(mockController.validatePattern).toHaveBeenCalled(); }); test('write should return validation errors', async () => { await (server as any).executeTool('init', {}); mockController.validatePattern.mockResolvedValue({ valid: false, errors: ['Syntax error'], warnings: [], suggestions: ['Check syntax'] }); const result = await (server as any).executeTool('write', { pattern: 'invalid{' }); expect(result.success).toBe(false); expect(result.errors).toContain('Syntax error'); }); }); describe('Compose Workflow (#42)', () => { test('compose should auto-initialize and generate pattern', async () => { const result = await (server as any).executeTool('compose', { style: 'techno' }); expect(result.success).toBe(true); expect(result.metadata.style).toBe('techno'); expect(result.status).toBe('playing'); expect(mockController.initialize).toHaveBeenCalled(); expect(mockController.writePattern).toHaveBeenCalled(); expect(mockController.play).toHaveBeenCalled(); }); test('compose should respect auto_play=false', async () => { mockController.play.mockClear(); const result = await (server as any).executeTool('compose', { style: 'ambient', auto_play: false }); expect(result.success).toBe(true); expect(result.status).toBe('ready'); expect(mockController.play).not.toHaveBeenCalled(); }); test('compose should use custom tempo and key', async () => { const result = await (server as any).executeTool('compose', { style: 'house', tempo: 128, key: 'A' }); expect(result.success).toBe(true); expect(result.metadata.bpm).toBe(128); expect(result.metadata.key).toBe('A'); }); test('compose should use genre-appropriate default tempo', async () => { const result = await (server as any).executeTool('compose', { style: 'dnb' }); expect(result.success).toBe(true); expect(result.metadata.bpm).toBe(174); // DnB default }); }); describe('Pattern History (#41)', () => { test('list_history should return empty message when no history', async () => { const newServer = new EnhancedMCPServerFixed(); const result = await (newServer as any).executeTool('list_history', {}); expect(result).toContain('No pattern history yet'); }); test('list_history should show pattern history after edits', async () => { await (server as any).executeTool('init', {}); mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); // Make some edits to build history await (server as any).executeTool('write', { pattern: 's("bd*4")' }); await (server as any).executeTool('write', { pattern: 's("hh*8")' }); const result = await (server as any).executeTool('list_history', {}); expect(result.count).toBeGreaterThan(0); expect(result.entries).toBeDefined(); expect(result.entries.length).toBeGreaterThan(0); expect(result.entries[0].id).toBeDefined(); expect(result.entries[0].preview).toBeDefined(); expect(result.entries[0].timestamp).toBeDefined(); }); test('list_history should respect limit parameter', async () => { await (server as any).executeTool('init', {}); mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); // Make several edits for (let i = 0; i < 5; i++) { await (server as any).executeTool('write', { pattern: `s("bd*${i}")` }); } const result = await (server as any).executeTool('list_history', { limit: 2 }); expect(result.entries.length).toBe(2); }); test('restore_history should restore pattern by ID', async () => { await (server as any).executeTool('init', {}); mockController.getCurrentPattern.mockResolvedValue('s("bd*4")'); // Make an edit to create history await (server as any).executeTool('write', { pattern: 's("cp*2")' }); // Get the history const history = await (server as any).executeTool('list_history', {}); const firstEntry = history.entries[history.entries.length - 1]; // Restore from history const result = await (server as any).executeTool('restore_history', { id: firstEntry.id }); expect(result).toContain('Restored pattern from history'); expect(mockController.writePattern).toHaveBeenCalled(); }); test('restore_history should return error for invalid ID', async () => { await (server as any).executeTool('init', {}); const result = await (server as any).executeTool('restore_history', { id: 99999 }); expect(result).toContain('not found'); }); test('restore_history should require initialization', async () => { const uninitServer = new EnhancedMCPServerFixed(); const result = await (uninitServer as any).executeTool('restore_history', { id: 1 }); expect(result).toContain('not initialized'); }); test('compare_patterns should show diff between two patterns', async () => { await (server as any).executeTool('init', {}); mockController.getCurrentPattern .mockResolvedValueOnce('s("bd*4")') .mockResolvedValueOnce('s("hh*8")'); // Make edits to create history await (server as any).executeTool('write', { pattern: 's("bd*4")' }); await (server as any).executeTool('write', { pattern: 's("hh*8")' }); // Get history IDs const history = await (server as any).executeTool('list_history', {}); const id1 = history.entries[history.entries.length - 1].id; // Compare with current const result = await (server as any).executeTool('compare_patterns', { id1: id1 }); expect(result.diff).toBeDefined(); expect(result.summary).toBeDefined(); expect(result.summary.charsDiff).toBeDefined(); }); test('compare_patterns should return error for invalid ID', async () => { await (server as any).executeTool('init', {}); const result = await (server as any).executeTool('compare_patterns', { id1: 99999 }); expect(result).toContain('not found'); }); }); }); });

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/williamzujkowski/strudel-mcp-server'

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