## Major Features ### Streaming Response System - Implement LlmDriver trait with `stream()` method returning async Stream - Add SSE parsing for Anthropic and OpenAI API streaming - Integrate Tauri event system for frontend streaming (`stream:chunk` events) - Add StreamChunk types: Delta, ToolStart, ToolEnd, Complete, Error ### MCP Protocol Implementation - Add MCP JSON-RPC 2.0 types (mcp_types.rs) - Implement stdio-based MCP transport (mcp_transport.rs) - Support tool discovery, execution, and resource operations ### Browser Hand Implementation - Complete browser automation with Playwright-style actions - Support Navigate, Click, Type, Scrape, Screenshot, Wait actions - Add educational Hands: Whiteboard, Slideshow, Speech, Quiz ### Security Enhancements - Implement command whitelist/blacklist for shell_exec tool - Add SSRF protection with private IP blocking - Create security.toml configuration file ## Test Improvements - Fix test import paths (security-utils, setup) - Fix vi.mock hoisting issues with vi.hoisted() - Update test expectations for validateUrl and sanitizeFilename - Add getUnsupportedLocalGatewayStatus mock ## Documentation Updates - Update architecture documentation - Improve configuration reference - Add quick-start guide updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
348 lines
11 KiB
TypeScript
348 lines
11 KiB
TypeScript
/**
|
||
* ZCLAW Tauri 模式 E2E 测试
|
||
*
|
||
* 测试 Tauri 桌面应用特有的功能和集成
|
||
* 验证 Kernel Client、Rust 后端和 Native 功能的完整性
|
||
*/
|
||
|
||
import { test, expect, Page } from '@playwright/test';
|
||
|
||
test.setTimeout(120000);
|
||
|
||
async function waitForAppReady(page: Page) {
|
||
await page.waitForLoadState('domcontentloaded');
|
||
await page.waitForTimeout(2000);
|
||
}
|
||
|
||
async function takeScreenshot(page: Page, name: string) {
|
||
await page.screenshot({
|
||
path: `test-results/tauri-artifacts/${name}.png`,
|
||
fullPage: true,
|
||
});
|
||
}
|
||
|
||
test.describe('ZCLAW Tauri 模式核心功能', () => {
|
||
test.beforeEach(async ({ page }) => {
|
||
await page.goto('/');
|
||
await waitForAppReady(page);
|
||
});
|
||
|
||
test.describe('1. Tauri 运行时检测', () => {
|
||
test('应该检测到 Tauri 运行时环境', async ({ page }) => {
|
||
const isTauri = await page.evaluate(() => {
|
||
return '__TAURI_INTERNALS__' in window;
|
||
});
|
||
|
||
console.log('[Tauri Check] isTauriRuntime:', isTauri);
|
||
|
||
if (!isTauri) {
|
||
console.warn('[Tauri Check] Warning: Not running in Tauri environment');
|
||
console.warn('[Tauri Check] Some tests may not work correctly');
|
||
}
|
||
|
||
await takeScreenshot(page, '01-tauri-runtime-check');
|
||
});
|
||
|
||
test('Tauri API 应该可用', async ({ page }) => {
|
||
const tauriAvailable = await page.evaluate(async () => {
|
||
try {
|
||
const { invoke } = await import('@tauri-apps/api/core');
|
||
const result = await invoke('kernel_status');
|
||
return { available: true, result };
|
||
} catch (error) {
|
||
return { available: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
console.log('[Tauri API] Available:', tauriAvailable);
|
||
|
||
if (tauriAvailable.available) {
|
||
console.log('[Tauri API] Kernel status:', tauriAvailable.result);
|
||
} else {
|
||
console.warn('[Tauri API] Not available:', tauriAvailable.error);
|
||
}
|
||
|
||
await takeScreenshot(page, '02-tauri-api-check');
|
||
});
|
||
});
|
||
|
||
test.describe('2. 内核状态验证', () => {
|
||
test('内核初始化状态', async ({ page }) => {
|
||
const kernelStatus = await page.evaluate(async () => {
|
||
try {
|
||
const { invoke } = await import('@tauri-apps/api/core');
|
||
const status = await invoke<{
|
||
initialized: boolean;
|
||
agentCount: number;
|
||
databaseUrl: string | null;
|
||
defaultProvider: string | null;
|
||
defaultModel: string | null;
|
||
}>('kernel_status');
|
||
|
||
return {
|
||
success: true,
|
||
initialized: status.initialized,
|
||
agentCount: status.agentCount,
|
||
provider: status.defaultProvider,
|
||
model: status.defaultModel,
|
||
};
|
||
} catch (error) {
|
||
return {
|
||
success: false,
|
||
error: String(error),
|
||
};
|
||
}
|
||
});
|
||
|
||
console.log('[Kernel Status]', kernelStatus);
|
||
|
||
if (kernelStatus.success) {
|
||
console.log('[Kernel] Initialized:', kernelStatus.initialized);
|
||
console.log('[Kernel] Agents:', kernelStatus.agentCount);
|
||
console.log('[Kernel] Provider:', kernelStatus.provider);
|
||
console.log('[Kernel] Model:', kernelStatus.model);
|
||
}
|
||
|
||
await takeScreenshot(page, '03-kernel-status');
|
||
});
|
||
|
||
test('Agent 列表获取', async ({ page }) => {
|
||
const agents = await page.evaluate(async () => {
|
||
try {
|
||
const { invoke } = await import('@tauri-apps/api/core');
|
||
const agentList = await invoke<Array<{
|
||
id: string;
|
||
name: string;
|
||
state: string;
|
||
model?: string;
|
||
provider?: string;
|
||
}>>('agent_list');
|
||
|
||
return { success: true, agents: agentList };
|
||
} catch (error) {
|
||
return { success: false, error: String(error) };
|
||
}
|
||
});
|
||
|
||
console.log('[Agent List]', agents);
|
||
|
||
if (agents.success) {
|
||
console.log('[Agents] Count:', agents.agents?.length);
|
||
agents.agents?.forEach((agent, i) => {
|
||
console.log(`[Agent ${i + 1}]`, agent);
|
||
});
|
||
}
|
||
|
||
await takeScreenshot(page, '04-agent-list');
|
||
});
|
||
});
|
||
|
||
test.describe('3. 连接状态', () => {
|
||
test('应用应该正确显示连接状态', async ({ page }) => {
|
||
await page.waitForTimeout(3000);
|
||
|
||
const connectionState = await page.evaluate(() => {
|
||
const statusElements = document.querySelectorAll('[class*="status"], [class*="connection"]');
|
||
return {
|
||
foundElements: statusElements.length,
|
||
texts: Array.from(statusElements).map((el) => el.textContent?.trim()).filter(Boolean),
|
||
};
|
||
});
|
||
|
||
console.log('[Connection State]', connectionState);
|
||
|
||
const pageText = await page.textContent('body');
|
||
console.log('[Page Text]', pageText?.substring(0, 500));
|
||
|
||
await takeScreenshot(page, '05-connection-state');
|
||
});
|
||
|
||
test('设置按钮应该可用', async ({ page }) => {
|
||
const settingsBtn = page.locator('button').filter({ hasText: /设置|Settings|⚙/i });
|
||
|
||
if (await settingsBtn.isVisible()) {
|
||
await settingsBtn.click();
|
||
await page.waitForTimeout(1000);
|
||
await takeScreenshot(page, '06-settings-access');
|
||
} else {
|
||
console.log('[Settings] Button not visible');
|
||
}
|
||
});
|
||
});
|
||
|
||
test.describe('4. UI 布局验证', () => {
|
||
test('主布局应该正确渲染', async ({ page }) => {
|
||
const layout = await page.evaluate(() => {
|
||
const app = document.querySelector('.h-screen');
|
||
const sidebar = document.querySelector('aside');
|
||
const main = document.querySelector('main');
|
||
|
||
return {
|
||
hasApp: !!app,
|
||
hasSidebar: !!sidebar,
|
||
hasMain: !!main,
|
||
appClasses: app?.className,
|
||
};
|
||
});
|
||
|
||
console.log('[Layout]', layout);
|
||
expect(layout.hasApp).toBe(true);
|
||
expect(layout.hasSidebar).toBe(true);
|
||
expect(layout.hasMain).toBe(true);
|
||
|
||
await takeScreenshot(page, '07-layout');
|
||
});
|
||
|
||
test('侧边栏导航应该存在', async ({ page }) => {
|
||
const navButtons = await page.locator('aside button').count();
|
||
console.log('[Navigation] Button count:', navButtons);
|
||
|
||
expect(navButtons).toBeGreaterThan(0);
|
||
|
||
await takeScreenshot(page, '08-navigation');
|
||
});
|
||
});
|
||
|
||
test.describe('5. 聊天功能 (Tauri 模式)', () => {
|
||
test('聊天输入框应该可用', async ({ page }) => {
|
||
const chatInput = page.locator('textarea').first();
|
||
|
||
if (await chatInput.isVisible()) {
|
||
await chatInput.fill('你好,ZCLAW');
|
||
const value = await chatInput.inputValue();
|
||
console.log('[Chat Input] Value:', value);
|
||
expect(value).toBe('你好,ZCLAW');
|
||
} else {
|
||
console.log('[Chat Input] Not visible - may need connection');
|
||
}
|
||
|
||
await takeScreenshot(page, '09-chat-input');
|
||
});
|
||
|
||
test('模型选择器应该可用', async ({ page }) => {
|
||
const modelSelector = page.locator('button').filter({ hasText: /模型|Model|选择/i });
|
||
|
||
if (await modelSelector.isVisible()) {
|
||
await modelSelector.click();
|
||
await page.waitForTimeout(500);
|
||
console.log('[Model Selector] Clicked');
|
||
} else {
|
||
console.log('[Model Selector] Not visible');
|
||
}
|
||
|
||
await takeScreenshot(page, '10-model-selector');
|
||
});
|
||
});
|
||
|
||
test.describe('6. 设置页面 (Tauri 模式)', () => {
|
||
test('设置页面应该能打开', async ({ page }) => {
|
||
const settingsBtn = page.getByRole('button', { name: /设置|Settings/i }).first();
|
||
|
||
if (await settingsBtn.isVisible()) {
|
||
await settingsBtn.click();
|
||
await page.waitForTimeout(1000);
|
||
|
||
const settingsContent = await page.locator('[class*="settings"]').count();
|
||
console.log('[Settings] Content elements:', settingsContent);
|
||
|
||
expect(settingsContent).toBeGreaterThan(0);
|
||
} else {
|
||
console.log('[Settings] Button not found');
|
||
}
|
||
|
||
await takeScreenshot(page, '11-settings-page');
|
||
});
|
||
|
||
test('通用设置标签应该可见', async ({ page }) => {
|
||
await page.getByRole('button', { name: /设置|Settings/i }).first().click();
|
||
await page.waitForTimeout(500);
|
||
|
||
const tabs = await page.getByRole('tab').count();
|
||
console.log('[Settings Tabs] Count:', tabs);
|
||
|
||
await takeScreenshot(page, '12-settings-tabs');
|
||
});
|
||
});
|
||
|
||
test.describe('7. 控制台日志检查', () => {
|
||
test('应该没有严重 JavaScript 错误', async ({ page }) => {
|
||
const errors: string[] = [];
|
||
|
||
page.on('pageerror', (error) => {
|
||
errors.push(error.message);
|
||
});
|
||
|
||
page.on('console', (msg) => {
|
||
if (msg.type() === 'error') {
|
||
errors.push(msg.text());
|
||
}
|
||
});
|
||
|
||
await page.waitForTimeout(3000);
|
||
|
||
const criticalErrors = errors.filter(
|
||
(e) =>
|
||
!e.includes('Warning') &&
|
||
!e.includes('DevTools') &&
|
||
!e.includes('extension') &&
|
||
!e.includes('favicon')
|
||
);
|
||
|
||
console.log('[Console Errors]', criticalErrors.length);
|
||
criticalErrors.forEach((e) => console.log(' -', e.substring(0, 200)));
|
||
|
||
await takeScreenshot(page, '13-console-errors');
|
||
});
|
||
|
||
test('Tauri 特定日志应该存在', async ({ page }) => {
|
||
const logs: string[] = [];
|
||
|
||
page.on('console', (msg) => {
|
||
if (msg.type() === 'log' || msg.type() === 'info') {
|
||
const text = msg.text();
|
||
if (text.includes('Tauri') || text.includes('Kernel') || text.includes('tauri')) {
|
||
logs.push(text);
|
||
}
|
||
}
|
||
});
|
||
|
||
await page.waitForTimeout(2000);
|
||
|
||
console.log('[Tauri Logs]', logs.length);
|
||
logs.forEach((log) => console.log(' -', log.substring(0, 200)));
|
||
|
||
await takeScreenshot(page, '14-tauri-logs');
|
||
});
|
||
});
|
||
});
|
||
|
||
test.describe('ZCLAW Tauri 设置页面测试', () => {
|
||
test.beforeEach(async ({ page }) => {
|
||
await page.goto('/');
|
||
await page.waitForLoadState('domcontentloaded');
|
||
});
|
||
|
||
test('模型与 API 设置', async ({ page }) => {
|
||
await page.getByRole('button', { name: /设置|Settings/i }).first().click();
|
||
await page.waitForTimeout(1000);
|
||
|
||
const modelSettings = await page.getByText(/模型|Model|API/i).count();
|
||
console.log('[Model Settings] Found:', modelSettings);
|
||
|
||
await takeScreenshot(page, '15-model-settings');
|
||
});
|
||
|
||
test('安全设置', async ({ page }) => {
|
||
await page.getByRole('button', { name: /设置|Settings/i }).first().click();
|
||
await page.waitForTimeout(500);
|
||
|
||
const securityTab = page.getByRole('tab', { name: /安全|Security|Privacy/i });
|
||
if (await securityTab.isVisible()) {
|
||
await securityTab.click();
|
||
await page.waitForTimeout(500);
|
||
}
|
||
|
||
await takeScreenshot(page, '16-security-settings');
|
||
});
|
||
});
|