Files
clarity-helper/test/audioEngine.test.ts
jpremill 1feea40684 feat(ui): redesign voice cockpit with clinical companion drawer, Svelte 5 migration, and Gemini 3.8 voice
- Re-architect VoiceSession into full-bleed studio cockpit with overlay slide-over companion drawer
- Eliminate width squishing defect on LiveInsightPanel and add native dark/light theme support
- Integrate authentic Google Gemini 3.8 Flash TTS with Fenrir persona and fallback audio pipeline
- Implement comprehensive Playwright E2E visual regression suite across Desktop, iPad, and iPhone (21 tests, 45 snapshots)
- Migrate core application to Svelte 5 runes (, , ) with reactive stores
- Maintain .NET 8 Azure Functions serverless endpoints and clean build hygiene
2026-09-29 23:35:05 -04:00

162 lines
4.5 KiB
TypeScript

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();
});
});