- 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
184 lines
5.5 KiB
TypeScript
184 lines
5.5 KiB
TypeScript
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');
|
|
});
|
|
});
|