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:
iven
2026-03-24 03:24:24 +08:00
parent e49ba4460b
commit 3ff08faa56
78 changed files with 29575 additions and 1682 deletions

View 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');
});
});