Files
zclaw_openfang/desktop/tests/e2e/fixtures/store-inspectors.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

636 lines
16 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.

/**
* Store 状态检查工具
* 用于验证 Zustand Store 的状态变化
*
* 实际 localStorage keys:
* - chatStore: zclaw-chat-storage (持久化)
* - teamStore: zclaw-teams (持久化)
* - gatewayStore: zclaw-gateway-url, zclaw-gateway-token, zclaw-device-id (单独键)
* - handStore: 不持久化
* - agentStore: 不持久化
*/
import { Page } from '@playwright/test';
import { expect } from '@playwright/test';
/**
* localStorage key 映射
*/
export const STORAGE_KEYS = {
// 标准持久化 Store (使用 zustand persist)
CHAT: 'zclaw-chat-storage',
TEAMS: 'zclaw-teams',
// Gateway Store 使用单独的键
GATEWAY: 'zclaw-gateway-url', // 主键
GATEWAY_URL: 'zclaw-gateway-url',
GATEWAY_TOKEN: 'zclaw-gateway-token',
DEVICE_ID: 'zclaw-device-id',
// 非持久化 Store (运行时状态,不在 localStorage)
HAND: null,
AGENT: null,
WORKFLOW: null,
CONNECTION: null,
CONFIG: null,
} as const;
/**
* Store 名称类型
*/
export type StoreName = keyof typeof STORAGE_KEYS;
/**
* 向后兼容: STORE_NAMES 枚举
* @deprecated 使用 STORAGE_KEYS 代替
*/
export const STORE_NAMES = {
CHAT: 'CHAT' as StoreName,
GATEWAY: 'GATEWAY' as StoreName,
AGENT: 'AGENT' as StoreName,
HAND: 'HAND' as StoreName,
WORKFLOW: 'WORKFLOW' as StoreName,
CONFIG: 'CONFIG' as StoreName,
TEAM: 'TEAMS' as StoreName,
CONNECTION: 'CONNECTION' as StoreName,
} as const;
/**
* Store 状态检查工具
*/
export const storeInspectors = {
/**
* 获取 localStorage key
*/
getStorageKey(storeName: StoreName): string | null {
return STORAGE_KEYS[storeName];
},
/**
* 获取持久化的 Chat Store 状态
*/
async getChatState<T = unknown>(page: Page): Promise<T | null> {
return page.evaluate((key) => {
const stored = localStorage.getItem(key);
if (!stored) return null;
try {
const parsed = JSON.parse(stored);
return parsed.state as T;
} catch {
return null;
}
}, STORAGE_KEYS.CHAT);
},
/**
* 获取持久化的 Teams Store 状态
*/
async getTeamsState<T = unknown>(page: Page): Promise<T | null> {
return page.evaluate((key) => {
const stored = localStorage.getItem(key);
if (!stored) return null;
try {
// teams store 可能直接存储数组或对象
const parsed = JSON.parse(stored);
return parsed.state ? parsed.state : parsed;
} catch {
return null;
}
}, STORAGE_KEYS.TEAMS);
},
/**
* 获取 Gateway 配置
*/
async getGatewayConfig(page: Page): Promise<{
url: string | null;
token: string | null;
deviceId: string | null;
}> {
return page.evaluate((keys) => {
return {
url: localStorage.getItem(keys.url),
token: localStorage.getItem(keys.token),
deviceId: localStorage.getItem(keys.deviceId),
};
}, {
url: STORAGE_KEYS.GATEWAY_URL,
token: STORAGE_KEYS.GATEWAY_TOKEN,
deviceId: STORAGE_KEYS.DEVICE_ID,
});
},
/**
* 获取持久化的 Store 状态 (通用方法)
*/
async getPersistedState<T = unknown>(page: Page, storeName: StoreName): Promise<T | null> {
const key = STORAGE_KEYS[storeName];
if (!key) {
// 非持久化 Store尝试从运行时获取
return this.getRuntimeState<T>(page, storeName);
}
return page.evaluate((storageKey) => {
const stored = localStorage.getItem(storageKey);
if (!stored) return null;
try {
const parsed = JSON.parse(stored);
return parsed.state ? (parsed.state as T) : (parsed as T);
} catch {
return null;
}
}, key);
},
/**
* 获取运行时 Store 状态 (通过 window 全局变量)
* 注意:需要在应用中暴露 store 到 window 对象
*/
async getRuntimeState<T = unknown>(page: Page, storeName: string): Promise<T | null> {
return page.evaluate((name) => {
// 尝试从 window.__ZCLAW_STORES__ 获取
const stores = (window as any).__ZCLAW_STORES__;
if (stores && stores[name]) {
return stores[name].getState() as T;
}
return null;
}, storeName);
},
/**
* 获取整个持久化对象(包含 state 和 version
*/
async getFullStorage<T = unknown>(
page: Page,
storeName: StoreName
): Promise<{ state: T; version: number } | null> {
const key = STORAGE_KEYS[storeName];
if (!key) return null;
return page.evaluate((storageKey) => {
const stored = localStorage.getItem(storageKey);
if (!stored) return null;
try {
return JSON.parse(stored);
} catch {
return null;
}
}, key);
},
/**
* 获取 Store 中的特定字段
*/
async getStateField<T = unknown>(
page: Page,
storeName: StoreName,
fieldPath: string
): Promise<T | null> {
const key = STORAGE_KEYS[storeName];
if (!key) return null;
return page.evaluate(
({ storageKey, path }) => {
const stored = localStorage.getItem(storageKey);
if (!stored) return null;
try {
const parsed = JSON.parse(stored);
const state = parsed.state ? parsed.state : parsed;
const value = path.split('.').reduce((obj: any, key) => obj?.[key], state);
return value as T;
} catch {
return null;
}
},
{ storageKey: key, path: fieldPath }
);
},
/**
* 等待 Store 状态变化
*/
async waitForStateChange<T = unknown>(
page: Page,
storeName: StoreName,
fieldPath: string,
expectedValue: T,
options?: { timeout?: number }
): Promise<void> {
const key = STORAGE_KEYS[storeName];
if (!key) {
throw new Error(`Store ${storeName} is not persisted`);
}
await page.waitForFunction(
({ storageKey, path, expected }) => {
const stored = localStorage.getItem(storageKey);
if (!stored) return false;
try {
const parsed = JSON.parse(stored);
const state = parsed.state ? parsed.state : parsed;
const value = path.split('.').reduce((obj: any, key) => obj?.[key], state);
return JSON.stringify(value) === JSON.stringify(expected);
} catch {
return false;
}
},
{
storageKey: key,
path: fieldPath,
expected: expectedValue,
},
{ timeout: options?.timeout ?? 5000 }
);
},
/**
* 等待 Store 中某个字段存在
*/
async waitForFieldExists(
page: Page,
storeName: StoreName,
fieldPath: string,
options?: { timeout?: number }
): Promise<void> {
const key = STORAGE_KEYS[storeName];
if (!key) return;
await page.waitForFunction(
({ storageKey, path }) => {
const stored = localStorage.getItem(storageKey);
if (!stored) return false;
try {
const parsed = JSON.parse(stored);
const state = parsed.state ? parsed.state : parsed;
const value = path.split('.').reduce((obj: any, key) => obj?.[key], state);
return value !== undefined && value !== null;
} catch {
return false;
}
},
{ storageKey: key, path: fieldPath },
{ timeout: options?.timeout ?? 5000 }
);
},
/**
* 等待消息数量变化
*/
async waitForMessageCount(
page: Page,
expectedCount: number,
options?: { timeout?: number }
): Promise<void> {
await page.waitForFunction(
({ expected, key }) => {
const stored = localStorage.getItem(key);
if (!stored) return false;
try {
const parsed = JSON.parse(stored);
return parsed.state?.messages?.length === expected;
} catch {
return false;
}
},
{ expected: expectedCount, key: STORAGE_KEYS.CHAT },
{ timeout: options?.timeout ?? 10000 }
);
},
/**
* 清除特定 Store 的持久化数据
*/
async clearStore(page: Page, storeName: StoreName): Promise<void> {
const key = STORAGE_KEYS[storeName];
if (key) {
await page.evaluate((storageKey) => {
localStorage.removeItem(storageKey);
}, key);
}
},
/**
* 清除所有 Store 数据
*/
async clearAllStores(page: Page): Promise<void> {
await page.evaluate(() => {
const keys = Object.keys(localStorage);
keys.forEach((key) => {
if (key.startsWith('zclaw-')) {
localStorage.removeItem(key);
}
});
});
},
/**
* 设置 Store 状态(用于测试初始化)
*/
async setStoreState<T>(page: Page, storeName: StoreName, state: T): Promise<void> {
const key = STORAGE_KEYS[storeName];
if (!key) return;
await page.evaluate(
({ storageKey, stateObj }) => {
const data = {
state: stateObj,
version: 0,
};
localStorage.setItem(storageKey, JSON.stringify(data));
},
{ storageKey: key, stateObj: state }
);
},
/**
* 设置 Chat Store 状态
*/
async setChatState<T>(page: Page, state: T): Promise<void> {
await page.evaluate(
({ key, stateObj }) => {
const data = {
state: stateObj,
version: 0,
};
localStorage.setItem(key, JSON.stringify(data));
},
{ key: STORAGE_KEYS.CHAT, stateObj: state }
);
},
/**
* 获取所有 Store 状态快照
*/
async getAllStoresSnapshot(page: Page): Promise<Record<string, unknown>> {
return page.evaluate(() => {
const snapshot: Record<string, unknown> = {};
const keys = Object.keys(localStorage);
keys.forEach((key) => {
if (key.startsWith('zclaw-')) {
const storeName = key.replace('zclaw-', '').replace('-storage', '');
try {
const stored = localStorage.getItem(key);
if (stored) {
const parsed = JSON.parse(stored);
snapshot[storeName] = parsed.state ? parsed.state : parsed;
}
} catch {
// ignore parse errors
}
}
});
return snapshot;
});
},
/**
* 检查 Store 是否持久化
*/
isPersistedStore(storeName: StoreName): boolean {
return STORAGE_KEYS[storeName] !== null;
},
};
/**
* Store 断言工具
*/
export const storeAssertions = {
/**
* 断言 Chat Store 状态匹配预期
*/
async assertChatState<T>(
page: Page,
expected: Partial<T>
): Promise<void> {
const state = await storeInspectors.getChatState<T>(page);
expect(state).not.toBeNull();
for (const [key, value] of Object.entries(expected)) {
expect(state).toHaveProperty(key, value);
}
},
/**
* 断言 Teams Store 状态匹配预期
*/
async assertTeamsState<T>(
page: Page,
expected: Partial<T>
): Promise<void> {
const state = await storeInspectors.getTeamsState<T>(page);
expect(state).not.toBeNull();
for (const [key, value] of Object.entries(expected)) {
expect(state).toHaveProperty(key, value);
}
},
/**
* 断言 Store 字段值
*/
async assertFieldEquals<T>(
page: Page,
storeName: StoreName,
fieldPath: string,
expected: T
): Promise<void> {
const value = await storeInspectors.getStateField<T>(page, storeName, fieldPath);
expect(value).toEqual(expected);
},
/**
* 断言消息数量
*/
async assertMessageCount(page: Page, expected: number): Promise<void> {
const state = await storeInspectors.getChatState<{ messages: unknown[] }>(page);
expect(state?.messages?.length).toBe(expected);
},
/**
* 断言最后一条消息内容
*/
async assertLastMessageContent(page: Page, expected: string): Promise<void> {
const state = await storeInspectors.getChatState<{
messages: Array<{ content: string }>;
}>(page);
const lastMessage = state?.messages?.[state.messages.length - 1];
expect(lastMessage?.content).toContain(expected);
},
/**
* 断言 Gateway 配置
*/
async assertGatewayConfig(
page: Page,
expected: { url?: string; token?: string; deviceId?: string }
): Promise<void> {
const config = await storeInspectors.getGatewayConfig(page);
if (expected.url !== undefined) {
expect(config.url).toBe(expected.url);
}
if (expected.token !== undefined) {
expect(config.token).toBe(expected.token);
}
if (expected.deviceId !== undefined) {
expect(config.deviceId).toBe(expected.deviceId);
}
},
/**
* 断言 Teams 列表非空
*/
async assertTeamsNotEmpty(page: Page): Promise<void> {
const state = await storeInspectors.getTeamsState<{ teams: unknown[] }>(page);
expect(state?.teams?.length).toBeGreaterThan(0);
},
/**
* 断言当前 Agent
*/
async assertCurrentAgent(page: Page, agentId: string): Promise<void> {
const state = await storeInspectors.getChatState<{
currentAgent: { id: string };
}>(page);
expect(state?.currentAgent?.id).toBe(agentId);
},
/**
* 断言 isStreaming 状态
*/
async assertStreamingState(page: Page, expected: boolean): Promise<void> {
const state = await storeInspectors.getChatState<{ isStreaming: boolean }>(page);
expect(state?.isStreaming).toBe(expected);
},
/**
* 断言当前模型
*/
async assertCurrentModel(page: Page, expectedModel: string): Promise<void> {
const state = await storeInspectors.getChatState<{ currentModel: string }>(page);
expect(state?.currentModel).toBe(expectedModel);
},
/**
* 断言会话存在
*/
async assertConversationExists(page: Page, conversationId: string): Promise<void> {
const state = await storeInspectors.getChatState<{
conversations: Array<{ id: string }>;
}>(page);
const exists = state?.conversations?.some(c => c.id === conversationId);
expect(exists).toBe(true);
},
};
/**
* 类型定义 - Chat Store 状态
*/
export interface ChatStoreState {
messages: Array<{
id: string;
role: 'user' | 'assistant' | 'tool' | 'hand' | 'workflow';
content: string;
timestamp: string;
streaming?: boolean;
error?: string;
runId?: string;
toolName?: string;
toolInput?: string;
toolOutput?: string;
handName?: string;
handStatus?: string;
handResult?: unknown;
workflowId?: string;
workflowStep?: string;
workflowStatus?: string;
workflowResult?: unknown;
}>;
conversations: Array<{
id: string;
title: string;
messages: string[];
sessionKey: string | null;
agentId: string | null;
createdAt: string;
updatedAt: string;
}>;
currentConversationId: string | null;
currentAgent: {
id: string;
name: string;
icon: string;
color: string;
lastMessage: string;
time: string;
} | null;
isStreaming: boolean;
currentModel: string;
sessionKey: string | null;
}
/**
* 类型定义 - Team Store 状态
*/
export interface TeamStoreState {
teams: Array<{
id: string;
name: string;
description?: string;
members: Array<{
id: string;
agentId: string;
name: string;
role: 'orchestrator' | 'reviewer' | 'worker';
skills: string[];
workload: number;
status: 'idle' | 'working' | 'offline';
maxConcurrentTasks: number;
currentTasks: string[];
}>;
tasks: Array<{
id: string;
title: string;
description?: string;
status: 'pending' | 'assigned' | 'in_progress' | 'review' | 'completed';
priority: 'low' | 'medium' | 'high';
assigneeId?: string;
dependencies: string[];
type: string;
estimate?: number;
createdAt: string;
updatedAt: string;
}>;
pattern: 'sequential' | 'parallel' | 'pipeline';
activeLoops: Array<{
id: string;
developerId: string;
reviewerId: string;
taskId: string;
state: 'developing' | 'revising' | 'reviewing' | 'approved' | 'escalated';
iterationCount: number;
maxIterations: number;
}>;
status: 'active' | 'paused' | 'completed';
createdAt: string;
updatedAt: string;
}>;
activeTeam: {
id: string;
name: string;
} | null;
metrics: {
tasksCompleted: number;
avgCompletionTime: number;
passRate: number;
avgIterations: number;
escalations: number;
efficiency: number;
} | null;
isLoading: boolean;
error: string | null;
}