Skip to main content
Glama

marm-mcp

chatbot.test.jsβ€’14.7 kB
import { handleUserInput } from '../src/chatbot/core.js'; import { voiceConfig, loadVoices, speakText, initializeVoice, cleanupVoice, voiceTimers, voiceListeners } from '../src/chatbot/voice.js'; describe('Chatbot Core', () => { test('handleUserInput returns undefined for empty input', async () => { const result = await handleUserInput(''); expect(result).toBeUndefined(); }); test('handleUserInput returns undefined for non-string input', async () => { const result = await handleUserInput(null); expect(result).toBeUndefined(); }); test('handleUserInput enforces input length limit', async () => { const longInput = 'a'.repeat(15001); // Updated to exceed new 15k limit const result = await handleUserInput(longInput); expect(result).toBeUndefined(); }); }); describe('Voice Module', () => { beforeEach(() => { // Mock speechSynthesis for each test global.speechSynthesis = { getVoices: jest.fn(() => [ { name: 'Google UK English Female', lang: 'en-GB', default: false }, { name: 'Microsoft Zira Desktop', lang: 'en-US', default: false }, { name: 'TestVoice', lang: 'en-US', default: true } ]), speak: jest.fn(), cancel: jest.fn(), speaking: false, pending: false, onvoiceschanged: null }; // Reset voice config voiceConfig.enabled = false; voiceConfig.voice = null; voiceConfig.rate = 1.10; voiceConfig.pitch = 1.0; }); afterEach(() => { cleanupVoice(); }); test('voiceConfig has correct default values', () => { expect(voiceConfig).toHaveProperty('enabled', false); expect(voiceConfig).toHaveProperty('rate', 1.10); expect(voiceConfig).toHaveProperty('pitch', 1.0); expect(voiceConfig).toHaveProperty('voice', null); }); test('loadVoices loads available voices and selects preferred one', () => { loadVoices(); expect(global.speechSynthesis.getVoices).toHaveBeenCalled(); expect(voiceConfig.voice).toBe('Google UK English Female'); }); test('loadVoices handles speech synthesis errors gracefully', () => { global.speechSynthesis.getVoices = jest.fn(() => { throw new Error('Speech synthesis error'); }); expect(() => loadVoices()).not.toThrow(); expect(voiceConfig.voice).toBeNull(); }); test('speakText calls speechSynthesis.speak for bot messages', () => { loadVoices(); speakText('Hello world', true); expect(global.speechSynthesis.speak).toHaveBeenCalled(); }); test('speakText does not speak for non-bot messages', () => { speakText('Hello world', false); expect(global.speechSynthesis.speak).not.toHaveBeenCalled(); }); test('speakText cleans markdown formatting from text', () => { loadVoices(); let actualUtterance; global.speechSynthesis.speak = jest.fn((utterance) => { actualUtterance = utterance; }); speakText('**Bold text** and *italic text* with `code`', true); expect(actualUtterance.text).not.toContain('**'); expect(actualUtterance.text).not.toContain('*'); expect(actualUtterance.text).not.toContain('`'); expect(actualUtterance.text).toContain('Bold text'); expect(actualUtterance.text).toContain('italic text'); }); test('speakText truncates long text for voice playback', () => { loadVoices(); let actualUtterance; global.speechSynthesis.speak = jest.fn((utterance) => { actualUtterance = utterance; }); const longText = 'a'.repeat(12000); // 12k chars - exceeds 10k voice limit speakText(longText, true); expect(actualUtterance.text.length).toBeLessThan(12000); expect(actualUtterance.text).toContain('Response truncated for voice playback'); }); test('speakText configures utterance with correct voice settings', () => { loadVoices(); let actualUtterance; global.speechSynthesis.speak = jest.fn((utterance) => { actualUtterance = utterance; }); speakText('Test message', true); // Rate is adjusted for Google voices (0.95x multiplier), so test the actual rate expect(actualUtterance.rate).toBeCloseTo(1.045, 3); // 1.1 * 0.95 = 1.045 expect(actualUtterance.pitch).toBe(voiceConfig.pitch); expect(actualUtterance.volume).toBe(0.9); }); test('initializeVoice sets up voice system correctly', () => { // Temporarily disable the self-test in voice.js by mocking the environment const originalNodeEnv = process.env?.NODE_ENV; if (process.env) { process.env.NODE_ENV = 'development'; } initializeVoice(); expect(global.speechSynthesis.onvoiceschanged).toBe(loadVoices); expect(voiceConfig.voice).toBe('Google UK English Female'); // Restore original environment if (process.env && originalNodeEnv !== undefined) { process.env.NODE_ENV = originalNodeEnv; } }); test('initializeVoice handles missing speechSynthesis gracefully', () => { const originalSpeechSynthesis = global.speechSynthesis; global.speechSynthesis = undefined; expect(() => initializeVoice()).not.toThrow(); expect(voiceConfig.enabled).toBe(false); global.speechSynthesis = originalSpeechSynthesis; }); test('voiceTimers and voiceListeners are properly initialized', () => { expect(voiceTimers).toBeInstanceOf(Set); expect(voiceListeners).toBeInstanceOf(Map); }); test('cleanupVoice properly cleans up resources', () => { // Add some mock timers and listeners const mockTimer = setTimeout(() => {}, 1000); voiceTimers.add(mockTimer); const mockElement = document.createElement('div'); voiceListeners.set(mockElement, []); cleanupVoice(); expect(global.speechSynthesis.cancel).toHaveBeenCalled(); expect(voiceTimers.size).toBe(0); expect(voiceListeners.size).toBe(0); }); }); describe('Commands Module', () => { let mockGenerateContent; let mockGetState; let mockUpdateState; let mockActivateMarmSession; let mockLogSession; let mockManageUserNotebook; let mockGetSessionContext; let mockUpdateSessionHistory; let mockGetMostRecentBotResponseLogic; let mockSetSessionReasoning; let mockTrimForContext; let mockCompileSessionSummary; beforeEach(() => { // Reset DOM document.body.innerHTML = '<div id="chat-log"></div>'; // Mock all external dependencies mockGenerateContent = jest.fn(); mockGetState = jest.fn(); mockUpdateState = jest.fn(); mockActivateMarmSession = jest.fn(); mockLogSession = jest.fn(); mockManageUserNotebook = jest.fn(); mockGetSessionContext = jest.fn(); mockUpdateSessionHistory = jest.fn(); mockGetMostRecentBotResponseLogic = jest.fn(); mockSetSessionReasoning = jest.fn(); mockTrimForContext = jest.fn(); mockCompileSessionSummary = jest.fn(); // Mock module imports jest.doMock('../src/replicateHelper.js', () => ({ generateContent: mockGenerateContent })); jest.doMock('../src/chatbot/state.js', () => ({ getState: mockGetState, updateState: mockUpdateState })); jest.doMock('../src/logic/marmLogic.js', () => ({ activateMarmSession: mockActivateMarmSession, logSession: mockLogSession, manageUserNotebook: mockManageUserNotebook, getSessionContext: mockGetSessionContext, updateSessionHistory: mockUpdateSessionHistory, getMostRecentBotResponseLogic: mockGetMostRecentBotResponseLogic, setSessionReasoning: mockSetSessionReasoning, trimForContext: mockTrimForContext, compileSessionSummary: mockCompileSessionSummary })); // Setup default state mockGetState.mockReturnValue({ isMarmActive: false, currentSessionId: null }); // Setup default generateContent response mockGenerateContent.mockResolvedValue({ text: jest.fn().mockResolvedValue('Mock bot response') }); mockGetSessionContext.mockReturnValue('Mock session context'); }); afterEach(() => { jest.clearAllMocks(); jest.resetModules(); }); test('handleCommand parses commands correctly', async () => { const { handleCommand } = await import('../src/chatbot/commands.js'); // Test unknown command await handleCommand('/unknown'); // Should show error message (we can't easily test the exact message without more complex mocking) }); test('handleCommand handles /start marm command', async () => { const { handleCommand } = await import('../src/chatbot/commands.js'); mockUpdateState.mockReturnValue({ isMarmActive: true, currentSessionId: 'test_session' }); await handleCommand('/start marm'); expect(mockUpdateState).toHaveBeenCalledWith({ isMarmActive: true, currentSessionId: expect.any(String) }); expect(mockActivateMarmSession).toHaveBeenCalled(); expect(mockGenerateContent).toHaveBeenCalled(); }); test('handleCommand handles /refresh marm command when MARM is active', async () => { const { handleCommand } = await import('../src/chatbot/commands.js'); mockGetState.mockReturnValue({ isMarmActive: true, currentSessionId: 'test_session' }); await handleCommand('/refresh marm'); expect(mockTrimForContext).toHaveBeenCalledWith('test_session'); expect(mockGenerateContent).toHaveBeenCalled(); }); test('handleCommand handles /log entry command', async () => { const { handleCommand } = await import('../src/chatbot/commands.js'); mockGetState.mockReturnValue({ isMarmActive: true, currentSessionId: 'test_session' }); await handleCommand('/log entry: 2025-08-18 - test log - summary'); expect(mockLogSession).toHaveBeenCalledWith('test_session', '2025-08-18 - test log - summary'); expect(mockGenerateContent).toHaveBeenCalled(); }); test('handleCommand handles /log session command', async () => { const { handleCommand } = await import('../src/chatbot/commands.js'); mockGetState.mockReturnValue({ isMarmActive: true, currentSessionId: 'test_session' }); mockUpdateState.mockReturnValue({ isMarmActive: true, currentSessionId: 'new_session' }); await handleCommand('/log session: new_session'); expect(mockUpdateState).toHaveBeenCalledWith({ currentSessionId: 'new_session' }); expect(mockGenerateContent).toHaveBeenCalled(); }); test('handleCommand handles /deep dive command', async () => { const { handleCommand } = await import('../src/chatbot/commands.js'); mockGetState.mockReturnValue({ isMarmActive: true, currentSessionId: 'test_session' }); // Mock response with reasoning mockGenerateContent.mockResolvedValue({ text: jest.fn().mockResolvedValue('Bot response\n\nREASONING: This is the reasoning') }); await handleCommand('/deep dive: test topic'); expect(mockGenerateContent).toHaveBeenCalled(); expect(mockSetSessionReasoning).toHaveBeenCalledWith('test_session', 'This is the reasoning'); }); test('handleCommand handles /show reasoning command', async () => { const { handleCommand } = await import('../src/chatbot/commands.js'); mockGetState.mockReturnValue({ isMarmActive: true, currentSessionId: 'test_session' }); mockGetMostRecentBotResponseLogic.mockReturnValue('Test reasoning'); await handleCommand('/show reasoning'); expect(mockGetMostRecentBotResponseLogic).toHaveBeenCalledWith('test_session'); }); test('handleCommand handles /summary command', async () => { const { handleCommand } = await import('../src/chatbot/commands.js'); mockGetState.mockReturnValue({ isMarmActive: true, currentSessionId: 'test_session' }); mockCompileSessionSummary.mockResolvedValue('Session summary'); await handleCommand('/summary'); expect(mockCompileSessionSummary).toHaveBeenCalledWith('test_session'); }); test('handleCommand handles /notebook add command', async () => { const { handleCommand } = await import('../src/chatbot/commands.js'); mockGetState.mockReturnValue({ isMarmActive: true, currentSessionId: 'test_session' }); mockManageUserNotebook.mockReturnValue('Notebook data'); await handleCommand('/notebook add: test_key test value'); expect(mockManageUserNotebook).toHaveBeenCalledWith('test_session', 'add', 'test_key', 'test value'); expect(mockGenerateContent).toHaveBeenCalled(); }); test('handleCommand handles /notebook show command', async () => { const { handleCommand } = await import('../src/chatbot/commands.js'); mockGetState.mockReturnValue({ isMarmActive: true, currentSessionId: 'test_session' }); mockManageUserNotebook.mockReturnValue('All notebook entries'); await handleCommand('/notebook show:'); expect(mockManageUserNotebook).toHaveBeenCalledWith('test_session', 'all'); expect(mockGenerateContent).toHaveBeenCalled(); }); test('handleCommand rejects commands when MARM not active', async () => { const { handleCommand } = await import('../src/chatbot/commands.js'); // Keep MARM inactive mockGetState.mockReturnValue({ isMarmActive: false, currentSessionId: null }); await handleCommand('/log entry: test'); await handleCommand('/deep dive: test'); await handleCommand('/notebook add: key value'); // Should not call MARM functions when inactive expect(mockLogSession).not.toHaveBeenCalled(); expect(mockManageUserNotebook).not.toHaveBeenCalled(); }); test('handleCommand handles API errors gracefully', async () => { const { handleCommand } = await import('../src/chatbot/commands.js'); mockGetState.mockReturnValue({ isMarmActive: true, currentSessionId: 'test_session' }); // Mock API failure mockGenerateContent.mockRejectedValue(new Error('API Error')); await handleCommand('/refresh marm'); // Should handle error without crashing expect(mockGenerateContent).toHaveBeenCalled(); }); test('handleCommand validates input formats', async () => { const { handleCommand } = await import('../src/chatbot/commands.js'); mockGetState.mockReturnValue({ isMarmActive: true, currentSessionId: 'test_session' }); // Test commands with missing arguments await handleCommand('/log'); await handleCommand('/notebook'); await handleCommand('/summary'); // Should not call underlying functions with invalid input // (Exact validation depends on implementation) }); });

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/Lyellr88/marm-mcp'

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