Files
zclaw_openfang/docs/superpowers/plans/2026-03-21-phase1-security-testing.md
iven 0d4fa96b82
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
refactor: 统一项目名称从OpenFang到ZCLAW
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括:
- 配置文件中的项目名称
- 代码注释和文档引用
- 环境变量和路径
- 类型定义和接口名称
- 测试用例和模拟数据

同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
2026-03-27 07:36:03 +08:00

1311 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<string, string> = {};
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 <noreply@anthropic.com>"
```
---
## 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<CryptoKey> {
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<string> {
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 <noreply@anthropic.com>"
```
---
## 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<CryptoKey> {
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<void> {
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<string | null> {
// 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<void> {
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<string | null> {
if (await isSecureStorageAvailable()) {
try {
const value = await invoke<string>('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 <noreply@anthropic.com>"
```
---
## 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<void> {
// 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 <noreply@anthropic.com>"
```
---
## 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 <noreply@anthropic.com>"
```
---
## 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 <noreply@anthropic.com>"
```
### 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` (待创建)