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:
474
desktop/tests/e2e/utils/store-assertions.ts
Normal file
474
desktop/tests/e2e/utils/store-assertions.ts
Normal file
@@ -0,0 +1,474 @@
|
||||
/**
|
||||
* Store 断言工具
|
||||
* 提供类型安全的 Store 状态断言方法
|
||||
*/
|
||||
|
||||
import { Page } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
import { storeInspectors, STORE_NAMES, type StoreName } from '../fixtures/store-inspectors';
|
||||
|
||||
/**
|
||||
* 通用断言工具
|
||||
*/
|
||||
export const storeAssertions = {
|
||||
/**
|
||||
* 断言 Store 状态匹配预期对象
|
||||
*/
|
||||
async assertStoreState<T>(
|
||||
page: Page,
|
||||
storeName: StoreName,
|
||||
expected: Partial<T>
|
||||
): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<T>(page, storeName);
|
||||
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);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言 Store 字段包含特定值(数组或字符串)
|
||||
*/
|
||||
async assertFieldContains(
|
||||
page: Page,
|
||||
storeName: StoreName,
|
||||
fieldPath: string,
|
||||
expected: unknown
|
||||
): Promise<void> {
|
||||
const value = await storeInspectors.getStateField<unknown>(page, storeName, fieldPath);
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
expect(value).toContainEqual(expected);
|
||||
} else if (typeof value === 'string') {
|
||||
expect(value).toContain(expected);
|
||||
} else {
|
||||
throw new Error(`Field ${fieldPath} is not an array or string`);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言 Store 字段数组长度
|
||||
*/
|
||||
async assertArrayLength(
|
||||
page: Page,
|
||||
storeName: StoreName,
|
||||
fieldPath: string,
|
||||
expected: number
|
||||
): Promise<void> {
|
||||
const value = await storeInspectors.getStateField<unknown[]>(page, storeName, fieldPath);
|
||||
expect(Array.isArray(value)).toBe(true);
|
||||
expect(value?.length).toBe(expected);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言 Store 字段数组长度大于指定值
|
||||
*/
|
||||
async assertArrayLengthGreaterThan(
|
||||
page: Page,
|
||||
storeName: StoreName,
|
||||
fieldPath: string,
|
||||
min: number
|
||||
): Promise<void> {
|
||||
const value = await storeInspectors.getStateField<unknown[]>(page, storeName, fieldPath);
|
||||
expect(Array.isArray(value)).toBe(true);
|
||||
expect(value?.length).toBeGreaterThan(min);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言 Store 字段为真值
|
||||
*/
|
||||
async assertFieldTruthy(page: Page, storeName: StoreName, fieldPath: string): Promise<void> {
|
||||
const value = await storeInspectors.getStateField<unknown>(page, storeName, fieldPath);
|
||||
expect(value).toBeTruthy();
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言 Store 字段为假值
|
||||
*/
|
||||
async assertFieldFalsy(page: Page, storeName: StoreName, fieldPath: string): Promise<void> {
|
||||
const value = await storeInspectors.getStateField<unknown>(page, storeName, fieldPath);
|
||||
expect(value).toBeFalsy();
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言 Store 字段为 null
|
||||
*/
|
||||
async assertFieldNull(page: Page, storeName: StoreName, fieldPath: string): Promise<void> {
|
||||
const value = await storeInspectors.getStateField<unknown>(page, storeName, fieldPath);
|
||||
expect(value).toBeNull();
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言 Store 字段不为 null
|
||||
*/
|
||||
async assertFieldNotNull(page: Page, storeName: StoreName, fieldPath: string): Promise<void> {
|
||||
const value = await storeInspectors.getStateField<unknown>(page, storeName, fieldPath);
|
||||
expect(value).not.toBeNull();
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言 Store 字段匹配正则表达式
|
||||
*/
|
||||
async assertFieldMatches(
|
||||
page: Page,
|
||||
storeName: StoreName,
|
||||
fieldPath: string,
|
||||
pattern: RegExp
|
||||
): Promise<void> {
|
||||
const value = await storeInspectors.getStateField<string>(page, storeName, fieldPath);
|
||||
expect(value).toMatch(pattern);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 聊天相关断言
|
||||
*/
|
||||
export const chatAssertions = {
|
||||
/**
|
||||
* 断言消息数量
|
||||
*/
|
||||
async assertMessageCount(page: Page, expected: number): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{
|
||||
messages: unknown[];
|
||||
}>(page, STORE_NAMES.CHAT);
|
||||
expect(state?.messages?.length).toBe(expected);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言消息数量大于
|
||||
*/
|
||||
async assertMessageCountGreaterThan(page: Page, min: number): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{
|
||||
messages: unknown[];
|
||||
}>(page, STORE_NAMES.CHAT);
|
||||
expect(state?.messages?.length).toBeGreaterThan(min);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言最后一条消息内容
|
||||
*/
|
||||
async assertLastMessageContent(page: Page, expected: string | RegExp): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{
|
||||
messages: Array<{ content: string }>;
|
||||
}>(page, STORE_NAMES.CHAT);
|
||||
const lastMessage = state?.messages?.[state.messages.length - 1];
|
||||
expect(lastMessage).toBeDefined();
|
||||
|
||||
if (expected instanceof RegExp) {
|
||||
expect(lastMessage?.content).toMatch(expected);
|
||||
} else {
|
||||
expect(lastMessage?.content).toContain(expected);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言最后一条消息角色
|
||||
*/
|
||||
async assertLastMessageRole(
|
||||
page: Page,
|
||||
role: 'user' | 'assistant' | 'tool' | 'hand' | 'workflow'
|
||||
): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{
|
||||
messages: Array<{ role: string }>;
|
||||
}>(page, STORE_NAMES.CHAT);
|
||||
const lastMessage = state?.messages?.[state.messages.length - 1];
|
||||
expect(lastMessage?.role).toBe(role);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言流式状态
|
||||
*/
|
||||
async assertStreamingState(page: Page, expected: boolean): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{ isStreaming: boolean }>(
|
||||
page,
|
||||
STORE_NAMES.CHAT
|
||||
);
|
||||
expect(state?.isStreaming).toBe(expected);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言当前模型
|
||||
*/
|
||||
async assertCurrentModel(page: Page, modelId: string): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{ currentModel: string }>(
|
||||
page,
|
||||
STORE_NAMES.CHAT
|
||||
);
|
||||
expect(state?.currentModel).toBe(modelId);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言当前 Agent
|
||||
*/
|
||||
async assertCurrentAgent(page: Page, agentId: string): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{
|
||||
currentAgent: { id: string } | null;
|
||||
}>(page, STORE_NAMES.CHAT);
|
||||
expect(state?.currentAgent?.id).toBe(agentId);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言 sessionKey 存在
|
||||
*/
|
||||
async assertSessionKeyExists(page: Page): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{ sessionKey: string | null }>(
|
||||
page,
|
||||
STORE_NAMES.CHAT
|
||||
);
|
||||
expect(state?.sessionKey).not.toBeNull();
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言消息包含错误
|
||||
*/
|
||||
async assertLastMessageHasError(page: Page): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{
|
||||
messages: Array<{ error?: string }>;
|
||||
}>(page, STORE_NAMES.CHAT);
|
||||
const lastMessage = state?.messages?.[state.messages.length - 1];
|
||||
expect(lastMessage?.error).toBeDefined();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 连接相关断言
|
||||
*/
|
||||
export const connectionAssertions = {
|
||||
/**
|
||||
* 断言连接状态
|
||||
*/
|
||||
async assertConnectionState(
|
||||
page: Page,
|
||||
expected: 'connected' | 'disconnected' | 'connecting' | 'reconnecting'
|
||||
): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{ connectionState: string }>(
|
||||
page,
|
||||
STORE_NAMES.CONNECTION
|
||||
);
|
||||
expect(state?.connectionState).toBe(expected);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言已连接
|
||||
*/
|
||||
async assertConnected(page: Page): Promise<void> {
|
||||
await this.assertConnectionState(page, 'connected');
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言已断开
|
||||
*/
|
||||
async assertDisconnected(page: Page): Promise<void> {
|
||||
await this.assertConnectionState(page, 'disconnected');
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言 Gateway 版本
|
||||
*/
|
||||
async assertGatewayVersion(page: Page, expected: string): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{ gatewayVersion: string | null }>(
|
||||
page,
|
||||
STORE_NAMES.CONNECTION
|
||||
);
|
||||
expect(state?.gatewayVersion).toBe(expected);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言无连接错误
|
||||
*/
|
||||
async assertNoError(page: Page): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{ error: string | null }>(
|
||||
page,
|
||||
STORE_NAMES.CONNECTION
|
||||
);
|
||||
expect(state?.error).toBeNull();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Hands 相关断言
|
||||
*/
|
||||
export const handAssertions = {
|
||||
/**
|
||||
* 断言 Hands 列表非空
|
||||
*/
|
||||
async assertHandsNotEmpty(page: Page): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{ hands: unknown[] }>(
|
||||
page,
|
||||
STORE_NAMES.HAND
|
||||
);
|
||||
expect(state?.hands?.length).toBeGreaterThan(0);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言 Hands 列表数量
|
||||
*/
|
||||
async assertHandsCount(page: Page, expected: number): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{ hands: unknown[] }>(
|
||||
page,
|
||||
STORE_NAMES.HAND
|
||||
);
|
||||
expect(state?.hands?.length).toBe(expected);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言 Hand 状态
|
||||
*/
|
||||
async assertHandStatus(
|
||||
page: Page,
|
||||
handId: string,
|
||||
expected: string
|
||||
): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{
|
||||
hands: Array<{ id: string; status: string }>;
|
||||
}>(page, STORE_NAMES.HAND);
|
||||
const hand = state?.hands?.find((h) => h.id === handId);
|
||||
expect(hand?.status).toBe(expected);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言存在运行中的 Hand
|
||||
*/
|
||||
async assertHasRunningHand(page: Page): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{
|
||||
hands: Array<{ status: string }>;
|
||||
}>(page, STORE_NAMES.HAND);
|
||||
const hasRunning = state?.hands?.some((h) => h.status === 'running');
|
||||
expect(hasRunning).toBe(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言存在待审批的 Hand
|
||||
*/
|
||||
async assertHasPendingApproval(page: Page): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{
|
||||
approvals: unknown[];
|
||||
}>(page, STORE_NAMES.HAND);
|
||||
expect(state?.approvals?.length).toBeGreaterThan(0);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 分身/Agent 相关断言
|
||||
*/
|
||||
export const agentAssertions = {
|
||||
/**
|
||||
* 断言分身列表数量
|
||||
*/
|
||||
async assertClonesCount(page: Page, expected: number): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{ clones: unknown[] }>(
|
||||
page,
|
||||
STORE_NAMES.AGENT
|
||||
);
|
||||
expect(state?.clones?.length).toBe(expected);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言分身列表包含指定名称
|
||||
*/
|
||||
async assertClonesContains(page: Page, name: string): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{
|
||||
clones: Array<{ name: string }>;
|
||||
}>(page, STORE_NAMES.AGENT);
|
||||
const hasClone = state?.clones?.some((c) => c.name === name);
|
||||
expect(hasClone).toBe(true);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 团队相关断言
|
||||
*/
|
||||
export const teamAssertions = {
|
||||
/**
|
||||
* 断言团队数量
|
||||
*/
|
||||
async assertTeamsCount(page: Page, expected: number): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{ teams: unknown[] }>(
|
||||
page,
|
||||
STORE_NAMES.TEAM
|
||||
);
|
||||
expect(state?.teams?.length).toBe(expected);
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言活跃团队
|
||||
*/
|
||||
async assertActiveTeam(page: Page, teamId: string): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{
|
||||
activeTeam: { id: string } | null;
|
||||
}>(page, STORE_NAMES.TEAM);
|
||||
expect(state?.activeTeam?.id).toBe(teamId);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 工作流相关断言
|
||||
*/
|
||||
export const workflowAssertions = {
|
||||
/**
|
||||
* 断言工作流数量
|
||||
*/
|
||||
async assertWorkflowsCount(page: Page, expected: number): Promise<void> {
|
||||
const state = await storeInspectors.getPersistedState<{ workflows: unknown[] }>(
|
||||
page,
|
||||
STORE_NAMES.WORKFLOW
|
||||
);
|
||||
expect(state?.workflows?.length).toBe(expected);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 组合断言 - 用于复杂场景
|
||||
*/
|
||||
export const compositeAssertions = {
|
||||
/**
|
||||
* 断言完整的聊天状态(发送消息后)
|
||||
*/
|
||||
async assertChatStateAfterSend(
|
||||
page: Page,
|
||||
expected: {
|
||||
messageCount?: number;
|
||||
isStreaming?: boolean;
|
||||
lastMessageRole?: 'user' | 'assistant';
|
||||
}
|
||||
): Promise<void> {
|
||||
if (expected.messageCount !== undefined) {
|
||||
await chatAssertions.assertMessageCount(page, expected.messageCount);
|
||||
}
|
||||
if (expected.isStreaming !== undefined) {
|
||||
await chatAssertions.assertStreamingState(page, expected.isStreaming);
|
||||
}
|
||||
if (expected.lastMessageRole !== undefined) {
|
||||
await chatAssertions.assertLastMessageRole(page, expected.lastMessageRole);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 断言完整的应用状态(健康检查)
|
||||
*/
|
||||
async assertHealthyAppState(page: Page): Promise<void> {
|
||||
// 连接正常
|
||||
await connectionAssertions.assertNoError(page);
|
||||
|
||||
// 聊天可用
|
||||
const chatState = await storeInspectors.getPersistedState<{ isStreaming: boolean }>(
|
||||
page,
|
||||
STORE_NAMES.CHAT
|
||||
);
|
||||
expect(chatState?.isStreaming).toBe(false);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user