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
This commit is contained in:
jpremill committed 2026-09-29 23:35:05 -04:00
1 parent c82b112624
commit 1feea40684
148 files changed
+11230 -1706

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
import { describe, test, expect, vi, beforeEach } from 'vitest';
import { UserSettingsStore } from '../src/lib/stores/UserSettingsStore.svelte';
import { AppStateStore } from '../src/lib/stores/AppStateStore.svelte';
import type { Scenario } from '../types';
describe('Svelte 5 Reactive Stores', () => {
const mockStorage: Record<string, string> = {};
beforeEach(() => {
for (const key in mockStorage) {
delete mockStorage[key];
}
vi.stubGlobal('localStorage', {
getItem: (key: string) => mockStorage[key] || null,
setItem: (key: string, val: string) => {
mockStorage[key] = val;
},
removeItem: (key: string) => {
delete mockStorage[key];
},
clear: () => {
for (const key in mockStorage) {
delete mockStorage[key];
}
}
});
});
test('UserSettingsStore initializes with 3.8 models and correct defaults', () => {
const store = new UserSettingsStore();
expect(store.voiceName).toBe('Zephyr');
expect(store.roleplayModel).toBe('google/gemini-3.8-flash');
expect(store.supervisorModel).toBe('google/gemini-3.8-flash');
expect(store.genderPreference).toBe('Neutral');
});
test('UserSettingsStore updates properties cleanly', () => {
const store = new UserSettingsStore();
store.update({
voiceName: 'Kore',
openRouterApiKey: 'sk-or-v1-svelte-test-key',
roleplayModel: 'google/gemini-3.7-flash'
});
expect(store.voiceName).toBe('Kore');
expect(store.openRouterApiKey).toBe('sk-or-v1-svelte-test-key');
expect(store.roleplayModel).toBe('google/gemini-3.7-flash');
const plain = store.toUserSettings();
expect(plain.voiceName).toBe('Kore');
expect(plain.openRouterApiKey).toBe('sk-or-v1-svelte-test-key');
});
test('UserSettingsStore recovers gracefully from corrupted localStorage', () => {
mockStorage['cognitive-clarity-settings'] = 'MALFORMED_JSON_STRING';
const store = new UserSettingsStore();
expect(store.voiceName).toBe('Zephyr');
expect(store.roleplayModel).toBe('google/gemini-3.8-flash');
});
test('AppStateStore manages scenario catalog and filtering', () => {
const app = new AppStateStore();
expect(app.allScenarios.length).toBe(14);
app.selectedDifficulty = 'Beginner';
expect(app.filteredScenarios.every((s) => s.difficulty === 'Beginner')).toBe(true);
app.selectedDifficulty = 'Advanced';
expect(app.filteredScenarios.every((s) => s.difficulty === 'Advanced')).toBe(true);
app.selectedDifficulty = 'All';
expect(app.filteredScenarios.length).toBe(14);
});
test('AppStateStore adds and removes custom scenarios', () => {
const app = new AppStateStore();
const customScenario: Scenario = {
id: 'custom-test-1',
title: 'Custom Job Negotiation',
description: 'Negotiating offer',
category: 'Workplace',
difficulty: 'Intermediate',
systemInstruction: 'INTERNAL CBT LOGIC: Be firm.',
initialMessage: 'Here is our final offer.',
cbtFocus: ['All-or-Nothing Thinking'],
characterName: 'Hiring Lead'
};
app.addCustomScenario(customScenario);
expect(app.customScenarios.length).toBe(1);
expect(app.allScenarios.length).toBe(15);
expect(app.allScenarios[0].id).toBe('custom-test-1');
app.deleteCustomScenario('custom-test-1');
expect(app.customScenarios.length).toBe(0);
expect(app.allScenarios.length).toBe(14);
});
test('AppStateStore selectScenario and exitSession lifecycle', async () => {
vi.useFakeTimers();
const app = new AppStateStore();
const targetScenario = app.allScenarios[0];
app.selectScenario(targetScenario, 'voice');
expect(app.isPreparing).toBe(true);
expect(app.activeScenario).toBe(null);
vi.advanceTimersByTime(1400);
expect(app.isPreparing).toBe(false);
expect(app.activeScenario?.id).toBe(targetScenario.id);
expect(app.sessionMode).toBe('voice');
app.exitSession();
expect(app.activeScenario).toBe(null);
vi.useRealTimers();
});
});