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:
1 parent
c82b112624
commit
1feea40684
148 files changed
+11230
-1706
No files matched your search
@@ -0,0 +1,183 @@
|
||||
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
||||
import {
|
||||
getSavedSessions,
|
||||
saveSession,
|
||||
deleteSession,
|
||||
formatSessionAsMarkdown,
|
||||
downloadFile
|
||||
} from '../services/sessionStorage';
|
||||
import type { Scenario, Message, AnalysisResult } from '../types';
|
||||
|
||||
describe('Session Storage & Export Service', () => {
|
||||
const mockStorage: Record<string, string> = {};
|
||||
|
||||
const mockScenario: Scenario = {
|
||||
id: 'test-scenario-1',
|
||||
title: 'Workplace Disagreement',
|
||||
description: 'Resolving friction with a manager',
|
||||
category: 'Workplace',
|
||||
difficulty: 'Intermediate',
|
||||
systemInstruction: 'INTERNAL CBT LOGIC: React with skepticism.',
|
||||
initialMessage: 'Why was this project late?',
|
||||
cbtFocus: ['All-or-Nothing Thinking', 'Mind Reading'],
|
||||
characterName: 'Director Vance'
|
||||
};
|
||||
|
||||
const mockMessages: Message[] = [
|
||||
{
|
||||
id: 'm1',
|
||||
role: 'model',
|
||||
text: 'Why was this project late?',
|
||||
timestamp: new Date()
|
||||
},
|
||||
{
|
||||
id: 'm2',
|
||||
role: 'user',
|
||||
text: 'We encountered unexpected API downtime, but delivered within scope.',
|
||||
timestamp: new Date()
|
||||
}
|
||||
];
|
||||
|
||||
const mockAnalysis: AnalysisResult = {
|
||||
analysis: {
|
||||
distortions: ['Mind Reading'],
|
||||
isAdaptive: true,
|
||||
evidence: 'User acknowledged the delay factually without catastrophic self-blame.'
|
||||
},
|
||||
rating: {
|
||||
score: 4,
|
||||
reason: 'Strong boundary maintenance and objective attribution.'
|
||||
},
|
||||
suggestions: {
|
||||
alternativeThought: 'Delays happen due to technical constraints; focus on resolution.',
|
||||
behavioralStep: 'Send an updated post-mortem dashboard to stakeholders.'
|
||||
}
|
||||
};
|
||||
|
||||
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('getSavedSessions returns empty array when storage is empty', () => {
|
||||
expect(getSavedSessions()).toEqual([]);
|
||||
});
|
||||
|
||||
test('getSavedSessions handles corrupted JSON gracefully', () => {
|
||||
mockStorage['cognitive-clarity-saved-sessions'] = 'INVALID_JSON_CORRUPTED';
|
||||
const sessions = getSavedSessions();
|
||||
expect(sessions).toEqual([]);
|
||||
});
|
||||
|
||||
test('saveSession stores new session and retrieves it', () => {
|
||||
const saved = saveSession(mockScenario, 'text', mockMessages, mockAnalysis);
|
||||
expect(saved.scenarioId).toBe(mockScenario.id);
|
||||
expect(saved.scenarioTitle).toBe(mockScenario.title);
|
||||
expect(saved.mode).toBe('text');
|
||||
expect(saved.messages.length).toBe(2);
|
||||
expect(saved.finalReport?.rating.score).toBe(4);
|
||||
|
||||
const all = getSavedSessions();
|
||||
expect(all.length).toBe(1);
|
||||
expect(all[0].id).toBe(saved.id);
|
||||
});
|
||||
|
||||
test('saveSession caps history at 50 sessions', () => {
|
||||
for (let i = 0; i < 55; i++) {
|
||||
saveSession(
|
||||
{ ...mockScenario, id: `scenario-${i}` },
|
||||
'text',
|
||||
mockMessages
|
||||
);
|
||||
}
|
||||
|
||||
const all = getSavedSessions();
|
||||
expect(all.length).toBe(50);
|
||||
});
|
||||
|
||||
test('deleteSession removes specific session by id', () => {
|
||||
const s1 = saveSession(mockScenario, 'text', mockMessages);
|
||||
const s2 = saveSession({ ...mockScenario, id: 's2', title: 'Second' }, 'voice', mockMessages);
|
||||
|
||||
expect(getSavedSessions().length).toBe(2);
|
||||
|
||||
deleteSession(s1.id);
|
||||
const remaining = getSavedSessions();
|
||||
expect(remaining.length).toBe(1);
|
||||
expect(remaining[0].id).toBe(s2.id);
|
||||
});
|
||||
|
||||
test('formatSessionAsMarkdown generates structured markdown report with all sections', () => {
|
||||
const md = formatSessionAsMarkdown('Workplace Disagreement', mockAnalysis, mockMessages);
|
||||
|
||||
expect(md).toContain('# Cognitive Clarity — Clinical CBT Session Report');
|
||||
expect(md).toContain('**Scenario:** Workplace Disagreement');
|
||||
expect(md).toContain('✅ Adaptive Growth Observed');
|
||||
expect(md).toContain('**Mastery Rating:** 4 / 5');
|
||||
expect(md).toContain('- Mind Reading');
|
||||
expect(md).toContain('User acknowledged the delay factually');
|
||||
expect(md).toContain('Delays happen due to technical constraints');
|
||||
expect(md).toContain('## 3. Session Transcript');
|
||||
expect(md).toContain('**[AI Partner]**: Why was this project late?');
|
||||
expect(md).toContain('**[You]**: We encountered unexpected API downtime');
|
||||
});
|
||||
|
||||
test('downloadFile creates object URL and triggers anchor click in DOM', () => {
|
||||
let clicked = false;
|
||||
let appended = false;
|
||||
let removed = false;
|
||||
let revokedUrl = '';
|
||||
|
||||
const mockAnchor = {
|
||||
href: '',
|
||||
download: '',
|
||||
click: () => {
|
||||
clicked = true;
|
||||
}
|
||||
};
|
||||
|
||||
vi.stubGlobal('URL', {
|
||||
createObjectURL: () => 'blob:mock-url-123',
|
||||
revokeObjectURL: (url: string) => {
|
||||
revokedUrl = url;
|
||||
}
|
||||
});
|
||||
|
||||
vi.stubGlobal('document', {
|
||||
createElement: () => mockAnchor,
|
||||
body: {
|
||||
appendChild: () => {
|
||||
appended = true;
|
||||
},
|
||||
removeChild: () => {
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
downloadFile('# Report', 'report.md', 'text/markdown');
|
||||
|
||||
expect(mockAnchor.download).toBe('report.md');
|
||||
expect(mockAnchor.href).toBe('blob:mock-url-123');
|
||||
expect(clicked).toBe(true);
|
||||
expect(appended).toBe(true);
|
||||
expect(removed).toBe(true);
|
||||
expect(revokedUrl).toBe('blob:mock-url-123');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user