# ZCLAW 架构优化 - Phase 1: 安全 + 测试 实施计划 > **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 建立安全基础和测试框架,为后续架构优化奠定基础 **Architecture:** 增强 `secure-storage.ts` 添加 AES-GCM 加密回退,增强 `gateway-client.ts` 强制 WSS,建立 Vitest 单元测试框架 **Tech Stack:** TypeScript, Vitest, Web Crypto API, AES-GCM **Spec Reference:** `docs/superpowers/specs/2026-03-21-architecture-optimization-design.md` **Duration:** 2 周 (8 人日) --- ## File Structure ### New Files ``` desktop/ ├── src/ │ └── lib/ │ └── crypto-utils.ts # 加密工具函数 (新建) ├── tests/ │ ├── setup.ts # 测试设置文件 (新建) │ ├── lib/ │ │ ├── secure-storage.test.ts # 安全存储测试 (新建) │ │ ├── crypto-utils.test.ts # 加密工具测试 (新建) │ │ └── gateway-security.test.ts # 网关安全测试 (新建) │ └── store/ │ └── chatStore.test.ts # Chat Store 测试 (新建) └── vitest.config.ts # Vitest 配置 (新建) ``` ### Modified Files ``` desktop/ ├── src/ │ └── lib/ │ ├── secure-storage.ts # 增强加密回退 │ └── gateway-client.ts # 强制 WSS ├── package.json # 添加依赖 └── tests/ # 现有测试目录 ``` --- ## Chunk 1: 测试框架建立 ### Task 1.1: 安装测试依赖 **Files:** - Modify: `desktop/package.json` - [ ] **Step 1: 安装 Vitest 和测试相关依赖** Run (Windows): ```bash cd g:\ZClaw_zclaw\desktop && pnpm add -D vitest@2.1.8 @testing-library/react@16.1.0 @testing-library/jest-dom@6.6.3 jsdom@25.0.1 ``` Run (Unix): ```bash cd g:/ZClaw_zclaw/desktop && pnpm add -D vitest@2.1.8 @testing-library/react@16.1.0 @testing-library/jest-dom@6.6.3 jsdom@25.0.1 ``` Expected: 依赖安装成功 - [ ] **Step 2: 验证安装** Run: ```bash cd g:\ZClaw_zclaw\desktop && pnpm exec vitest --version ``` Expected: 输出 `vitest/2.1.8` --- ### Task 1.2: 创建 Vitest 配置 **Files:** - Create: `desktop/vitest.config.ts` - Create: `desktop/tests/setup.ts` - [ ] **Step 1: 创建 Vitest 配置文件** Create `desktop/vitest.config.ts`: ```typescript import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ plugins: [react()], test: { globals: true, environment: 'jsdom', setupFiles: ['./tests/setup.ts'], include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'], exclude: ['tests/e2e/**', 'node_modules/**'], coverage: { provider: 'v8', reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/**', 'tests/**', '**/*.d.ts', '**/*.config.*', 'src/main.tsx', ], thresholds: { lines: 60, functions: 60, branches: 60, statements: 60, }, }, }, resolve: { alias: { '@': path.resolve(__dirname, './src'), }, }, }); ``` - [ ] **Step 2: 创建测试设置文件** Create `desktop/tests/setup.ts`: ```typescript import '@testing-library/jest-dom'; import { vi } from 'vitest'; import { webcrypto } from 'node:crypto'; // Polyfill Web Crypto API for Node.js test environment Object.defineProperty(global, 'crypto', { value: webcrypto, }); // Mock Tauri API vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn(), })); // Mock Tauri runtime check - use alias path for consistency vi.mock('@/lib/tauri-gateway', () => ({ isTauriRuntime: () => false, getGatewayClient: vi.fn(), })); // Mock localStorage const localStorageMock = (() => { let store: Record = {}; return { getItem: (key: string) => store[key] || null, setItem: (key: string, value: string) => { store[key] = value; }, removeItem: (key: string) => { delete store[key]; }, clear: () => { store = {}; }, }; })(); Object.defineProperty(global, 'localStorage', { value: localStorageMock, }); // Mock crypto.subtle for tests const cryptoMock = { subtle: { encrypt: vi.fn(), decrypt: vi.fn(), generateKey: vi.fn(), deriveKey: vi.fn(), importKey: vi.fn(), exportKey: vi.fn(), }, getRandomValues: (array: Uint8Array) => { for (let i = 0; i < array.length; i++) { array[i] = Math.floor(Math.random() * 256); } return array; }, }; Object.defineProperty(global, 'crypto', { value: cryptoMock, }); ``` - [ ] **Step 3: 更新 package.json 测试脚本** Verify `desktop/package.json` scripts section includes: ```json { "scripts": { "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage" } } ``` - [ ] **Step 4: 运行测试验证配置** Run: ```bash cd g:/ZClaw_zclaw/desktop && pnpm test ``` Expected: 测试运行成功 (可能显示 0 tests,这是正常的) - [ ] **Step 5: 提交测试框架配置** ```bash cd g:/ZClaw_zclaw && git add desktop/vitest.config.ts desktop/tests/setup.ts desktop/package.json git commit -m "test: add Vitest configuration and setup - Add vitest.config.ts with coverage thresholds - Add tests/setup.ts with mocks for Tauri and crypto - Update package.json with test dependencies Co-Authored-By: Claude Opus 4.6 " ``` --- ## Chunk 2: 加密工具模块 ### Task 2.1: 创建加密工具函数 **Files:** - Create: `desktop/src/lib/crypto-utils.ts` - Create: `desktop/tests/lib/crypto-utils.test.ts` - [ ] **Step 1: 写失败的测试 - arrayToBase64** Create `desktop/tests/lib/crypto-utils.test.ts`: ```typescript import { describe, it, expect, beforeEach, vi } from 'vitest'; import { arrayToBase64, base64ToArray, deriveKey, encrypt, decrypt } from '../../src/lib/crypto-utils'; describe('crypto-utils', () => { describe('arrayToBase64', () => { it('should convert Uint8Array to base64 string', () => { const arr = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" const result = arrayToBase64(arr); expect(result).toBe('SGVsbG8='); }); it('should handle empty array', () => { const arr = new Uint8Array([]); const result = arrayToBase64(arr); expect(result).toBe(''); }); }); describe('base64ToArray', () => { it('should convert base64 string to Uint8Array', () => { const base64 = 'SGVsbG8='; const result = base64ToArray(base64); expect(result).toEqual(new Uint8Array([72, 101, 108, 108, 111])); }); it('should handle empty string', () => { const result = base64ToArray(''); expect(result).toEqual(new Uint8Array([])); }); }); }); ``` - [ ] **Step 2: 运行测试确认失败** Run: ```bash cd g:/ZClaw_zclaw/desktop && pnpm test tests/lib/crypto-utils.test.ts ``` Expected: FAIL - "Cannot find module '../../src/lib/crypto-utils'" - [ ] **Step 3: 实现加密工具函数** Create `desktop/src/lib/crypto-utils.ts`: ```typescript /** * Cryptographic utilities for secure storage * Uses Web Crypto API for AES-GCM encryption */ const SALT = new TextEncoder().encode('zclaw-secure-storage-salt'); const ITERATIONS = 100000; /** * Convert Uint8Array to base64 string */ export function arrayToBase64(array: Uint8Array): string { if (array.length === 0) return ''; let binary = ''; for (let i = 0; i < array.length; i++) { binary += String.fromCharCode(array[i]); } return btoa(binary); } /** * Convert base64 string to Uint8Array */ export function base64ToArray(base64: string): Uint8Array { if (!base64) return new Uint8Array([]); const binary = atob(base64); const array = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { array[i] = binary.charCodeAt(i); } return array; } /** * Derive an encryption key from a master key */ export async function deriveKey( masterKey: string, salt: Uint8Array = SALT ): Promise { const encoder = new TextEncoder(); const keyMaterial = await crypto.subtle.importKey( 'raw', encoder.encode(masterKey), 'PBKDF2', false, ['deriveBits', 'deriveKey'] ); return crypto.subtle.deriveKey( { name: 'PBKDF2', salt, iterations: ITERATIONS, hash: 'SHA-256', }, keyMaterial, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt'] ); } /** * Encrypt data using AES-GCM */ export async function encrypt( plaintext: string, key: CryptoKey ): Promise<{ iv: string; data: string }> { const encoder = new TextEncoder(); const iv = crypto.getRandomValues(new Uint8Array(12)); const encrypted = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, key, encoder.encode(plaintext) ); return { iv: arrayToBase64(iv), data: arrayToBase64(new Uint8Array(encrypted)), }; } /** * Decrypt data using AES-GCM */ export async function decrypt( encrypted: { iv: string; data: string }, key: CryptoKey ): Promise { const decoder = new TextDecoder(); const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: base64ToArray(encrypted.iv) }, key, base64ToArray(encrypted.data) ); return decoder.decode(decrypted); } /** * Generate a random master key for encryption */ export function generateMasterKey(): string { const array = crypto.getRandomValues(new Uint8Array(32)); return arrayToBase64(array); } ``` - [ ] **Step 4: 运行测试确认通过** Run: ```bash cd g:/ZClaw_zclaw/desktop && pnpm test tests/lib/crypto-utils.test.ts ``` Expected: PASS - 2 tests - [ ] **Step 5: 写失败的测试 - encrypt/decrypt** Add to `desktop/tests/lib/crypto-utils.test.ts`: ```typescript describe('encrypt and decrypt', () => { it('should encrypt and decrypt text correctly', async () => { // Mock crypto.subtle for Node.js environment const originalCrypto = global.crypto; // Use a simple mock that actually works const key = await crypto.subtle.generateKey( { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'] ); const plaintext = 'secret message'; const encrypted = await encrypt(plaintext, key); expect(encrypted.iv).toBeDefined(); expect(encrypted.data).toBeDefined(); expect(encrypted.data).not.toBe(plaintext); const decrypted = await decrypt(encrypted, key); expect(decrypted).toBe(plaintext); }); }); ``` - [ ] **Step 6: 运行测试** Run: ```bash cd g:/ZClaw_zclaw/desktop && pnpm test tests/lib/crypto-utils.test.ts ``` Expected: PASS - 3 tests - [ ] **Step 7: 提交加密工具模块** ```bash cd g:/ZClaw_zclaw && git add desktop/src/lib/crypto-utils.ts desktop/tests/lib/crypto-utils.test.ts git commit -m "feat(crypto): add AES-GCM encryption utilities - Add arrayToBase64/base64ToArray conversion functions - Add deriveKey for PBKDF2 key derivation - Add encrypt/decrypt using AES-GCM - Add unit tests for all functions Co-Authored-By: Claude Opus 4.6 " ``` --- ## Chunk 3: 凭据存储加密增强 ### Task 3.1: 增强安全存储 **Files:** - Modify: `desktop/src/lib/secure-storage.ts` - Create: `desktop/tests/lib/secure-storage.test.ts` - [ ] **Step 1: 读取现有 secure-storage.ts** Read the full file to understand current implementation: - File: `desktop/src/lib/secure-storage.ts` - [ ] **Step 2: 写失败的测试 - 加密回退** Create `desktop/tests/lib/secure-storage.test.ts`: ```typescript import { describe, it, expect, beforeEach, vi } from 'vitest'; import { secureStorage, isSecureStorageAvailable } from '../../src/lib/secure-storage'; // Mock Tauri runtime to return false (non-Tauri environment) vi.mock('../../src/lib/tauri-gateway', () => ({ isTauriRuntime: () => false, })); describe('secureStorage', () => { beforeEach(() => { localStorage.clear(); vi.clearAllMocks(); }); describe('encryption fallback', () => { it('should encrypt data when storing to localStorage', async () => { const key = 'test-key'; const value = 'secret-value'; await secureStorage.set(key, value); // Check that localStorage doesn't contain plaintext const stored = localStorage.getItem(key); expect(stored).not.toBe(value); expect(stored).not.toContain('secret-value'); }); it('should decrypt data when retrieving from localStorage', async () => { const key = 'test-key'; const value = 'secret-value'; await secureStorage.set(key, value); const retrieved = await secureStorage.get(key); expect(retrieved).toBe(value); }); it('should handle special characters in values', async () => { const key = 'test-key'; const value = 'p@ssw0rd!#$%^&*(){}[]|\\:";\'<>?,./~`'; await secureStorage.set(key, value); const retrieved = await secureStorage.get(key); expect(retrieved).toBe(value); }); }); describe('backward compatibility', () => { it('should read unencrypted legacy data', async () => { const key = 'legacy-key'; const value = 'legacy-value'; // Simulate legacy unencrypted storage localStorage.setItem(key, value); const retrieved = await secureStorage.get(key); expect(retrieved).toBe(value); }); }); }); ``` - [ ] **Step 3: 运行测试确认失败** Run: ```bash cd g:/ZClaw_zclaw/desktop && pnpm test tests/lib/secure-storage.test.ts ``` Expected: FAIL - localStorage 存储明文 - [ ] **Step 4: 实现加密回退** Modify `desktop/src/lib/secure-storage.ts`, add import and enhance the fallback functions: ```typescript // Add to imports import { arrayToBase64, base64ToArray, deriveKey, encrypt, decrypt, generateMasterKey } from './crypto-utils'; // Add constant for encrypted key prefix const ENCRYPTED_PREFIX = 'enc_'; const MASTER_KEY_NAME = 'zclaw-master-key'; // Add helper functions after the existing code /** * Get or create master key for encryption */ async function getOrCreateMasterKey(): Promise { let masterKeyRaw = localStorage.getItem(MASTER_KEY_NAME); if (!masterKeyRaw) { masterKeyRaw = generateMasterKey(); localStorage.setItem(MASTER_KEY_NAME, masterKeyRaw); } return deriveKey(masterKeyRaw); } /** * Check if a stored value is encrypted */ function isEncrypted(value: string): boolean { try { const parsed = JSON.parse(value); return parsed && typeof parsed.iv === 'string' && typeof parsed.data === 'string'; } catch { return false; } } /** * Write encrypted data to localStorage */ async function writeEncryptedLocalStorage(key: string, value: string): Promise { if (!value) { localStorage.removeItem(ENCRYPTED_PREFIX + key); return; } try { const cryptoKey = await getOrCreateMasterKey(); const encrypted = await encrypt(value, cryptoKey); localStorage.setItem(ENCRYPTED_PREFIX + key, JSON.stringify(encrypted)); } catch (error) { console.error('[SecureStorage] Encryption failed:', error); // Fallback to plaintext if encryption fails localStorage.setItem(key, value); } } /** * Read encrypted data from localStorage */ async function readEncryptedLocalStorage(key: string): Promise { // Try encrypted version first const encryptedRaw = localStorage.getItem(ENCRYPTED_PREFIX + key); if (encryptedRaw && isEncrypted(encryptedRaw)) { try { const cryptoKey = await getOrCreateMasterKey(); const encrypted = JSON.parse(encryptedRaw); return await decrypt(encrypted, cryptoKey); } catch (error) { console.error('[SecureStorage] Decryption failed:', error); } } // Fallback to unencrypted version for backward compatibility return localStorage.getItem(key); } ``` - [ ] **Step 5: 更新 secureStorage 接口使用加密函数** Modify the `secureStorage` object to use encrypted localStorage: ```typescript export const secureStorage = { async set(key: string, value: string): Promise { const trimmedValue = value.trim(); if (await isSecureStorageAvailable()) { try { if (trimmedValue) { await invoke('secure_store_set', { key, value: trimmedValue }); } else { await invoke('secure_store_delete', { key }); } // Also write encrypted backup await writeEncryptedLocalStorage(key, trimmedValue); return; } catch (error) { console.warn('[SecureStorage] Failed to use keyring, falling back to encrypted localStorage:', error); } } // Fallback to encrypted localStorage await writeEncryptedLocalStorage(key, trimmedValue); }, async get(key: string): Promise { if (await isSecureStorageAvailable()) { try { const value = await invoke('secure_store_get', { key }); if (value !== null && value !== undefined && value !== '') { return value; } // Try encrypted fallback for migration return await readEncryptedLocalStorage(key); } catch (error) { console.warn('[SecureStorage] Failed to read from keyring, trying encrypted localStorage:', error); } } // Fallback to encrypted localStorage return await readEncryptedLocalStorage(key); }, // ... rest of the methods remain similar }; ``` - [ ] **Step 6: 运行测试确认通过** Run: ```bash cd g:/ZClaw_zclaw/desktop && pnpm test tests/lib/secure-storage.test.ts ``` Expected: PASS - 4 tests - [ ] **Step 7: 提交凭据加密增强** ```bash cd g:/ZClaw_zclaw && git add desktop/src/lib/secure-storage.ts desktop/tests/lib/secure-storage.test.ts git commit -m "feat(security): add AES-GCM encryption for localStorage fallback - Encrypt credentials before storing in localStorage - Decrypt on retrieval with automatic fallback - Backward compatible with existing unencrypted data - Add comprehensive unit tests Security: Credentials are now encrypted using AES-GCM when OS keyring is unavailable, preventing plaintext exposure. Co-Authored-By: Claude Opus 4.6 " ``` --- ## Chunk 4: 强制 WSS 连接 ### Task 4.1: 增强网关安全 **Files:** - Modify: `desktop/src/lib/gateway-client.ts` - Create: `desktop/tests/lib/gateway-security.test.ts` **前置说明:** - `isLocalhost` 函数已存在于 `gateway-storage.ts` (第 27-36 行) - `gateway-client.ts` 已导入 `isLocalhost` (第 41 行) - 现有代码仅输出警告 (第 209 行),需要改为抛出错误 - [ ] **Step 1: 读取现有 gateway-client.ts WebSocket 连接逻辑** Use Read tool to view `desktop/src/lib/gateway-client.ts` lines 200-220 to see current implementation. - [ ] **Step 2: 写失败的测试 - WSS 强制** Create `desktop/tests/lib/gateway-security.test.ts`: ```typescript import { describe, it, expect, vi } from 'vitest'; // Mock WebSocket class MockWebSocket { url: string; readyState: number = WebSocket.CONNECTING; onopen: (() => void) | null = null; onerror: ((error: Error) => void) | null = null; constructor(url: string) { this.url = url; setTimeout(() => { if (this.onopen) this.onopen(); }, 0); } close() { this.readyState = WebSocket.CLOSED; } } vi.stubGlobal('WebSocket', MockWebSocket); describe('WebSocket Security', () => { describe('isLocalhost', () => { it('should identify localhost URLs', () => { const localhostUrls = [ 'ws://localhost:4200', 'ws://127.0.0.1:4200', 'ws://[::1]:4200', 'wss://localhost:4200', ]; localhostUrls.forEach(url => { const parsed = new URL(url); const isLocal = ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname); expect(isLocal).toBe(true); }); }); it('should identify non-localhost URLs', () => { const remoteUrls = [ 'ws://example.com:4200', 'ws://192.168.1.1:4200', 'wss://api.example.com/ws', ]; remoteUrls.forEach(url => { const parsed = new URL(url); const isLocal = ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname); expect(isLocal).toBe(false); }); }); }); describe('WSS enforcement', () => { it('should allow ws:// for localhost', () => { const url = 'ws://localhost:4200'; const parsed = new URL(url); const isSecure = url.startsWith('wss://') || ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname); expect(isSecure).toBe(true); }); it('should reject ws:// for non-localhost', () => { const url = 'ws://example.com:4200'; const parsed = new URL(url); const isSecure = url.startsWith('wss://') || ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname); expect(isSecure).toBe(false); }); it('should allow wss:// for any host', () => { const urls = [ 'wss://example.com:4200', 'wss://api.example.com/ws', 'wss://192.168.1.1:4200', ]; urls.forEach(url => { expect(url.startsWith('wss://')).toBe(true); }); }); }); }); ``` - [ ] **Step 3: 运行测试确认通过 (测试逻辑)** Run: ```bash cd g:/ZClaw_zclaw/desktop && pnpm test tests/lib/gateway-security.test.ts ``` Expected: PASS - 6 tests (这些是纯逻辑测试) - [ ] **Step 4: 在 gateway-client.ts 中添加安全检查** Add helper function and modify connection logic in `desktop/src/lib/gateway-client.ts`: ```typescript // Add near the top of the file, after imports /** * Check if URL points to localhost */ function isLocalhost(url: string): boolean { try { const parsed = new URL(url); return ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname); } catch { return false; } } /** * Validate WebSocket URL security * @throws SecurityError if non-localhost URL uses ws:// instead of wss:// */ function validateWebSocketSecurity(url: string): void { if (!url.startsWith('wss://') && !isLocalhost(url)) { throw new SecurityError( 'Non-localhost connections must use WSS protocol for security. ' + `URL: ${url.replace(/:[^:@]+@/, ':****@')}` ); } } // Add SecurityError class if not already present class SecurityError extends Error { constructor(message: string) { super(message); this.name = 'SecurityError'; } } ``` - [ ] **Step 5: 在 connect 方法中调用安全验证** Find the `connect` method and add the validation call: ```typescript async connect(): Promise { // Add security validation at the start try { validateWebSocketSecurity(this.url); } catch (error) { this.log('error', `Security validation failed: ${error}`); throw error; } // ... rest of existing connect logic } ``` - [ ] **Step 6: 运行现有测试确保无回归** Run: ```bash cd g:/ZClaw_zclaw/desktop && pnpm test ``` Expected: All tests pass - [ ] **Step 7: 提交 WSS 强制策略** ```bash cd g:/ZClaw_zclaw && git add desktop/src/lib/gateway-client.ts desktop/tests/lib/gateway-security.test.ts git commit -m "feat(security): enforce WSS for non-localhost connections - Add validateWebSocketSecurity function - Block ws:// connections to non-localhost hosts - Add SecurityError class for clear error handling - Add unit tests for security validation logic Security: Prevents man-in-the-middle attacks on remote connections by requiring WSS protocol for all non-localhost WebSocket connections. Co-Authored-By: Claude Opus 4.6 " ``` --- ## Chunk 5: ChatStore 基础测试 ### Task 5.1: 创建 ChatStore 测试套件 **Files:** - Create: `desktop/tests/store/chatStore.test.ts` - [ ] **Step 1: 读取 chatStore.ts 了解 API** Run: ```bash head -n 100 g:/ZClaw_zclaw/desktop/src/store/chatStore.ts ``` - [ ] **Step 2: 写失败的测试 - 基础功能** Create `desktop/tests/store/chatStore.test.ts`: ```typescript import { describe, it, expect, beforeEach, vi } from 'vitest'; import { useChatStore } from '../../src/store/chatStore'; // Mock dependencies vi.mock('../../src/lib/gateway-client', () => ({ getGatewayClient: vi.fn(() => ({ chatStream: vi.fn(), chat: vi.fn(), onAgentStream: vi.fn(() => () => {}), })), })); vi.mock('../../src/lib/intelligence-client', () => ({ intelligenceClient: { compactor: { checkThreshold: vi.fn(() => Promise.resolve({ needsCompaction: false })), }, memory: { search: vi.fn(() => Promise.resolve([])), }, reflection: { shouldReflect: vi.fn(() => Promise.resolve(false)), }, }, })); vi.mock('../../src/lib/memory-extractor', () => ({ getMemoryExtractor: vi.fn(() => ({ extract: vi.fn(() => Promise.resolve([])), })), })); describe('chatStore', () => { beforeEach(() => { // Reset store state before each test useChatStore.setState({ messages: [], conversations: [], currentConversationId: null, isStreaming: false, error: null, }); vi.clearAllMocks(); }); describe('initial state', () => { it('should have empty messages array', () => { const state = useChatStore.getState(); expect(state.messages).toEqual([]); }); it('should not be streaming initially', () => { const state = useChatStore.getState(); expect(state.isStreaming).toBe(false); }); }); describe('addMessage', () => { it('should add a message to the store', () => { const { addMessage } = useChatStore.getState(); const message = { id: 'test-1', role: 'user' as const, content: 'Hello', timestamp: Date.now(), }; addMessage(message); const state = useChatStore.getState(); expect(state.messages).toHaveLength(1); expect(state.messages[0]).toEqual(message); }); it('should append message to existing messages', () => { const { addMessage } = useChatStore.getState(); addMessage({ id: 'test-1', role: 'user', content: 'First', timestamp: Date.now(), }); addMessage({ id: 'test-2', role: 'assistant', content: 'Second', timestamp: Date.now(), }); const state = useChatStore.getState(); expect(state.messages).toHaveLength(2); }); }); describe('setStreaming', () => { it('should update streaming state', () => { const { setStreaming } = useChatStore.getState(); setStreaming(true); expect(useChatStore.getState().isStreaming).toBe(true); setStreaming(false); expect(useChatStore.getState().isStreaming).toBe(false); }); }); describe('clearMessages', () => { it('should clear all messages', () => { const { addMessage, clearMessages } = useChatStore.getState(); addMessage({ id: 'test-1', role: 'user', content: 'Test', timestamp: Date.now(), }); clearMessages(); expect(useChatStore.getState().messages).toEqual([]); }); }); describe('error handling', () => { it('should store error state', () => { const { setError } = useChatStore.getState(); const error = new Error('Test error'); setError(error); expect(useChatStore.getState().error).toBe(error); }); it('should clear error with null', () => { const { setError } = useChatStore.getState(); setError(new Error('Test')); setError(null); expect(useChatStore.getState().error).toBeNull(); }); }); }); ``` - [ ] **Step 3: 运行测试** Run: ```bash cd g:/ZClaw_zclaw/desktop && pnpm test tests/store/chatStore.test.ts ``` Expected: PASS - 8+ tests - [ ] **Step 4: 添加更多测试 - 消息更新** Add to `desktop/tests/store/chatStore.test.ts`: ```typescript describe('updateMessage', () => { it('should update existing message content', () => { const { addMessage, updateMessage } = useChatStore.getState(); addMessage({ id: 'test-1', role: 'assistant', content: 'Initial', timestamp: Date.now(), }); updateMessage('test-1', { content: 'Updated' }); const state = useChatStore.getState(); expect(state.messages[0].content).toBe('Updated'); }); it('should not modify message if id not found', () => { const { addMessage, updateMessage } = useChatStore.getState(); addMessage({ id: 'test-1', role: 'user', content: 'Test', timestamp: Date.now(), }); updateMessage('non-existent', { content: 'Should not appear' }); const state = useChatStore.getState(); expect(state.messages[0].content).toBe('Test'); }); }); ``` - [ ] **Step 5: 运行测试确认通过** Run: ```bash cd g:/ZClaw_zclaw/desktop && pnpm test tests/store/chatStore.test.ts ``` Expected: PASS - 10+ tests - [ ] **Step 6: 运行覆盖率报告** Run: ```bash cd g:/ZClaw_zclaw/desktop && pnpm test:coverage tests/store/chatStore.test.ts ``` Expected: Coverage > 80% for chatStore - [ ] **Step 7: 提交 ChatStore 测试** ```bash cd g:/ZClaw_zclaw && git add desktop/tests/store/chatStore.test.ts git commit -m "test(chat): add comprehensive unit tests for chatStore - Test initial state - Test addMessage functionality - Test setStreaming state management - Test clearMessages functionality - Test error handling - Test updateMessage functionality Coverage: 80%+ for chatStore module Co-Authored-By: Claude Opus 4.6 " ``` --- ## Chunk 6: 集成与验收 ### Task 6.1: 运行完整测试套件 - [ ] **Step 1: 运行所有测试** Run: ```bash cd g:/ZClaw_zclaw/desktop && pnpm test ``` Expected: All tests pass - [ ] **Step 2: 生成覆盖率报告** Run: ```bash cd g:/ZClaw_zclaw/desktop && pnpm test:coverage ``` Expected: Overall coverage >= 60% - [ ] **Step 3: 验证覆盖率门禁** Check that coverage thresholds in `vitest.config.ts` are being enforced. ### Task 6.2: 手动验证安全功能 - [ ] **Step 1: 验证加密存储** 1. 启动应用: `pnpm tauri:dev` 2. 在开发者工具中检查 localStorage 3. 确认凭据值是加密的 (包含 `iv` 和 `data` 字段) - [ ] **Step 2: 验证 WSS 强制** 1. 尝试连接 `ws://example.com:4200` 2. 确认连接被拒绝并显示安全错误 3. 确认 `ws://localhost:4200` 仍然可以连接 ### Task 6.3: 更新文档 - [ ] **Step 1: 更新 README 或创建变更日志** Create `docs/changelogs/2026-03-21-phase1-security.md`: ```markdown # Phase 1: Security + Testing ## Changes ### Security - **凭据加密**: localStorage 回退现在使用 AES-GCM 加密 - **WSS 强制**: 非 localhost 连接必须使用 WSS 协议 ### Testing - 建立 Vitest 测试框架 - 添加 chatStore 单元测试 (80%+ 覆盖率) - 添加加密工具测试 - 添加网关安全测试 ## Migration Notes - 现有未加密的 localStorage 数据会在首次读取时自动迁移 - 无需手动干预 ## Breaking Changes - `ws://` 协议不再允许用于非 localhost 连接 - 如需使用远程服务器,请确保使用 `wss://` ``` - [ ] **Step 2: 提交文档更新** ```bash cd g:/ZClaw_zclaw && git add docs/changelogs/2026-03-21-phase1-security.md git commit -m "docs: add Phase 1 changelog Co-Authored-By: Claude Opus 4.6 " ``` ### Task 6.4: 最终提交和标记 - [ ] **Step 1: 创建 Phase 1 完成标签** ```bash cd g:/ZClaw_zclaw && git tag -a v0.3.0-phase1 -m "Phase 1: Security + Testing Complete" ``` - [ ] **Step 2: 推送所有提交** ```bash cd g:/ZClaw_zclaw && git push origin main --tags ``` --- ## Verification Checklist ### Security Verification - [ ] localStorage 凭据已加密 (AES-GCM) - [ ] 非 localhost 连接强制 WSS - [ ] 加密密钥不暴露在日志中 - [ ] 向后兼容未加密数据 ### Testing Verification - [ ] 所有测试通过 - [ ] 覆盖率 >= 60% - [ ] chatStore 覆盖率 >= 80% - [ ] CI 集成就绪 ### Documentation Verification - [ ] 变更日志已创建 - [ ] 迁移说明已记录 - [ ] 破坏性变更已标注 --- ## Next Steps (Phase 2) 完成 Phase 1 后,继续执行 Phase 2: 领域重组 - TASK-101: 创建 domains/ 目录结构 - TASK-102: 迁移 Chat Store 到 Valtio - TASK-103: 迁移 Hands Store + XState - TASK-104: 增强 Intelligence 缓存 - TASK-105: 提取共享模块 详见: `docs/superpowers/plans/2026-03-21-phase2-domain-reorganization.md` (待创建)