import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'; import { AudioEngine } from '../src/lib/audio/AudioEngine.svelte'; import { UserSettingsStore } from '../src/lib/stores/UserSettingsStore.svelte'; import type { Scenario } from '../types'; describe('AudioEngine Reactive Controller', () => { const mockScenario: Scenario = { id: 'test-s1', title: 'Silent Friend', description: 'Friend has not replied to your text', category: 'Relationships', difficulty: 'Beginner', systemInstruction: 'INTERNAL CBT LOGIC: You were just busy.', initialMessage: 'Hey, sorry I was swamped yesterday.', cbtFocus: ['Mind Reading'], characterName: 'Sam' }; let mockAudioContext: any; let mockUserMediaStream: any; beforeEach(() => { mockAudioContext = { state: 'running', destination: {}, createAnalyser: () => ({ fftSize: 256, frequencyBinCount: 128, getByteFrequencyData: (arr: Uint8Array) => arr.fill(50) }), createMediaStreamSource: () => ({ connect: vi.fn() }), createBufferSource: () => ({ buffer: null, connect: vi.fn(), start: vi.fn(), stop: vi.fn(), onended: null }), decodeAudioData: async () => ({ duration: 2.0 }), resume: vi.fn(), close: vi.fn() }; mockUserMediaStream = { getTracks: () => [ { stop: vi.fn() } ] }; vi.stubGlobal('AudioContext', function () { return mockAudioContext; }); vi.stubGlobal('navigator', { mediaDevices: { getUserMedia: async () => mockUserMediaStream, enumerateDevices: async () => [] } }); vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => 1); vi.stubGlobal('cancelAnimationFrame', vi.fn()); }); afterEach(() => { vi.restoreAllMocks(); }); test('AudioEngine reports error if OpenRouter API key is missing', async () => { const userSettings = new UserSettingsStore(); userSettings.openRouterApiKey = ''; const originalEnv = process.env.OPENROUTER_API_KEY; try { process.env.OPENROUTER_API_KEY = ''; const engine = new AudioEngine(mockScenario, userSettings); await engine.start(); expect(engine.status).toBe('error'); expect(engine.errorMessage).toContain('OpenRouter API Key is missing'); } finally { if (originalEnv !== undefined) process.env.OPENROUTER_API_KEY = originalEnv; } }); test('AudioEngine initializes, starts, and loads initial greeting when API key is present', async () => { const userSettings = new UserSettingsStore(); userSettings.openRouterApiKey = 'sk-or-v1-mock-valid-key'; const engine = new AudioEngine(mockScenario, userSettings); // Mock speakText to avoid real network call engine.speakText = vi.fn(); await engine.start(); expect(engine.status).toBe('active'); expect(engine.errorMessage).toBe(null); expect(engine.messages.length).toBe(1); expect(engine.messages[0].text).toBe(mockScenario.initialMessage); expect(engine.speakText).toHaveBeenCalledWith(mockScenario.initialMessage); engine.destroy(); }); test('toggleMute toggles isMuted state and stops audio', async () => { const userSettings = new UserSettingsStore(); const engine = new AudioEngine(mockScenario, userSettings); expect(engine.isMuted).toBe(false); engine.toggleMute(); expect(engine.isMuted).toBe(true); engine.toggleMute(); expect(engine.isMuted).toBe(false); engine.destroy(); }); test('stopAllAudio resets AI speaking flags and cancels speech synthesis', () => { const cancelMock = vi.fn(); vi.stubGlobal('speechSynthesis', { cancel: cancelMock, speak: vi.fn() }); const userSettings = new UserSettingsStore(); const engine = new AudioEngine(mockScenario, userSettings); engine.isAiSpeaking = true; engine.aiAudioLevel = 45; engine.stopAllAudio(); expect(engine.isAiSpeaking).toBe(false); expect(engine.aiAudioLevel).toBe(0); expect(cancelMock).toHaveBeenCalled(); engine.destroy(); }); test('activeLevel derived property reflects active speaker volume', () => { const userSettings = new UserSettingsStore(); const engine = new AudioEngine(mockScenario, userSettings); engine.micLevel = 25; engine.aiAudioLevel = 60; engine.isAiSpeaking = false; expect(engine.activeLevel).toBe(25); engine.isAiSpeaking = true; expect(engine.activeLevel).toBe(60); engine.destroy(); }); });