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

475 lines
13 KiB
TypeScript

/**
* 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);
},
};