docs(guide): rewrite CLAUDE.md with ZCLAW-first perspective

Major changes:
- Shift from "OpenFang desktop client" to "independent AI Agent desktop app"
- Add decision principle: "Is this useful for ZCLAW? Does it affect ZCLAW?"
- Simplify project structure and tech stack sections
- Replace OpenClaw vs OpenFang comparison with unified backend approach
- Consolidate troubleshooting from scattered sections into organized FAQ
- Update Hands system documentation with 8 capabilities and status
- Stream
This commit is contained in:
iven
2026-03-20 19:30:09 +08:00
parent 3518fc8ece
commit 6f72442531
63 changed files with 8920 additions and 857 deletions

View File

@@ -0,0 +1,538 @@
/**
* ZCLAW Store 状态验证测试
*
* 专注于验证 Zustand Store 的状态管理和转换
* 确保状态正确初始化、更新和持久化
*/
import { test, expect, Page } from '@playwright/test';
import { storeInspectors, STORE_NAMES } from '../fixtures/store-inspectors';
import {
chatAssertions,
connectionAssertions,
handAssertions,
agentAssertions,
storeAssertions,
} from '../utils/store-assertions';
import { userActions, waitForAppReady, navigateToTab } from '../utils/user-actions';
import { messageFactory, cloneFactory, handFactory } from '../fixtures/test-data';
// 测试超时配置
test.setTimeout(120000);
const BASE_URL = 'http://localhost:1420';
// ============================================
// 测试套件 1: Store 初始化验证
// ============================================
test.describe('Store 初始化验证', () => {
test('STORE-INIT-01: Chat Store 初始化', async ({ page }) => {
await page.goto(BASE_URL);
await waitForAppReady(page);
// 验证 Chat Store 存在并初始化
const state = await storeInspectors.getPersistedState<{
messages: unknown[];
isStreaming: boolean;
currentModel: string;
}>(page, STORE_NAMES.CHAT);
expect(state).not.toBeNull();
expect(Array.isArray(state?.messages)).toBe(true);
expect(typeof state?.isStreaming).toBe('boolean');
expect(typeof state?.currentModel).toBe('string');
});
test('STORE-INIT-02: Gateway Store 初始化', async ({ page }) => {
await page.goto(BASE_URL);
await waitForAppReady(page);
// 验证 Gateway Store 存在
const state = await storeInspectors.getPersistedState<{
connectionState: string;
hands: unknown[];
workflows: unknown[];
clones: unknown[];
}>(page, STORE_NAMES.GATEWAY);
expect(state).not.toBeNull();
// 连接状态应该是有效值
const validStates = ['connected', 'disconnected', 'connecting', 'reconnecting', 'handshaking'];
expect(validStates).toContain(state?.connectionState);
});
test('STORE-INIT-03: Agent Store 初始化', async ({ page }) => {
await page.goto(BASE_URL);
await waitForAppReady(page);
// 验证 Agent Store 存在
const state = await storeInspectors.getPersistedState<{
clones: unknown[];
isLoading: boolean;
}>(page, STORE_NAMES.AGENT);
expect(state).not.toBeNull();
expect(Array.isArray(state?.clones)).toBe(true);
});
test('STORE-INIT-04: Hand Store 初始化', async ({ page }) => {
await page.goto(BASE_URL);
await waitForAppReady(page);
// 验证 Hand Store 存在
const state = await storeInspectors.getPersistedState<{
hands: unknown[];
handRuns: Record<string, unknown[]>;
isLoading: boolean;
}>(page, STORE_NAMES.HAND);
expect(state).not.toBeNull();
expect(Array.isArray(state?.hands)).toBe(true);
expect(typeof state?.handRuns).toBe('object');
});
test('STORE-INIT-05: Config Store 初始化', async ({ page }) => {
await page.goto(BASE_URL);
await waitForAppReady(page);
// 验证 Config Store 存在
const state = await storeInspectors.getPersistedState<{
quickConfig: Record<string, unknown>;
models: unknown[];
}>(page, STORE_NAMES.CONFIG);
expect(state).not.toBeNull();
});
});
// ============================================
// 测试套件 2: Store 持久化验证
// ============================================
test.describe('Store 持久化验证', () => {
test('STORE-PERSIST-01: Chat Store 持久化', async ({ page }) => {
// 1. 加载页面
await page.goto(BASE_URL);
await waitForAppReady(page);
// 2. 发送一条消息
await userActions.sendChatMessage(page, '持久化测试消息');
await page.waitForTimeout(3000);
// 3. 获取当前状态
const stateBefore = await storeInspectors.getPersistedState<{
messages: Array<{ content: string }>;
}>(page, STORE_NAMES.CHAT);
const countBefore = stateBefore?.messages?.length ?? 0;
// 4. 刷新页面
await page.reload();
await waitForAppReady(page);
// 5. 验证状态恢复
const stateAfter = await storeInspectors.getPersistedState<{
messages: Array<{ content: string }>;
}>(page, STORE_NAMES.CHAT);
const countAfter = stateAfter?.messages?.length ?? 0;
// 消息应该被恢复(数量相同或更多)
expect(countAfter).toBeGreaterThanOrEqual(countBefore - 2); // 允许一定误差
});
test('STORE-PERSIST-02: 配置持久化', async ({ page }) => {
// 1. 加载页面并获取配置
await page.goto(BASE_URL);
await waitForAppReady(page);
const configBefore = await storeInspectors.getPersistedState<{
quickConfig: Record<string, unknown>;
}>(page, STORE_NAMES.CONFIG);
// 2. 刷新页面
await page.reload();
await waitForAppReady(page);
// 3. 验证配置恢复
const configAfter = await storeInspectors.getPersistedState<{
quickConfig: Record<string, unknown>;
}>(page, STORE_NAMES.CONFIG);
// 配置应该相同
expect(configAfter?.quickConfig).toEqual(configBefore?.quickConfig);
});
test('STORE-PERSIST-03: 清除 Store 后重新初始化', async ({ page }) => {
// 1. 加载页面
await page.goto(BASE_URL);
await waitForAppReady(page);
// 2. 清除所有 Store
await storeInspectors.clearAllStores(page);
// 3. 刷新页面
await page.reload();
await waitForAppReady(page);
// 4. 验证 Store 重新初始化
const chatState = await storeInspectors.getPersistedState<{
messages: unknown[];
}>(page, STORE_NAMES.CHAT);
// Store 应该被重新初始化messages 为空数组)
expect(Array.isArray(chatState?.messages)).toBe(true);
});
});
// ============================================
// 测试套件 3: Chat Store 状态转换验证
// ============================================
test.describe('Chat Store 状态转换验证', () => {
test.beforeEach(async ({ page }) => {
await page.goto(BASE_URL);
await waitForAppReady(page);
});
test('CHAT-STATE-01: isStreaming 状态转换', async ({ page }) => {
// 1. 初始状态应该是 false
const initialState = await storeInspectors.getPersistedState<{
isStreaming: boolean;
}>(page, STORE_NAMES.CHAT);
expect(initialState?.isStreaming).toBe(false);
// 2. 发送消息
await userActions.sendChatMessage(page, '测试消息');
// 3. 等待流式完成
await page.waitForTimeout(5000);
// 4. 最终状态应该是 false
const finalState = await storeInspectors.getPersistedState<{
isStreaming: boolean;
}>(page, STORE_NAMES.CHAT);
expect(finalState?.isStreaming).toBe(false);
});
test('CHAT-STATE-02: messages 数组状态变化', async ({ page }) => {
// 1. 获取初始消息数量
const initialState = await storeInspectors.getPersistedState<{
messages: unknown[];
}>(page, STORE_NAMES.CHAT);
const initialCount = initialState?.messages?.length ?? 0;
// 2. 发送消息
await userActions.sendChatMessage(page, '新消息');
await page.waitForTimeout(3000);
// 3. 验证消息数量增加
const newState = await storeInspectors.getPersistedState<{
messages: unknown[];
}>(page, STORE_NAMES.CHAT);
const newCount = newState?.messages?.length ?? 0;
// 消息数量应该增加(至少用户消息)
expect(newCount).toBeGreaterThan(initialCount);
});
test('CHAT-STATE-03: currentModel 状态', async ({ page }) => {
// 1. 获取当前模型
const state = await storeInspectors.getPersistedState<{
currentModel: string;
}>(page, STORE_NAMES.CHAT);
// 2. 验证模型是有效值
expect(state?.currentModel).toBeDefined();
expect(state?.currentModel.length).toBeGreaterThan(0);
});
test('CHAT-STATE-04: sessionKey 状态', async ({ page }) => {
// 1. 发送消息建立会话
await userActions.sendChatMessage(page, '建立会话');
await page.waitForTimeout(3000);
// 2. 检查 sessionKey
const state = await storeInspectors.getPersistedState<{
sessionKey: string | null;
}>(page, STORE_NAMES.CHAT);
// sessionKey 应该存在(如果后端返回了)
console.log(`SessionKey exists: ${!!state?.sessionKey}`);
});
});
// ============================================
// 测试套件 4: Agent Store 状态转换验证
// ============================================
test.describe('Agent Store 状态转换验证', () => {
test.beforeEach(async ({ page }) => {
await page.goto(BASE_URL);
await waitForAppReady(page);
await navigateToTab(page, '分身');
});
test('AGENT-STATE-01: clones 数组状态', async ({ page }) => {
// 1. 获取 clones 列表
const state = await storeInspectors.getPersistedState<{
clones: Array<{ id: string; name: string }>;
}>(page, STORE_NAMES.AGENT);
// 2. 验证格式
expect(Array.isArray(state?.clones)).toBe(true);
// 3. 每个 clone 应该有必需字段
if (state?.clones && state.clones.length > 0) {
const firstClone = state.clones[0];
expect(firstClone).toHaveProperty('id');
expect(firstClone).toHaveProperty('name');
}
});
test('AGENT-STATE-02: currentAgent 切换状态', async ({ page }) => {
// 1. 获取当前 Agent
const chatState = await storeInspectors.getPersistedState<{
currentAgent: { id: string } | null;
}>(page, STORE_NAMES.CHAT);
// 2. 验证 currentAgent 结构
if (chatState?.currentAgent) {
expect(chatState.currentAgent).toHaveProperty('id');
}
});
});
// ============================================
// 测试套件 5: Hand Store 状态转换验证
// ============================================
test.describe('Hand Store 状态转换验证', () => {
test.beforeEach(async ({ page }) => {
await page.goto(BASE_URL);
await waitForAppReady(page);
await navigateToTab(page, 'Hands');
await page.waitForTimeout(1500);
});
test('HAND-STATE-01: hands 数组状态', async ({ page }) => {
// 1. 获取 hands 列表
const state = await storeInspectors.getPersistedState<{
hands: Array<{
id: string;
name: string;
status: string;
requirements_met?: boolean;
}>;
}>(page, STORE_NAMES.HAND);
// 2. 验证格式
expect(Array.isArray(state?.hands)).toBe(true);
// 3. 每个 hand 应该有必需字段
if (state?.hands && state.hands.length > 0) {
const firstHand = state.hands[0];
expect(firstHand).toHaveProperty('id');
expect(firstHand).toHaveProperty('name');
expect(firstHand).toHaveProperty('status');
// 状态应该是有效值
const validStatuses = ['idle', 'running', 'needs_approval', 'error', 'unavailable', 'setup_needed'];
expect(validStatuses).toContain(firstHand.status);
}
});
test('HAND-STATE-02: handRuns 记录状态', async ({ page }) => {
// 1. 获取 handRuns
const state = await storeInspectors.getPersistedState<{
handRuns: Record<string, unknown[]>;
}>(page, STORE_NAMES.HAND);
// 2. 验证格式
expect(typeof state?.handRuns).toBe('object');
});
test('HAND-STATE-03: approvals 队列状态', async ({ page }) => {
// 1. 获取 approvals
const state = await storeInspectors.getPersistedState<{
approvals: unknown[];
}>(page, STORE_NAMES.HAND);
// 2. 验证格式
expect(Array.isArray(state?.approvals)).toBe(true);
});
});
// ============================================
// 测试套件 6: Workflow Store 状态转换验证
// ============================================
test.describe('Workflow Store 状态转换验证', () => {
test.beforeEach(async ({ page }) => {
await page.goto(BASE_URL);
await waitForAppReady(page);
await navigateToTab(page, '工作流');
await page.waitForTimeout(1000);
});
test('WF-STATE-01: workflows 数组状态', async ({ page }) => {
// 1. 获取 workflows 列表
const state = await storeInspectors.getPersistedState<{
workflows: Array<{
id: string;
name: string;
steps: number;
}>;
}>(page, STORE_NAMES.WORKFLOW);
// 2. 验证格式
expect(Array.isArray(state?.workflows)).toBe(true);
// 3. 每个 workflow 应该有必需字段
if (state?.workflows && state.workflows.length > 0) {
const firstWorkflow = state.workflows[0];
expect(firstWorkflow).toHaveProperty('id');
expect(firstWorkflow).toHaveProperty('name');
}
});
});
// ============================================
// 测试套件 7: Team Store 状态转换验证
// ============================================
test.describe('Team Store 状态转换验证', () => {
test.beforeEach(async ({ page }) => {
await page.goto(BASE_URL);
await waitForAppReady(page);
await navigateToTab(page, '团队');
await page.waitForTimeout(1000);
});
test('TEAM-STATE-01: teams 数组状态', async ({ page }) => {
// 1. 获取 teams 列表
const state = await storeInspectors.getPersistedState<{
teams: Array<{
id: string;
name: string;
members: unknown[];
tasks: unknown[];
}>;
}>(page, STORE_NAMES.TEAM);
// 2. 验证格式
expect(Array.isArray(state?.teams)).toBe(true);
// 3. 每个 team 应该有必需字段
if (state?.teams && state.teams.length > 0) {
const firstTeam = state.teams[0];
expect(firstTeam).toHaveProperty('id');
expect(firstTeam).toHaveProperty('name');
expect(Array.isArray(firstTeam.members)).toBe(true);
expect(Array.isArray(firstTeam.tasks)).toBe(true);
}
});
test('TEAM-STATE-02: activeTeam 状态', async ({ page }) => {
// 1. 获取 activeTeam
const state = await storeInspectors.getPersistedState<{
activeTeam: { id: string } | null;
}>(page, STORE_NAMES.TEAM);
// 2. 验证状态
// activeTeam 可以是 null 或有 id 的对象
if (state?.activeTeam) {
expect(state.activeTeam).toHaveProperty('id');
}
});
});
// ============================================
// 测试套件 8: Connection Store 状态转换验证
// ============================================
test.describe('Connection Store 状态转换验证', () => {
test.beforeEach(async ({ page }) => {
await page.goto(BASE_URL);
await waitForAppReady(page);
});
test('CONN-STATE-01: connectionState 状态', async ({ page }) => {
// 1. 获取连接状态
const state = await storeInspectors.getPersistedState<{
connectionState: string;
}>(page, STORE_NAMES.CONNECTION);
// 2. 验证状态是有效值
const validStates = ['connected', 'disconnected', 'connecting', 'reconnecting', 'handshaking'];
expect(validStates).toContain(state?.connectionState);
});
test('CONN-STATE-02: gatewayVersion 状态', async ({ page }) => {
// 1. 等待连接尝试
await page.waitForTimeout(3000);
// 2. 获取版本
const state = await storeInspectors.getPersistedState<{
gatewayVersion: string | null;
}>(page, STORE_NAMES.CONNECTION);
// 3. 如果连接成功,版本应该存在
console.log(`Gateway version: ${state?.gatewayVersion}`);
});
test('CONN-STATE-03: error 状态', async ({ page }) => {
// 1. 获取错误状态
const state = await storeInspectors.getPersistedState<{
error: string | null;
}>(page, STORE_NAMES.CONNECTION);
// 2. 正常情况下 error 应该是 null
// 但如果连接失败error 可能有值
console.log(`Connection error: ${state?.error}`);
});
});
// ============================================
// 测试套件 9: Store 快照验证
// ============================================
test.describe('Store 快照验证', () => {
test('SNAPSHOT-01: 获取所有 Store 快照', async ({ page }) => {
await page.goto(BASE_URL);
await waitForAppReady(page);
// 1. 获取所有 Store 快照
const snapshot = await storeInspectors.getAllStoresSnapshot(page);
// 2. 验证快照包含预期的 Store
console.log('Store snapshot keys:', Object.keys(snapshot));
// 3. 验证每个 Store 的基本结构
for (const [storeName, state] of Object.entries(snapshot)) {
console.log(`Store ${storeName}:`, typeof state);
expect(state).toBeDefined();
}
});
test('SNAPSHOT-02: Store 状态一致性', async ({ page }) => {
await page.goto(BASE_URL);
await waitForAppReady(page);
// 1. 获取两次快照
const snapshot1 = await storeInspectors.getAllStoresSnapshot(page);
await page.waitForTimeout(100);
const snapshot2 = await storeInspectors.getAllStoresSnapshot(page);
// 2. 验证状态一致性(无操作时状态应该相同)
expect(snapshot1).toEqual(snapshot2);
});
});
// 测试报告
test.afterAll(async ({}, testInfo) => {
console.log('\n========================================');
console.log('ZCLAW Store 状态验证测试完成');
console.log('========================================');
console.log(`测试时间: ${new Date().toISOString()}`);
console.log('========================================\n');
});