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