// apps/desktop/tests/e2e/red_team_cycle2.spec.ts // Extreme Red Team: Headful Deep Interactive Testing for Cycle 2 (AI Pipelines & Workspaces) import { test, expect, _electron as electron, type Page, type ElectronApplication } from '@playwright/test'; import * as fs from 'fs'; import * as path from 'path'; const SCREENSHOT_DIR = 'C:/Users/encep/.gemini/antigravity/brain/bbff18a3-721d-4c43-989f-1d6964e15be7/screenshots'; test.describe('Extreme Red Team - Cycle 2: AI Pipelines & Advanced Workspaces', () => { test.describe.configure({ mode: 'serial' }); let electronApp: ElectronApplication; let window: Page; const consoleErrors: string[] = []; const uncaughtExceptions: string[] = []; let testUserDataDir: string; test.beforeAll(async () => { test.setTimeout(60000); if (!fs.existsSync(SCREENSHOT_DIR)) { fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); } testUserDataDir = path.join( 'C:/Users/encep/AppData/Local/Temp', 'playwright-redteam-cycle2-' + Date.now() ); electronApp = await electron.launch({ args: [ 'out/main/index.js', '--disable-gpu', '--no-sandbox', `--user-data-dir=${testUserDataDir}`, ], env: { ...process.env, NODE_ENV: 'test', }, }); // Find main window for (let i = 0; i < 50; i++) { for (const w of electronApp.windows()) { try { const url = w.url(); if (url && url.includes('index.html') && !url.includes('popups/')) { window = w; break; } } catch { // window might be navigating } } if (window) break; await new Promise((r) => setTimeout(r, 400)); } if (!window) { window = electronApp.windows()[0] || (await electronApp.firstWindow()); } // Attach sentinels window.on('pageerror', (err) => { console.error('[PAGEERROR]', err.message); uncaughtExceptions.push(err.message); }); window.on('console', (msg) => { if (msg.type() === 'error') { const text = msg.text(); // Ignore benign chrome font/csp warnings if (!text.includes('Failed to load resource') && !text.includes('favicon.ico')) { consoleErrors.push(text); } } }); await window.waitForLoadState('domcontentloaded'); // Bypass Onboarding modal for testing await window.evaluate(async () => { if (window.electronAPI?.config) { await window.electronAPI.config.set({ key: 'onboardingCompleted', value: true }); await window.electronAPI.config.set({ key: 'appUsageMode', value: 'online' }); await window.electronAPI.config.set({ key: 'llmBackend', value: 'online' }); } }); await window.waitForTimeout(500); await window.reload(); await window.waitForLoadState('domcontentloaded'); await window.waitForTimeout(1000); }); test.afterAll(async () => { if (electronApp) { await electronApp.close(); } }); test('RT-06: Meeting Mode List, Start Recording Trigger & Diagnostic Dialog', async () => { // 1. Navigate to Meeting Mode const meetingNav = window.locator('text=/^(Meeting|회의)$/').first(); await expect(meetingNav).toBeVisible({ timeout: 5000 }); await meetingNav.click(); await window.waitForTimeout(600); // Verify page header await expect(window.locator('text=/^(Meeting|회의 모드)$/').first()).toBeVisible({ timeout: 5000 }); // 2. Click "새 회의 시작" button const startBtn = window.getByRole('button', { name: /새 회의 시작|새 회의/i }).first(); await expect(startBtn).toBeVisible(); await startBtn.click(); await window.waitForTimeout(1000); // If diagnostic dialog opened (e.g. no mic or whisper unavailable in test container) const dialog = window.getByRole('dialog'); const isDialogVisible = await dialog.isVisible({ timeout: 2000 }).catch(() => false); if (isDialogVisible) { // Verify diagnostic elements await expect(window.getByText('Meeting Intelligence Diagnostics')).toBeVisible(); await expect(window.getByText(/발생 원인/)).toBeVisible(); await expect(window.getByText(/해결 조치/)).toBeVisible(); // Click "닫기" button const closeBtn = window.getByRole('button', { name: '닫기' }); await expect(closeBtn).toBeVisible(); await closeBtn.click(); await window.waitForTimeout(500); await expect(dialog).not.toBeVisible(); } // Take screenshot await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt06_meeting_list_and_dialog.png'), }); expect(uncaughtExceptions).toEqual([]); }); test('RT-07: Meeting Mode Detail Workspace - Inline Title, ViewModes, MarkdownEditor, Action Items & Chat', async () => { // Seed a meeting session directly via python to avoid Node ABI mismatch const sessionId = 'test-session-redteam-' + Date.now(); const dbPath = path.join(testUserDataDir, 'users', '_local', 'd3ro.db'); const seedPyPath = path.join(testUserDataDir, 'seed.py'); const now = Date.now(); const pyScript = `import sqlite3, sys db_path = sys.argv[1] session_id = sys.argv[2] now = int(sys.argv[3]) conn = sqlite3.connect(db_path) c = conn.cursor() c.execute(""" INSERT INTO meeting_sessions (id, title, status, started_at, ended_at, duration_ms, raw_transcript, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( session_id, 'Red Team AI 전략 회의', 'completed', now - 1800000, now, 1800000, '[00:05] [참석자 A] D3RO Voice 시스템 성능 점검을 시작합니다.\\n[00:15] [참석자 B] 극단적 레드팀 테스트를 수행 중입니다.\\n[01:00] [참석자 A] 품질 검증을 완벽하게 통과했습니다.', now - 1800000, now )) c.execute(""" INSERT INTO meeting_memos (id, session_id, content, timestamp_ms, created_at) VALUES (?, ?, ?, ?, ?) """, ( 'memo-' + session_id, session_id, '#결정 음성 인식 모델 레이턴시 최적화 완료', 5000, now - 1795000 )) c.execute(""" INSERT INTO meeting_documents (id, session_id, template_type, title, content, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?) """, ( 'doc-' + session_id, session_id, 'minutes', '전략 회의록', '# 레드팀 전략 회의록\\n\\n## 1. 개요\\n- 시스템 안정성 심층 검증\\n\\n## 2. 액션 아이템\\n- [ ] E2E 헤드풀 테스트 통과하기\\n- [x] SQLite ABI 불일치 수정 완료\\n\\n## 3. 결론\\n전체 파이프라인 완벽 가동', now, now )) conn.commit() conn.close() `; fs.writeFileSync(seedPyPath, pyScript, 'utf-8'); const cp = require('child_process'); cp.execSync(`python "${seedPyPath}" "${dbPath}" "${sessionId}" "${now}"`); fs.unlinkSync(seedPyPath); // Refresh meeting list by navigating to dashboard and back to meeting await window.locator('text=/^(Dashboard|대시보드)$/').first().click(); await window.waitForTimeout(400); await window.locator('text=/^(Meeting|회의)$/').first().click(); await window.waitForTimeout(800); // Click on the seeded session card const sessionCard = window.getByText('Red Team AI 전략 회의'); await expect(sessionCard).toBeVisible({ timeout: 5000 }); await sessionCard.click(); await window.waitForTimeout(800); // 1. Verify MeetingDetailTabs is rendered await expect(window.getByRole('tab', { name: '전략 회의록' })).toBeVisible({ timeout: 5000 }); // 2. Test Inline Title Editing const editTitleBtn = window.locator('button:has(svg.lucide-pencil)'); if (await editTitleBtn.isVisible()) { await editTitleBtn.click(); await window.waitForTimeout(300); const titleInput = window.locator('input[value*="Red Team AI 전략 회의"]'); await expect(titleInput).toBeVisible(); await titleInput.fill('Red Team AI 전략 회의 (검증됨)'); const saveTitleBtn = window.locator('button:has(svg.lucide-check)'); await saveTitleBtn.click(); await window.waitForTimeout(500); await expect(window.getByText('Red Team AI 전략 회의 (검증됨)')).toBeVisible(); } // 3. Test ViewMode switches (SegmentControl) const transcriptOnlyBtn = window.getByText('Transcript ▤'); if (await transcriptOnlyBtn.isVisible()) { await transcriptOnlyBtn.click(); await window.waitForTimeout(300); await expect(window.getByText(/D3RO Voice 시스템 성능 점검/)).toBeVisible(); const docOnlyBtn = window.getByText('Minutes ▥'); await docOnlyBtn.click(); await window.waitForTimeout(300); await expect(window.getByRole('heading', { name: '레드팀 전략 회의록' })).toBeVisible(); const splitBtn = window.getByText('Split ◫'); await splitBtn.click(); await window.waitForTimeout(300); } // 4. Test MarkdownEditor preview vs edit toggle const editModeBtn = window.locator('button').filter({ hasText: '편집' }).first(); if (await editModeBtn.isVisible()) { await editModeBtn.click(); await window.waitForTimeout(300); const textarea = window.locator('textarea'); await expect(textarea).toBeVisible(); await textarea.type('\n\n## 4. 추가 사항\n- 레드팀 사이클 2 통과'); await window.waitForTimeout(600); // let autosave debounce fire const previewModeBtn = window.locator('button').filter({ hasText: '미리보기' }).first(); await previewModeBtn.click(); await window.waitForTimeout(300); await expect(window.getByText(/추가 사항/)).toBeVisible(); } // 5. Test Action Items tab const actionItemsTab = window.getByText(/Action Items/i); if (await actionItemsTab.isVisible()) { await actionItemsTab.click(); await window.waitForTimeout(400); await expect(window.getByText(/E2E 헤드풀 테스트/)).toBeVisible(); // Toggle action item checkbox const checkbox = window.locator('input[type="checkbox"]').first(); await checkbox.click(); await window.waitForTimeout(300); } // 6. Test Scratchpad Notepad tab const notepadTab = window.getByText(/Granola Notepad/i); if (await notepadTab.isVisible()) { await notepadTab.click(); await window.waitForTimeout(400); const notepadTextarea = window.locator('textarea').first(); if (await notepadTextarea.isVisible()) { await notepadTextarea.fill('핵심 안건: 실시간 STT 엔진 최적화 및 텍스트 렌더링 검증'); await window.waitForTimeout(300); } } // 7. Test Export Menu const exportBtn = window.getByRole('button', { name: /내보내기/i }); if (await exportBtn.isVisible()) { await exportBtn.click(); await window.waitForTimeout(300); await expect(window.getByText(/Markdown/i)).toBeVisible(); // Press Escape to dismiss menu await window.keyboard.press('Escape'); await window.waitForTimeout(300); } // 8. Test Meeting Chat Panel const chatInput = window.locator('input[placeholder*="AI에게 질문"]'); if (await chatInput.isVisible()) { await chatInput.fill('회의 요약해줘'); await window.keyboard.press('Enter'); await window.waitForTimeout(600); // Verify user message appears await expect(window.getByText('회의 요약해줘')).toBeVisible(); // Clear chat const clearChatBtn = window.locator('button:has(svg.lucide-trash2)'); if (await clearChatBtn.isVisible()) { await clearChatBtn.click(); await window.waitForTimeout(300); await expect(window.getByText('회의 요약해줘')).not.toBeVisible(); } } // 9. Back to meeting list const backBtn = window.locator('button:has(svg.lucide-arrow-left)'); await backBtn.click(); await window.waitForTimeout(500); await expect(window.getByText('Red Team AI 전략 회의 (검증됨)')).toBeVisible(); // Take screenshot await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt07_meeting_detail_workspace.png'), }); expect(uncaughtExceptions).toEqual([]); }); test('RT-08: Knowledge Base - Semantic Synthesis Query, Chips, Add & Delete Document', async () => { // 1. Navigate to Knowledge Base const ragNav = window.locator('text=/^(Knowledge Base|지식 베이스)$/').first(); await expect(ragNav).toBeVisible({ timeout: 5000 }); await ragNav.click(); await window.waitForTimeout(600); // Verify page header await expect(window.locator('text=/^(Knowledge Base|지식 베이스)$/').first()).toBeVisible({ timeout: 5000 }); // 2. Test query input const queryInput = window.locator('input[placeholder*="질문하세요"]'); await expect(queryInput).toBeVisible(); await queryInput.fill('D3RO Voice 아키텍처'); // Click Search button const searchBtn = window.getByRole('button', { name: /Search|검색/i }); await expect(searchBtn).toBeVisible(); await searchBtn.click(); await window.waitForTimeout(600); // Verify searching spinner or graceful error feedback await window.waitForTimeout(1000); // 3. Add a mock document via IPC const tempFilePath = path.join(testUserDataDir, 'red_team_sample.md'); fs.writeFileSync( tempFilePath, '# D3RO Voice 시스템 문서\n\nD3RO Voice는 고성능 로컬 퍼스트 음성 비서 아키텍처를 지원하며 빠른 STT 및 LLM 파이프라인을 제공합니다.', 'utf-8' ); const addDocResult = await window.evaluate(async (filePath) => { return await window.electronAPI.rag.addDocument({ filePath }); }, tempFilePath); expect(addDocResult.success).toBe(true); // Reload documents by re-navigating await ragNav.click(); await window.waitForTimeout(800); // Verify document card appears await expect(window.getByText('red_team_sample.md')).toBeVisible({ timeout: 5000 }); await expect(window.getByText('MD', { exact: true })).toBeVisible(); // 4. Test Reindex button const reindexBtn = window.locator('button:has(svg.lucide-refresh-cw)').first(); if (await reindexBtn.isVisible()) { await reindexBtn.click(); await window.waitForTimeout(500); } // 5. Test Delete document button const deleteDocBtn = window.locator('button:has(svg.lucide-trash-2)').first(); await expect(deleteDocBtn).toBeVisible(); await deleteDocBtn.click(); await window.waitForTimeout(600); // Verify document is removed await expect(window.getByText('red_team_sample.md')).not.toBeVisible(); // Clean up temp file if (fs.existsSync(tempFilePath)) { fs.unlinkSync(tempFilePath); } // Take screenshot await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt08_knowledge_base.png'), }); expect(uncaughtExceptions).toEqual([]); }); test('RT-09: Voice Conversation - Canvas, Text Chat, Mic Toggle & Clear History', async () => { // 1. Navigate to Voice Conversation const voiceNav = window.locator('text=/^(Talk|대화)$/').first(); await expect(voiceNav).toBeVisible({ timeout: 5000 }); await voiceNav.click(); await window.waitForTimeout(600); // Verify PageHeader await expect(window.locator('text=/^(Voice Conversation|음성 대화)$/').first()).toBeVisible({ timeout: 5000 }); await expect(window.getByText(/대기|IDLE/i).first()).toBeVisible(); // Verify empty state await expect(window.getByText(/D3RO에게 말을 걸어보세요|Talk to D3RO/i)).toBeVisible(); // 2. Test Text Fallback Chat Input const chatInput = window.locator('input[placeholder*="메시지 입력"]'); await expect(chatInput).toBeVisible(); await chatInput.fill('안녕 D3RO, 현재 상태 점검해줘'); const sendBtn = window.locator('button:has(svg.lucide-send)'); await expect(sendBtn).toBeVisible(); await sendBtn.click(); await window.waitForTimeout(1000); // Verify user message appears in bubble await expect(window.getByText('안녕 D3RO, 현재 상태 점검해줘')).toBeVisible({ timeout: 5000 }); // 3. Test Clear History const clearBtn = window.locator('button:has(svg.lucide-trash-2)'); if (await clearBtn.isVisible()) { await clearBtn.click(); await window.waitForTimeout(500); } // If session is active or thinking, end session so state returns to idle const stopSessionBtn = window.getByRole('button', { name: /종료|End/i }); if (await stopSessionBtn.isVisible()) { await stopSessionBtn.click(); await window.waitForTimeout(600); } await expect(window.getByText(/D3RO에게 말을 걸어보세요|Talk to D3RO/i)).toBeVisible({ timeout: 5000 }); // Take screenshot await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt09_voice_conversation.png'), }); expect(uncaughtExceptions).toEqual([]); }); });