d3ro-voice/apps/desktop/tests/e2e/red_team_cycle5.spec.ts
Yun Chan 6ba25f53b7 fix(desktop): surface configuration and provider failures instead of hiding them
Several desktop paths quietly substituted defaults or partial results: a
config write could fall back to a throwaway in-memory store, speech provider
errors were absorbed into empty transcriptions, and meeting exports built
file names from raw titles.

Writes now fail explicitly when the store is unavailable, provider and model
failures reach the UI as errors, and export names pass through one
sanitizer. Settings, license, ad, and support surfaces use the shared theme
tokens, unused hotkey helpers are gone, and the package gains strict
node/renderer typecheck configs plus red-team e2e scenarios for these flows.
2026-09-16 23:23:58 +09:00

265 lines
9 KiB
TypeScript

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\'; <script>alert("XSS")</script>';
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([]);
});
});