Files
clarity-helper/test/sessionStorage.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

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