/** * Smoke Tests — Desktop 功能闭环断裂探测 * * 6 个冒烟测试验证 Desktop 功能的完整闭环。 * 覆盖 Agent/Hands/Pipeline/记忆/管家/技能。 * * 前提条件: * - Desktop App 运行在 http://localhost:1420 (pnpm tauri dev) * - 后端服务可用 (SaaS 或 Kernel) * * 运行: cd desktop && npx playwright test smoke_features */ import { test, expect, type Page } from '@playwright/test'; test.setTimeout(120000); async function waitForAppReady(page: Page) { await page.goto('/'); await page.waitForSelector('#root > *', { timeout: 15000 }); await page.waitForTimeout(1000); } // ── F1: Agent 全生命周期 ────────────────────────────────────────── test('F1: Agent 生命周期 — 创建→切换→删除', async ({ page }) => { await waitForAppReady(page); // 检查 Agent 列表 const agentCount = await page.evaluate(() => { const stores = (window as any).__ZCLAW_STORES__; const agents = stores?.agent?.getState?.()?.agents || stores?.chat?.getState?.()?.agents || []; return agents.length; }); console.log(`Current agent count: ${agentCount}`); // 查找新建 Agent 按钮 const newAgentBtn = page.locator( 'button[aria-label*="新建"], button[aria-label*="创建"], button:has-text("新建"), [data-testid="new-agent"]' ).first(); if (await newAgentBtn.isVisible().catch(() => false)) { await newAgentBtn.click(); // 等待创建对话框/向导 await page.waitForTimeout(2000); await page.screenshot({ path: 'test-results/smoke_f1_agent_create.png' }); // 检查是否有创建表单 const formVisible = await page.locator( '.ant-modal, [role="dialog"], .agent-wizard, [class*="create"]' ).first().isVisible().catch(() => false); console.log(`F1: Agent create form visible: ${formVisible}`); } else { // 通过 Store 直接检查 Agent 列表 const agents = await page.evaluate(() => { const stores = (window as any).__ZCLAW_STORES__; const state = stores?.agent?.getState?.() || stores?.chat?.getState?.(); return state?.agents || state?.clones || []; }); console.log(`F1: ${agents.length} agents found, no create button`); } }); // ── F2: Hands 触发 ──────────────────────────────────────────────── test('F2: Hands 触发 — 面板加载→列表非空', async ({ page }) => { await waitForAppReady(page); // 查找 Hands 面板入口 const handsBtn = page.locator( 'button[aria-label*="Hands"], button[aria-label*="自动化"], [data-testid="hands-panel"], :text("Hands")' ).first(); if (await handsBtn.isVisible().catch(() => false)) { await handsBtn.click(); await page.waitForTimeout(2000); // 检查 Hands 列表 const handsCount = await page.evaluate(() => { const stores = (window as any).__ZCLAW_STORES__; const hands = stores?.hand?.getState?.()?.hands || []; return hands.length; }); console.log(`F2: ${handsCount} Hands available`); expect(handsCount).toBeGreaterThan(0); } else { // 通过 Store 检查 const handsCount = await page.evaluate(() => { const stores = (window as any).__ZCLAW_STORES__; return stores?.hand?.getState?.()?.hands?.length ?? -1; }); console.log(`F2: handsCount from Store = ${handsCount}`); if (handsCount >= 0) { expect(handsCount).toBeGreaterThan(0); } } await page.screenshot({ path: 'test-results/smoke_f2_hands.png' }); }); // ── F3: Pipeline 执行 ───────────────────────────────────────────── test('F3: Pipeline — 模板列表加载', async ({ page }) => { await waitForAppReady(page); // 查找 Pipeline/Workflow 入口 const workflowBtn = page.locator( 'button[aria-label*="Pipeline"], button[aria-label*="工作流"], [data-testid="workflow"], :text("Pipeline")' ).first(); if (await workflowBtn.isVisible().catch(() => false)) { await workflowBtn.click(); await page.waitForTimeout(2000); } // 通过 Store 检查 Pipeline 状态 const pipelineState = await page.evaluate(() => { const stores = (window as any).__ZCLAW_STORES__; const wf = stores?.workflow?.getState?.(); return { workflowCount: wf?.workflows?.length ?? -1, templates: wf?.templates?.length ?? -1, }; }); console.log(`F3: Pipeline state = ${JSON.stringify(pipelineState)}`); await page.screenshot({ path: 'test-results/smoke_f3_pipeline.png' }); }); // ── F4: 记忆闭环 ────────────────────────────────────────────────── test('F4: 记忆 — 提取器检查+FTS5 索引', async ({ page }) => { await waitForAppReady(page); // 检查记忆相关 Store const memoryState = await page.evaluate(() => { const stores = (window as any).__ZCLAW_STORES__; const memGraph = stores?.memoryGraph?.getState?.(); return { hasMemoryStore: !!stores?.memoryGraph, memoryCount: memGraph?.memories?.length ?? -1, }; }); console.log(`F4: Memory state = ${JSON.stringify(memoryState)}`); // 查找记忆面板 const memoryBtn = page.locator( 'button[aria-label*="记忆"], [data-testid="memory"], :text("记忆")' ).first(); if (await memoryBtn.isVisible().catch(() => false)) { await memoryBtn.click(); await page.waitForTimeout(2000); await page.screenshot({ path: 'test-results/smoke_f4_memory.png' }); } }); // ── F5: 管家路由 ────────────────────────────────────────────────── test('F5: 管家路由 — ButlerRouter 分类检查', async ({ page }) => { await waitForAppReady(page); // 检查管家模式是否激活 const butlerState = await page.evaluate(() => { const stores = (window as any).__ZCLAW_STORES__; const chat = stores?.chat?.getState?.(); return { hasButlerStore: !!stores?.butler, uiMode: chat?.uiMode || stores?.uiMode?.getState?.()?.mode || 'unknown', }; }); console.log(`F5: Butler state = ${JSON.stringify(butlerState)}`); // 发送一个 healthcare 相关消息测试路由 await sendMessage(page, '帮我整理上周的科室会议记录'); // 等待响应 await page.waitForTimeout(5000); // 检查 Butler 分类结果 (通过 Store) const routing = await page.evaluate(() => { const stores = (window as any).__ZCLAW_STORES__; const butler = stores?.butler?.getState?.(); return { lastDomain: butler?.lastDomain || butler?.domain || null, lastConfidence: butler?.lastConfidence || null, }; }); console.log(`F5: Butler routing = ${JSON.stringify(routing)}`); await page.screenshot({ path: 'test-results/smoke_f5_butler.png' }); }); // ── F6: 技能发现 ────────────────────────────────────────────────── test('F6: 技能 — 列表加载→搜索', async ({ page }) => { await waitForAppReady(page); // 查找技能面板入口 const skillBtn = page.locator( 'button[aria-label*="技能"], [data-testid="skills"], :text("技能")' ).first(); if (await skillBtn.isVisible().catch(() => false)) { await skillBtn.click(); await page.waitForTimeout(2000); } // 通过 Store 或 Tauri invoke 检查技能列表 const skillState = await page.evaluate(async () => { const stores = (window as any).__ZCLAW_STORES__; // 方法 1: Store if (stores?.skill?.getState?.()?.skills) { return { source: 'store', count: stores.skill.getState().skills.length }; } // 方法 2: Tauri invoke if ((window as any).__TAURI__) { try { const { invoke } = (window as any).__TAURI__.core || (window as any).__TAURI__; const skills = await invoke('list_skills'); return { source: 'tauri', count: Array.isArray(skills) ? skills.length : 0 }; } catch (e) { return { source: 'tauri_error', error: String(e) }; } } return { source: 'none', count: 0 }; }); console.log(`F6: Skill state = ${JSON.stringify(skillState)}`); await page.screenshot({ path: 'test-results/smoke_f6_skills.png' }); }); // Helper for F5/F6: send message through chat async function sendMessage(page: Page, message: string) { const selectors = [ 'textarea[placeholder*="消息"]', 'textarea[placeholder*="输入"]', 'textarea', '.chat-input textarea', ]; for (const sel of selectors) { const el = page.locator(sel).first(); if (await el.isVisible().catch(() => false)) { await el.fill(message); await el.press('Enter'); return; } } console.log('sendMessage: no chat input found'); }