Files
zclaw_openfang/desktop/tests/e2e/specs/store-state.spec.ts
iven 6f72442531 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
2026-03-20 19:30:09 +08:00

539 lines
17 KiB
TypeScript
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 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');
});