import { test, expect, _electron as electron, ElectronApplication, Page } from '@playwright/test'; import path from 'path'; import fs from 'fs'; const SCREENSHOT_DIR = path.resolve('C:/Users/encep/.gemini/antigravity/brain/bbff18a3-721d-4c43-989f-1d6964e15be7/screenshots'); test.describe.serial('Extreme Red Team - Cycle 5: Rapid Route Thrashing, Onboarding Wizard & Input Fuzzing', () => { let app: ElectronApplication; let window: Page; let testUserDataDir: string; let uncaughtExceptions: string[] = []; test.beforeAll(async () => { if (!fs.existsSync(SCREENSHOT_DIR)) { fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); } testUserDataDir = path.join( 'C:/Users/encep/AppData/Local/Temp', 'playwright-redteam-cycle5-' + Date.now() ); app = 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 app.windows()) { try { const url = w.url(); if (url && url.includes('index.html') && !url.includes('popups/')) { window = w; break; } } catch { // window navigating } } if (window) break; await new Promise((r) => setTimeout(r, 400)); } if (!window) { window = app.windows()[0] || (await app.firstWindow()); } window.on('pageerror', (err) => { console.error('[PAGEERROR in Electron Window]:', err.message); uncaughtExceptions.push(err.message); }); await window.waitForLoadState('domcontentloaded'); // Bypass Onboarding modal initially 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 (app) { await app.close(); } }); test.beforeEach(() => { uncaughtExceptions = []; }); test('RT-16: Rapid Route Thrashing & Concurrency Stress Test', async () => { // 7 navigation routes in AppLayout const routes = [ '대시보드', '기록', '단어장', '명령어', '음성 대화', '지식 베이스', '회의록' ]; // Rapid thrashing loop - 10 full passes at high speed for (let loop = 0; loop < 5; loop++) { for (const routeName of routes) { const navButton = window.getByText(routeName, { exact: true }).first(); if (await navButton.isVisible()) { await navButton.click(); // Hyper rapid 40ms interval await window.waitForTimeout(40); } } } // Settle back to dashboard const dashboardNav = window.getByText('대시보드', { exact: true }).first(); await dashboardNav.click(); await window.waitForTimeout(500); // Verify system survived rapid thrashing and dashboard is completely healthy await expect(window.getByText(/총 발화|오늘 사용량|음성 인식|대시보드/i).first()).toBeVisible({ timeout: 5000 }); await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt16_route_thrashing_settled.png'), }); expect(uncaughtExceptions).toEqual([]); }); test('RT-17: Onboarding Modal & First-Run Wizard Headful Execution', async () => { // 1. Reset onboarding to false and reload to trigger OnboardingModal await window.evaluate(async () => { if (window.electronAPI?.config) { await window.electronAPI.config.set({ key: 'onboardingCompleted', value: false }); } }); await window.reload(); await window.waitForLoadState('domcontentloaded'); await window.waitForTimeout(1000); // 2. Verify OnboardingModal is visible await expect(window.getByText(/D3RO Voice 시작 가이드/i)).toBeVisible({ timeout: 10000 }); await expect(window.getByText(/로컬 AI 모드/i).first()).toBeVisible(); await expect(window.getByText(/온라인 클라우드 모드/i).first()).toBeVisible(); await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt17_01_onboarding_select_mode.png'), }); // 3. Test Local Mode Selection await window.getByRole('button', { name: /로컬 AI 모드로 시작/i }).click(); await window.waitForTimeout(600); // Verify transitions to local_ollama_setup phase await expect(window.getByText(/Ollama 로컬 엔진/i).first()).toBeVisible({ timeout: 5000 }); await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt17_02_onboarding_local_setup.png'), }); // Test back button const backBtn = window.getByText('뒤로 가기', { exact: true }); await expect(backBtn).toBeVisible(); await backBtn.click(); await window.waitForTimeout(500); // Verify back to select_mode await expect(window.getByText(/로컬 AI 모드/i).first()).toBeVisible(); // 4. Test Online Cloud Mode Selection await window.getByRole('button', { name: /온라인 계정 로그인/i }).click(); await window.waitForTimeout(600); // Verify transitions to online_auth phase await expect(window.getByText(/온라인 서비스를 이용하기 위해/i).first()).toBeVisible({ timeout: 5000 }); await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt17_03_onboarding_online_auth.png'), }); // Go back again await window.getByText('뒤로 가기', { exact: true }).click(); await window.waitForTimeout(500); // 5. Restore onboardingCompleted await window.evaluate(async () => { await window.electronAPI.config.set({ key: 'onboardingCompleted', value: true }); }); await window.reload(); await window.waitForLoadState('domcontentloaded'); await window.waitForTimeout(1000); expect(uncaughtExceptions).toEqual([]); }); test('RT-18: Adversarial Input Fuzzing, Boundary Conditions & SQL/XSS Injection Defense', async () => { // 1. Dictionary Fuzzing const dictNav = window.locator('text=/^(Dictionary|사전|단어장)$/').first(); await dictNav.click(); await window.waitForTimeout(600); const sqlXssPayload = '\' OR \'1\'=\'1\'; '; const safeReplacement = 'FuzzReplacement'; // Click 추가 button const addWordBtn = window.locator('button:has-text("추가"), button:has-text("Add")').first(); await addWordBtn.click(); await window.waitForTimeout(400); // Fill inputs in dialog const wordInput = window.locator('div[role="dialog"] input').first(); const pronInput = window.locator('div[role="dialog"] input').nth(1); await wordInput.fill(sqlXssPayload); await pronInput.fill(safeReplacement); // Save const saveBtn = window.locator('div[role="dialog"] button:has-text("저장"), div[role="dialog"] button:has-text("Save")').first(); await saveBtn.click(); await window.waitForTimeout(600); // Verify word was stored safely without executing XSS or corrupting SQLite await expect(window.getByText(sqlXssPayload).first()).toBeVisible({ timeout: 5000 }); await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt18_01_dictionary_fuzzing_saved.png'), }); // Clean up: delete the fuzzed word const deleteBtn = window.locator('button[aria-label*="삭제"], button:has(svg.lucide-trash-2)').first(); if (await deleteBtn.isVisible()) { await deleteBtn.click(); await window.waitForTimeout(400); // Confirm dialog if any const confirmBtn = window.locator('div[role="dialog"] button:has-text("삭제"), div[role="dialog"] button:has-text("Delete")').first(); if (await confirmBtn.isVisible()) { await confirmBtn.click(); await window.waitForTimeout(400); } } // 2. Knowledge Base Query Fuzzing (Regex Break Characters) const kbNav = window.locator('text=/^(Knowledge|지식 베이스)$/').first(); await kbNav.click(); await window.waitForTimeout(600); const regexBreakQuery = '[([{\\\\^$|?*+'; const kbSearchInput = window.locator('input[placeholder*="질문"], input[placeholder*="검색"]').first(); if (await kbSearchInput.isVisible()) { await kbSearchInput.fill(regexBreakQuery); await window.keyboard.press('Enter'); await window.waitForTimeout(800); } await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt18_02_knowledge_fuzzing_query.png'), }); // Settle back to dashboard const dashboardNav = window.locator('text=/^(Dashboard|대시보드)$/').first(); await dashboardNav.click(); await window.waitForTimeout(500); expect(uncaughtExceptions).toEqual([]); }); });