test: add 30 smoke tests for break detection across SaaS/Admin/Desktop
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

Layer 1 断裂探测矩阵:
- S1-S6: SaaS API 端到端 (auth/lockout/relay/permissions/billing/knowledge)
- A1-A6: Admin V2 连通性 (login/dashboard/CRUD/knowledge/roles/models)
- D1-D6: Desktop 聊天流 (gateway/kernel/relay/cancel/offline/error)
- F1-F6: Desktop 功能闭环 (agent/hands/pipeline/memory/butler/skills)
- X1-X6: 跨系统闭环 (provider→desktop/disabled user/knowledge/stats/totp/billing)

Also adds: admin-v2 Playwright config, updated spec doc with cross-reference

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-04-10 09:47:35 +08:00
parent ffa137eff6
commit 2e70e1a3f8
7 changed files with 1390 additions and 0 deletions

View File

@@ -0,0 +1,326 @@
/**
* Smoke Tests — Desktop 聊天流断裂探测
*
* 6 个冒烟测试验证 Desktop 聊天功能的完整连通性。
* 覆盖 3 种聊天模式 + 流取消 + 离线恢复 + 错误处理。
*
* 前提条件:
* - Desktop App 运行在 http://localhost:1420 (pnpm tauri dev)
* - 后端服务可用 (SaaS 或 Kernel)
*
* 运行: cd desktop && npx playwright test smoke_chat
*/
import { test, expect, type Page } from '@playwright/test';
test.setTimeout(120000);
// Helper: 等待应用就绪
async function waitForAppReady(page: Page) {
await page.goto('/');
await page.waitForLoadState('networkidle');
await page.waitForSelector('.h-screen', { timeout: 15000 });
}
// Helper: 查找聊天输入框
async function findChatInput(page: Page) {
// 多种选择器尝试,兼容不同 UI 状态
const selectors = [
'textarea[placeholder*="消息"]',
'textarea[placeholder*="输入"]',
'textarea[placeholder*="chat"]',
'textarea',
'input[type="text"]',
'.chat-input textarea',
'[data-testid="chat-input"]',
];
for (const sel of selectors) {
const el = page.locator(sel).first();
if (await el.isVisible().catch(() => false)) {
return el;
}
}
// 截图帮助调试
await page.screenshot({ path: 'test-results/smoke_chat_input_not_found.png' });
throw new Error('Chat input not found — screenshot saved');
}
// Helper: 发送消息
async function sendMessage(page: Page, message: string) {
const input = await findChatInput(page);
await input.fill(message);
// 按回车发送或点击发送按钮
await input.press('Enter').catch(async () => {
const sendBtn = page.locator('button[aria-label*="发送"], button[aria-label*="send"], button:has-text("发送")').first();
if (await sendBtn.isVisible().catch(() => false)) {
await sendBtn.click();
}
});
}
// ── D1: Gateway 模式聊天 ──────────────────────────────────────────
test('D1: Gateway 模式 — 发送消息→接收响应', async ({ page }) => {
await waitForAppReady(page);
// 检查连接状态
const connectionStatus = page.locator('[data-testid="connection-status"], .connection-status, [aria-label*="连接"]');
const isConnected = await connectionStatus.isVisible().catch(() => false);
if (!isConnected) {
// 尝试通过 Store 检查连接状态
const state = await page.evaluate(() => {
const stores = (window as any).__ZCLAW_STORES__;
return stores ? { hasStores: true } : { hasStores: false };
});
console.log('Store state:', JSON.stringify(state));
}
// 发送测试消息
await sendMessage(page, '你好,这是一个冒烟测试');
// 等待响应 (流式消息或错误)
// 检查是否有新的消息气泡出现
const messageBubble = page.locator('.message-bubble, [class*="message"], [data-role="assistant"]').last();
const gotResponse = await messageBubble.waitFor({ state: 'visible', timeout: 30000 }).catch(() => false);
if (!gotResponse) {
await page.screenshot({ path: 'test-results/smoke_d1_no_response.png' });
}
// 记录结果但不硬性失败 — Smoke 的目的是探测
console.log(`D1 result: ${gotResponse ? 'PASS — response received' : 'BROKEN — no response'}`);
expect(gotResponse).toBeTruthy();
});
// ── D2: Kernel 模式 (Tauri IPC) ───────────────────────────────────
test('D2: Kernel 模式 — Tauri invoke 检查', async ({ page }) => {
await waitForAppReady(page);
// 检查 Tauri 是否可用
const tauriAvailable = await page.evaluate(() => {
return !!(window as any).__TAURI__;
});
if (!tauriAvailable) {
console.log('D2 SKIP: Not running in Tauri context (browser mode)');
test.skip();
return;
}
// 检查 connectionStore 模式
const connectionMode = await page.evaluate(() => {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.connection) {
const state = stores.connection.getState?.();
return state?.connectionMode || state?.mode || 'unknown';
}
return 'no_store';
});
console.log(`Connection mode: ${connectionMode}`);
// 尝试通过 Kernel 发送消息 (需要 Kernel 模式)
if (connectionMode === 'tauri' || connectionMode === 'unknown') {
await sendMessage(page, 'Kernel 模式冒烟测试');
// 等待流式响应
const gotChunk = await page.evaluate(() => {
return new Promise<boolean>((resolve) => {
const stores = (window as any).__ZCLAW_STORES__;
if (!stores?.stream) {
resolve(false);
return;
}
const unsub = stores.stream.subscribe((state: any) => {
if (state.chunks?.length > 0) {
unsub();
resolve(true);
}
});
// 超时 15s
setTimeout(() => { unsub(); resolve(false); }, 15000);
});
});
console.log(`D2 result: ${gotChunk ? 'PASS' : 'BROKEN'}`);
}
});
// ── D3: SaaS Relay 模式 ───────────────────────────────────────────
test('D3: SaaS Relay — SSE 流式检查', async ({ page }) => {
await waitForAppReady(page);
// 检查 SaaS 登录状态
const saasState = await page.evaluate(() => {
const stores = (window as any).__ZCLAW_STORES__;
if (!stores) return { hasStores: false };
const saas = stores.saas?.getState?.();
return {
hasStores: true,
isAuthenticated: saas?.isAuthenticated || false,
connectionMode: stores.connection?.getState?.()?.connectionMode || 'unknown',
};
});
if (saasState.connectionMode !== 'saas' && saasState.connectionMode !== 'browser') {
console.log(`D3 SKIP: Not in SaaS mode (current: ${saasState.connectionMode})`);
test.skip();
return;
}
// 发送消息
await sendMessage(page, 'SaaS Relay 冒烟测试');
// 检查是否有 relay 请求发出
const relayEvents = await page.evaluate(() => {
return new Promise<boolean>((resolve) => {
const stores = (window as any).__ZCLAW_STORES__;
if (!stores?.stream) { resolve(false); return; }
const unsub = stores.stream.subscribe((state: any) => {
if (state.isStreaming || state.chunks?.length > 0) {
unsub();
resolve(true);
}
});
setTimeout(() => { unsub(); resolve(false); }, 20000);
});
});
if (!relayEvents) {
await page.screenshot({ path: 'test-results/smoke_d3_no_relay.png' });
}
console.log(`D3 result: ${relayEvents ? 'PASS' : 'BROKEN'}`);
expect(relayEvents).toBeTruthy();
});
// ── D4: 流取消 ────────────────────────────────────────────────────
test('D4: 流取消 — 发送→取消→停止', async ({ page }) => {
await waitForAppReady(page);
// 发送一条长回复请求
await sendMessage(page, '请详细解释量子计算的基本原理,包括量子比特、叠加态和纠缠');
// 等待流式开始
await page.waitForTimeout(1000);
// 查找取消按钮
const cancelBtn = page.locator(
'button[aria-label*="取消"], button[aria-label*="停止"], button:has-text("停止"), [data-testid="cancel-stream"]'
).first();
if (await cancelBtn.isVisible().catch(() => false)) {
await cancelBtn.click();
// 验证流状态变为 cancelled/stopped
const streamState = await page.evaluate(() => {
const stores = (window as any).__ZCLAW_STORES__;
return stores?.stream?.getState?.()?.isStreaming ?? 'no_store';
});
console.log(`D4 result: stream cancelled, isStreaming=${streamState}`);
expect(streamState).toBe(false);
} else {
// 如果没有取消按钮,尝试通过 Store 取消
const cancelled = await page.evaluate(() => {
const stores = (window as any).__ZCLAW_STORES__;
if (stores?.stream?.getState?.()?.cancelStream) {
stores.stream.getState().cancelStream();
return true;
}
return false;
});
console.log(`D4 result: ${cancelled ? 'cancelled via Store' : 'no cancel mechanism found'}`);
}
});
// ── D5: 离线→在线 ─────────────────────────────────────────────────
test('D5: 离线队列 — 断网→发消息→恢复→重放', async ({ page, context }) => {
await waitForAppReady(page);
// 模拟离线
await context.setOffline(true);
// 发送消息 (应该被排队)
await sendMessage(page, '离线测试消息');
// 检查 offlineStore 是否有队列
const offlineState = await page.evaluate(() => {
const stores = (window as any).__ZCLAW_STORES__;
// 没有 offlineStore 暴露在 window 上,通过 connection 状态检查
return {
hasOfflineStore: !!stores?.offline,
isOnline: stores?.connection?.getState?.()?.isOnline ?? 'unknown',
};
});
console.log('Offline state:', JSON.stringify(offlineState));
// 恢复网络
await context.setOffline(false);
// 等待重连
await page.waitForTimeout(3000);
// 检查消息是否被重放 (或至少没有被丢失)
const afterReconnect = await page.evaluate(() => {
const stores = (window as any).__ZCLAW_STORES__;
return {
isOnline: stores?.connection?.getState?.()?.isOnline ?? 'unknown',
};
});
console.log('After reconnect:', JSON.stringify(afterReconnect));
});
// ── D6: 错误恢复 ──────────────────────────────────────────────────
test('D6: 错误恢复 — 无效模型→错误提示→恢复', async ({ page }) => {
await waitForAppReady(page);
// 尝试通过 Store 设置无效模型
const errorTriggered = await page.evaluate(() => {
const stores = (window as any).__ZCLAW_STORES__;
if (!stores?.chat) return false;
// 有些 Store 允许直接设置 model
const state = stores.chat.getState?.();
if (state?.setSelectedModel) {
state.setSelectedModel('nonexistent-model-xyz');
return true;
}
return false;
});
if (errorTriggered) {
// 尝试发送消息,期望看到错误
await sendMessage(page, '测试错误恢复');
// 等待错误 UI 或错误消息
await page.waitForTimeout(5000);
// 检查是否有错误提示
const errorVisible = await page.locator(
'.error-message, [class*="error"], [role="alert"], .ant-message-error'
).first().isVisible().catch(() => false);
// 恢复有效模型
await page.evaluate(() => {
const stores = (window as any).__ZCLAW_STORES__;
const state = stores?.chat?.getState?.();
if (state?.setSelectedModel && state?.availableModels?.length > 0) {
state.setSelectedModel(state.availableModels[0]);
}
});
console.log(`D6 result: error=${errorVisible}, recovery attempted`);
} else {
console.log('D6 SKIP: Cannot trigger model error via Store');
}
});

View File

@@ -0,0 +1,240 @@
/**
* Smoke Tests — 跨系统闭环断裂探测
*
* 6 个冒烟测试验证 Admin→SaaS→Desktop 的跨系统闭环。
* 同时操作 Admin API 和 Desktop UI验证数据一致性。
*
* 前提条件:
* - SaaS Server: http://localhost:8080
* - Desktop App: http://localhost:1420
* - Admin V2: http://localhost:5173
* - 真实 LLM API Key (部分测试需要)
*
* 运行: cd desktop && npx playwright test smoke_cross
*/
import { test, expect, type Page } from '@playwright/test';
test.setTimeout(180000);
const SaaS_BASE = 'http://localhost:8080/api/v1';
const ADMIN_USER = 'testadmin';
const ADMIN_PASS = 'Admin123456';
async function saasLogin(page: Page): Promise<string> {
const res = await page.request.post(`${SaaS_BASE}/auth/login`, {
data: { username: ADMIN_USER, password: ADMIN_PASS },
});
expect(res.ok()).toBeTruthy();
const json = await res.json();
return json.token;
}
async function waitForDesktopReady(page: Page) {
await page.goto('/');
await page.waitForLoadState('networkidle');
await page.waitForSelector('.h-screen', { timeout: 15000 });
}
// ── X1: Admin 配 Provider → Desktop 可用 ──────────────────────────
test('X1: Admin 创建 Provider → Desktop 模型列表包含', async ({ page }) => {
const token = await saasLogin(page);
const providerName = `cross_provider_${Date.now()}`;
// Step 1: 通过 SaaS API 创建 Provider
const provRes = await page.request.post(`${SaaS_BASE}/providers`, {
headers: { Authorization: `Bearer ${token}` },
data: {
name: providerName,
provider_type: 'openai',
base_url: 'https://api.cross.test/v1',
enabled: true,
},
});
expect(provRes.ok()).toBeTruthy();
const provJson = await provRes.json();
const providerId = provJson.id;
// Step 2: 创建 Model
const modelRes = await page.request.post(`${SaaS_BASE}/models`, {
headers: { Authorization: `Bearer ${token}` },
data: {
name: `cross_model_${Date.now()}`,
provider_id: providerId,
model_id: 'test-model',
enabled: true,
},
});
expect(modelRes.ok()).toBeTruthy();
// Step 3: 等待缓存刷新 (最多 60s)
console.log('Waiting for cache refresh...');
await page.waitForTimeout(5000);
// Step 4: 在 Desktop 检查模型列表
await waitForDesktopReady(page);
// 通过 SaaS API 验证模型可被 relay 获取
const relayModels = await page.request.get(`${SaaS_BASE}/relay/models`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(relayModels.ok()).toBeTruthy();
const modelsJson = await relayModels.json();
const models = modelsJson.models || modelsJson.data || modelsJson;
const found = Array.isArray(models) && models.some(
(m: any) => m.id === providerId || m.name?.includes('cross_model')
);
console.log(`X1: Model found in relay list: ${found}`);
if (!found) {
console.log('Available models:', JSON.stringify(models).slice(0, 500));
}
});
// ── X2: Admin 改权限 → Desktop 受限 ───────────────────────────────
test('X2: Admin 禁用 user → Desktop 请求失败', async ({ page }) => {
const token = await saasLogin(page);
// 创建一个测试用户
const regRes = await page.request.post(`${SaaS_BASE}/auth/register`, {
data: {
username: `cross_user_${Date.now()}`,
email: `cross_${Date.now()}@test.io`,
password: 'TestPassword123',
},
});
expect(regRes.ok()).toBeTruthy();
const regJson = await regRes.json();
const userId = regJson.account?.id || regJson.id;
// 禁用该用户
if (userId) {
const disableRes = await page.request.patch(`${SaaS_BASE}/accounts/${userId}`, {
headers: { Authorization: `Bearer ${token}` },
data: { status: 'disabled' },
});
console.log(`Disable user: ${disableRes.status()}`);
// 验证被禁用的用户无法登录
const loginRes = await page.request.post(`${SaaS_BASE}/auth/login`, {
data: {
username: regJson.username || `cross_user_${Date.now()}`,
password: 'TestPassword123',
},
});
console.log(`Disabled user login: ${loginRes.status()} (expected 401/403)`);
expect([401, 403]).toContain(loginRes.status());
}
});
// ── X3: Admin 创建知识 → Desktop 检索 ──────────────────────────────
test('X3: Admin 创建知识条目 → SaaS 搜索可找到', async ({ page }) => {
const token = await saasLogin(page);
const uniqueContent = `跨系统测试知识_${Date.now()}_唯一标识`;
// Step 1: 创建分类
const catRes = await page.request.post(`${SaaS_BASE}/knowledge/categories`, {
headers: { Authorization: `Bearer ${token}` },
data: { name: `cross_cat_${Date.now()}`, description: 'Cross-system test' },
});
expect(catRes.ok()).toBeTruthy();
const catJson = await catRes.json();
// Step 2: 创建知识条目
const itemRes = await page.request.post(`${SaaS_BASE}/knowledge/items`, {
headers: { Authorization: `Bearer ${token}` },
data: {
title: uniqueContent,
content: '这是跨系统知识检索测试的内容,包含特定关键词。',
category_id: catJson.id,
tags: ['cross-system', 'test'],
},
});
expect(itemRes.ok()).toBeTruthy();
// Step 3: 通过 SaaS API 搜索
const searchRes = await page.request.post(`${SaaS_BASE}/knowledge/search`, {
headers: { Authorization: `Bearer ${token}` },
data: { query: uniqueContent, limit: 5 },
});
expect(searchRes.ok()).toBeTruthy();
const searchJson = await searchRes.json();
const items = searchJson.items || searchJson;
const found = Array.isArray(items) && items.some(
(i: any) => i.title === uniqueContent
);
expect(found).toBeTruthy();
console.log(`X3: Knowledge item found via search: ${found}`);
});
// ── X4: Desktop 聊天 → Admin 统计更新 ─────────────────────────────
test('X4: Dashboard 统计 — API 验证结构', async ({ page }) => {
const token = await saasLogin(page);
// Step 1: 获取初始统计
const statsRes = await page.request.get(`${SaaS_BASE}/dashboard/stats`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(statsRes.ok()).toBeTruthy();
const stats = await statsRes.json();
// 验证统计结构
expect(stats).toHaveProperty('total_accounts');
expect(stats).toHaveProperty('active_providers');
expect(stats).toHaveProperty('active_models');
expect(stats).toHaveProperty('tasks_today');
console.log(`X4: Dashboard stats = ${JSON.stringify(stats).slice(0, 300)}`);
});
// ── X5: TOTP 全流程 ──────────────────────────────────────────────
test('X5: TOTP 设置→验证→登录', async ({ page }) => {
const token = await saasLogin(page);
// Step 1: 设置 TOTP
const setupRes = await page.request.post(`${SaaS_BASE}/auth/totp/setup`, {
headers: { Authorization: `Bearer ${token}` },
});
const setupStatus = setupRes.status();
if (setupStatus === 200 || setupStatus === 201) {
const setupJson = await setupRes.json();
console.log(`X5: TOTP setup returned secret (encrypted): ${setupJson.secret ? 'yes' : 'no'}`);
expect(setupJson.secret || setupJson.totp_secret).toBeTruthy();
} else {
console.log(`X5: TOTP setup returned ${setupStatus} (may need verification endpoint)`);
const body = await setupRes.text();
console.log(`Response: ${body.slice(0, 200)}`);
}
});
// ── X6: 计费一致性 ───────────────────────────────────────────────
test('X6: 计费一致性 — billing_usage 查询', async ({ page }) => {
const token = await saasLogin(page);
// 查询计费用量
const usageRes = await page.request.get(`${SaaS_BASE}/billing/usage`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(usageRes.ok()).toBeTruthy();
const usageJson = await usageRes.json();
// 验证结构
console.log(`X6: Billing usage = ${JSON.stringify(usageJson).slice(0, 300)}`);
// 查询计划
const plansRes = await page.request.get(`${SaaS_BASE}/billing/plans`, {
headers: { Authorization: `Bearer ${token}` },
});
expect(plansRes.ok()).toBeTruthy();
const plansJson = await plansRes.json();
console.log(`X6: Billing plans count = ${Array.isArray(plansJson) ? plansJson.length : 'object'}`);
});

View File

@@ -0,0 +1,260 @@
/**
* 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.waitForLoadState('networkidle');
await page.waitForSelector('.h-screen', { timeout: 15000 });
}
// ── 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');
}