release(v0.2.0): streaming, MCP protocol, Browser Hand, security enhancements
## 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>
This commit is contained in:
347
desktop/tests/e2e/specs/tauri-core.spec.ts
Normal file
347
desktop/tests/e2e/specs/tauri-core.spec.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user