Files
zclaw_openfang/plans/toasty-wibbling-pudding.md
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

22 KiB

ZCLAW 前端完整验证方案

背景

当前 E2E 测试(functional-scenarios.spec.ts)主要验证 UI 表面:

  • 验证元素是否存在
  • 验证按钮能否点击
  • 验证对话框能否打开

问题:这些测试无法发现真正的功能问题,例如:

  • 点击按钮后 API 是否被正确调用
  • Store 状态是否正确更新
  • 数据是否正确渲染
  • 边界情况是否处理

目标

设计一套深度模拟用户操作的验证方案,确保:

  1. 完整操作流程 - 从用户视角完成端到端操作
  2. 数据流验证 - 检查 UI → Store → API → 后端的完整链路
  3. 状态变化验证 - 验证操作前后状态正确转换
  4. 结果展示验证 - 验证返回数据正确渲染到 UI
  5. 边界情况验证 - 验证错误处理、空状态、加载状态

验证架构

测试分层

┌─────────────────────────────────────────────────────────┐
│                    E2E 用户流程测试                       │
│  (Playwright - 完整用户操作 + 数据流验证)                  │
├─────────────────────────────────────────────────────────┤
│                    Store 状态测试                         │
│  (Vitest - Store 逻辑 + 状态转换验证)                     │
├─────────────────────────────────────────────────────────┤
│                    API 集成测试                           │
│  (MSW Mock - 请求/响应验证)                               │
└─────────────────────────────────────────────────────────┘

新增测试文件

desktop/tests/
├── e2e/
│   ├── specs/
│   │   ├── functional-scenarios.spec.ts  # 现有 - 保留
│   │   ├── data-flow.spec.ts             # 新增 - 数据流深度验证
│   │   ├── store-state.spec.ts           # 新增 - Store 状态验证
│   │   └── edge-cases.spec.ts            # 新增 - 边界情况测试
│   ├── fixtures/
│   │   ├── mock-gateway.ts               # 新增 - Gateway Mock
│   │   ├── test-data.ts                  # 新增 - 测试数据工厂
│   │   └── store-inspectors.ts           # 新增 - Store 检查工具
│   └── utils/
│       ├── network-helpers.ts            # 新增 - 网络拦截工具
│       ├── store-assertions.ts           # 新增 - Store 断言
│       └── user-actions.ts               # 新增 - 用户操作模拟
└── store/
    └── *.test.ts                         # 现有 - 扩展 Store 单元测试

模块验证用例

1. 聊天模块 (ChatArea.tsx + chatStore.ts)

数据流验证

用户输入 → sendMessage() → gateway-client.chatStream()
  → WebSocket 事件 → initStreamListener() → Store 更新 → UI 渲染

深度验证用例

用例 ID 验证场景 验证步骤 断言点
CHAT-DF-01 发送消息并接收流式响应 1. 输入消息 2. 点击发送 3. 拦截 WebSocket 4. 验证请求格式 5. 模拟流式响应 6. 验证 Store 状态 7. 验证 UI 渲染 ① 请求包含正确 sessionKey ② isStreaming=true ③ 消息增量追加 ④ streaming=false ⑤ messages 数组更新
CHAT-DF-02 模型切换 1. 点击模型选择器 2. 选择新模型 3. 发送消息 ① Store.currentModel 更新 ② 请求使用新模型 ③ UI 显示新模型名
CHAT-DF-03 Agent 切换 1. 当前有消息 2. 切换 Agent 3. 验证状态保存 4. 切换回来 ① 会话保存到 conversations ② 消息清空 ③ sessionKey 更新 ④ 切回后恢复原消息
CHAT-DF-04 新建对话 1. 发送消息 2. 点击新建 3. 验证状态 ① 消息清空 ② currentConversationId=null ③ sessionKey=null
CHAT-DF-05 网络错误处理 1. 模拟网络断开 2. 发送消息 ① 错误消息显示 ② isStreaming=false ③ error 字段设置 ④ 可重试
CHAT-DF-06 流式中断 1. 开始流式响应 2. 中途断开连接 ① 部分消息保留 ② 错误指示器显示 ③ 可恢复

Store 断言示例

// 验证 chatStore 状态
const chatState = await page.evaluate(() => {
  const stored = localStorage.getItem('zclaw-chat-storage');
  return stored ? JSON.parse(stored).state : null;
});

expect(chatState).toMatchObject({
  isStreaming: false,
  messages: expect.arrayContaining([
    expect.objectContaining({ role: 'user', content: 'test' }),
    expect.objectContaining({ role: 'assistant', streaming: false }),
  ]),
});

2. 分身管理 (CloneManager.tsx + agentStore.ts)

数据流验证

UI → createClone() → POST /api/clones → Response → clones 更新 → UI 重渲染

深度验证用例

用例 ID 验证场景 验证步骤 断言点
CLONE-DF-01 创建分身 1. 点击创建 2. 填写表单 3. 提交 4. 拦截 API 请求 ① POST 请求格式正确 ② Store.clones 增加 ③ UI 显示新分身
CLONE-DF-02 切换分身 1. 当前在 Agent A 2. 切换到 Agent B ① currentAgent 更新 ② 聊天消息切换 ③ sessionKey 更新
CLONE-DF-03 删除分身 1. 选择分身 2. 点击删除 3. 确认 ① DELETE API 调用 ② Store 移除 ③ 回退到默认
CLONE-DF-04 修改配置 1. 打开设置 2. 修改字段 3. 保存 ① PATCH 请求含 delta ② Store 更新 ③ UI 显示新值

3. Hands 系统 (HandsPanel.tsx + handStore.ts)

数据流验证

组件挂载 → loadHands() → GET /api/hands → 数据映射 → hands 数组 → UI 渲染
触发执行 → triggerHand() → POST /api/hands/:name/trigger → 状态变更 → UI 更新

深度验证用例

用例 ID 验证场景 验证步骤 断言点
HAND-DF-01 加载 Hands 列表 1. 导航到 Hands 2. 拦截 API 3. 验证映射 ① GET /api/hands 调用 ② 状态徽章正确 ③ requirements_met 显示
HAND-DF-02 触发 Hand 执行 1. 点击执行 2. 如有参数则填写 3. 提交 ① POST 触发请求 ② status → running ③ currentRunId 设置
HAND-DF-03 参数表单验证 1. 展开 Hand 2. 填写各类型参数 3. 验证失败 4. 修正后提交 ① 类型验证生效 ② 错误提示显示 ③ 正确值传递
HAND-DF-04 审批流程 1. 触发需审批 Hand 2. 状态 needs_approval 3. 批准/拒绝 ① 审批对话框显示 ② approveHand API 调用 ③ 状态更新
HAND-DF-05 执行历史 1. 查看 Hand 详情 2. 历史记录标签 ① GET /api/hands/:name/runs ② 记录正确显示

参数类型验证矩阵

参数类型 验证项
text 空值、最大长度、正则 pattern
number 边界值、min/max、非数字
boolean 切换状态、默认值
select 选项列表、默认选中
array 添加/删除项、空数组
object JSON 解析、格式验证
file 文件选择、大小限制

4. 自动化面板 (AutomationPanel.tsx)

深度验证用例

用例 ID 验证场景 验证步骤 断言点
AUTO-DF-01 分类过滤 1. 点击分类 2. 验证列表 ① 请求带 category 参数 ② 列表过滤正确 ③ 空状态处理
AUTO-DF-02 搜索功能 1. 输入关键词 2. 实时过滤 ① 匹配名称/描述 ② 高亮显示 ③ 无结果提示
AUTO-DF-03 批量执行 1. 多选 Hands 2. 批量触发 ① 选中状态管理 ② 依次触发 API ③ 进度显示
AUTO-DF-04 视图切换 1. 网格视图 2. 列表视图 ① 布局变化 ② 数据一致 ③ 响应式适配

5. 工作流编辑 (WorkflowEditor.tsx + workflowStore.ts)

深度验证用例

用例 ID 验证场景 验证步骤 断言点
WF-DF-01 创建工作流 1. 打开编辑器 2. 添加步骤 3. 配置参数 4. 保存 ① POST 请求格式 ② steps 数组正确 ③ Store 更新
WF-DF-02 步骤操作 1. 添加步骤 2. 上移/下移 3. 删除 ① 顺序更新 ② Hand 选择器 ③ JSON 参数解析
WF-DF-03 执行工作流 1. 选择工作流 2. 触发执行 ① POST execute ② 状态 running ③ 步骤进度显示
WF-DF-04 验证规则 1. 空名称 2. 无步骤 3. 保存 ① 验证错误显示 ② 禁用保存按钮

6. 技能市场 (SkillMarket.tsx)

深度验证用例

用例 ID 验证场景 验证步骤 断言点
SKILL-DF-01 浏览技能 1. 加载目录 2. 验证数据 ① GET /api/skills ② 分类显示 ③ 安装状态
SKILL-DF-02 搜索技能 1. 输入关键词 2. 验证匹配 ① 名称/能力/触发词匹配 ② 结果排序
SKILL-DF-03 安装技能 1. 点击安装 2. 等待完成 ① 安装 API 调用 ② 状态变更 ③ UI 更新
SKILL-DF-04 技能详情 1. 展开/点击 2. 查看详情 ① 触发词列表 ② 工具依赖 ③ 使用示例

7. 团队协作 (TeamList.tsx + teamStore.ts)

深度验证用例

用例 ID 验证场景 验证步骤 断言点
TEAM-DF-01 创建团队 1. 填写表单 2. 选择成员 3. 选择模式 4. 创建 ① POST 格式正确 ② members 数组 ③ pattern 字段
TEAM-DF-02 成员管理 1. 添加成员 2. 分配角色 3. 移除 ① Store 更新 ② 角色字段 ③ UI 反映
TEAM-DF-03 任务分配 1. 创建任务 2. 分配成员 3. 更新状态 ① tasks 数组 ② assigneeId ③ status 变化
TEAM-DF-04 Dev↔QA 循环 1. 启动循环 2. 提交审查 3. 反馈 4. 迭代 ① activeLoops 更新 ② iteration 递增 ③ state 转换

8. 设置页面 (Settings/*)

深度验证用例

用例 ID 验证场景 验证步骤 断言点
SET-DF-01 通用设置 1. 修改字段 2. 保存 ① PATCH 请求 ② Store 更新 ③ 成功提示
SET-DF-02 模型配置 1. 添加 API Key 2. 测试连接 ① 密钥掩码显示 ② 连接测试 API ③ 模型列表加载
SET-DF-03 MCP 服务 1. 查看列表 2. 启用/禁用 ① GET /api/plugins ② PATCH 状态 ③ UI 更新
SET-DF-04 审计日志 1. 打开日志 2. 筛选 3. 分页 ① 日志加载 ② 过滤参数 ③ 分页正确

9. 审批系统 (ApprovalQueue.tsx)

深度验证用例

用例 ID 验证场景 验证步骤 断言点
APPROVE-DF-01 查看队列 1. 加载审批列表 ① GET /api/approvals?status=pending ② 详情显示
APPROVE-DF-02 批准操作 1. 选择审批 2. 批准 ① POST respond (approved=true) ② 队列移除 ③ 状态更新
APPROVE-DF-03 拒绝操作 1. 选择审批 2. 拒绝 3. 填写原因 ① 原因字段 ② POST respond (approved=false) ③ 队列更新

边界情况验证清单

网络边界

场景 预期行为 验证方法
Gateway 断开 连接指示器红色,输入禁用,错误消息 模拟断开,检查 UI 状态
WebSocket 重连 自动重连尝试,状态保留,指示器显示 断开后恢复,验证状态
请求超时 超时错误显示,重试选项可用 设置短超时,检查处理
无效响应 解析错误处理,优雅降级 返回畸形 JSON
限流 (429) 用户通知,自动退避重试 模拟 429 响应

数据边界

场景 预期行为 验证方法
空消息列表 欢迎状态,开始提示 清空数据,检查 UI
超长消息 容器内滚动,布局不破 输入 10000+ 字符
空 Hands 列表 "无可用 Hands" + 连接提示 Mock 空响应
无效 JSON 参数 解析错误显示,原值保留 输入畸形 JSON
大文件上传 进度指示,大小限制检查 选择超大文件

状态边界

场景 预期行为 验证方法
快速连续点击 防抖,单次操作,正确最终状态 连续点击 10 次
流式响应中切换 取消/完成当前,干净切换 发消息后立即切换
浏览器刷新 从 localStorage 恢复,会话保留 刷新后检查状态
标签页切换 操作继续,返回时状态同步 切换标签再返回

实现方案

1. 网络拦截工具

// tests/e2e/utils/network-helpers.ts
export const networkHelpers = {
  // 拦截并记录所有 API 请求
  async interceptAllAPI(page: Page) {
    const requests: Array<{ url: string; method: string; body?: any }> = [];

    await page.route('**/api/**', async (route) => {
      const request = route.request();
      requests.push({
        url: request.url(),
        method: request.method(),
        body: request.postDataJSON(),
      });
      await route.continue();
    });

    return requests;
  },

  // Mock API 响应
  async mockAPI(page: Page, path: string, response: any, status = 200) {
    await page.route(`**/api/${path}**`, async (route) => {
      await route.fulfill({
        status,
        contentType: 'application/json',
        body: JSON.stringify(response),
      });
    });
  },

  // 模拟网络错误
  async simulateNetworkError(page: Page, path: string) {
    await page.route(`**/api/${path}**`, async (route) => {
      await route.abort('failed');
    });
  },

  // 模拟延迟响应
  async simulateDelay(page: Page, path: string, delayMs: number) {
    await page.route(`**/api/${path}**`, async (route) => {
      await new Promise(r => setTimeout(r, delayMs));
      await route.continue();
    });
  },
};

2. Store 检查工具

// tests/e2e/fixtures/store-inspectors.ts
export const storeInspectors = {
  // 获取持久化的 Store 状态
  async getPersistedState(page: Page, storeName: string) {
    return page.evaluate((name) => {
      const stored = localStorage.getItem(`zclaw-${name}-storage`);
      return stored ? JSON.parse(stored).state : null;
    }, storeName);
  },

  // 等待 Store 状态变化
  async waitForStateChange(page: Page, storeName: string, path: string, expectedValue: any) {
    await page.waitForFunction(
      ({ store, path, expected }) => {
        const stored = localStorage.getItem(`zclaw-${store}-storage`);
        if (!stored) return false;
        const state = JSON.parse(stored).state;
        const value = path.split('.').reduce((obj, key) => obj?.[key], state);
        return JSON.stringify(value) === JSON.stringify(expected);
      },
      { store: storeName, path, expected: expectedValue },
      { timeout: 5000 }
    );
  },

  // 验证 Store 状态
  async assertStoreState(page: Page, storeName: string, expected: Record<string, any>) {
    const state = await this.getPersistedState(page, storeName);
    for (const [key, value] of Object.entries(expected)) {
      expect(state?.[key]).toEqual(value);
    }
  },
};

3. 用户操作模拟

// tests/e2e/utils/user-actions.ts
export const userActions = {
  // 完整聊天流程
  async sendChatMessage(page: Page, message: string) {
    const chatInput = page.locator('textarea').first();
    await chatInput.waitFor({ state: 'visible' });
    await chatInput.fill(message);

    // 拦截请求
    const [request] = await Promise.all([
      page.waitForRequest('**/api/chat**'),
      page.getByRole('button', { name: '发送消息' }).click(),
    ]);

    return request;
  },

  // 创建分身完整流程
  async createClone(page: Page, data: { name: string; role?: string }) {
    await page.getByRole('button', { name: /创建|new/i }).click();
    await page.waitForSelector('[role="dialog"]');

    const dialog = page.locator('[role="dialog"]');
    await dialog.locator('input').first().fill(data.name);

    const [response] = await Promise.all([
      page.waitForResponse('**/api/clones'),
      dialog.getByRole('button', { name: /确认|创建|save/i }).click(),
    ]);

    return response;
  },

  // 触发 Hand 完整流程
  async triggerHand(page: Page, handName: string, params?: Record<string, any>) {
    // 导航到 Hands
    await page.getByRole('tab', { name: 'Hands' }).click();

    // 找到并点击 Hand
    const handCard = page.locator(`[data-hand-name="${handName}"]`).or(
      page.getByRole('button', { name: new RegExp(handName, 'i') })
    );
    await handCard.click();

    // 如果有参数表单,填写参数
    if (params) {
      for (const [key, value] of Object.entries(params)) {
        const input = page.locator(`[name="${key}"]`).or(
          page.locator(`label:has-text("${key}")`).locator('..').locator('input, textarea, select')
        );
        if (await input.isVisible()) {
          await input.fill(String(value));
        }
      }
    }

    // 触发执行
    const [request] = await Promise.all([
      page.waitForRequest(`**/api/hands/${handName}/trigger`),
      page.getByRole('button', { name: /执行|触发|run/i }).click(),
    ]);

    return request;
  },
};

4. 数据流测试示例

// tests/e2e/specs/data-flow.spec.ts
import { test, expect } from '@playwright/test';
import { networkHelpers } from '../utils/network-helpers';
import { storeInspectors } from '../fixtures/store-inspectors';
import { userActions } from '../utils/user-actions';

test.describe('聊天数据流验证', () => {
  test('CHAT-DF-01: 发送消息完整数据流', async ({ page }) => {
    // 1. 设置网络拦截
    const requests = await networkHelpers.interceptAllAPI(page);

    // 2. Mock 流式响应
    await networkHelpers.mockAPI(page, 'chat', {
      type: 'stream',
      messages: [
        { delta: '你好', phase: 'start' },
        { delta: '!我是', phase: 'delta' },
        { delta: 'AI 助手', phase: 'delta' },
        { phase: 'end' },
      ],
    });

    // 3. 发送消息
    await page.goto('http://localhost:1420');
    const request = await userActions.sendChatMessage(page, '测试消息');

    // 4. 验证请求格式
    const requestBody = request.postDataJSON();
    expect(requestBody).toMatchObject({
      message: '测试消息',
      sessionKey: expect.any(String),
    });

    // 5. 验证 Store 状态
    const chatState = await storeInspectors.getPersistedState(page, 'chat');
    expect(chatState.messages).toContainEqual(
      expect.objectContaining({ role: 'user', content: '测试消息' })
    );

    // 6. 验证 UI 渲染
    await expect(page.locator('[class*="message"]').filter({ hasText: '测试消息' })).toBeVisible();
    await expect(page.locator('[class*="assistant"]')).toBeVisible();
  });
});

验证执行

运行命令

# 运行所有 E2E 测试
pnpm --filter desktop test:e2e

# 运行数据流测试
pnpm --filter desktop playwright test -- --grep "数据流|data-flow"

# 运行边界情况测试
pnpm --filter desktop playwright test -- --grep "边界|edge-case"

# 调试模式
pnpm --filter desktop playwright test -- --headed --debug

# 生成报告
pnpm --filter desktop playwright show-report

CI 集成

# .github/workflows/frontend-verification.yml
name: Frontend Verification
on: [push, pull_request]

jobs:
  e2e-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v2
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install
      - run: pnpm --filter desktop exec playwright install --with-deps
      - name: Run E2E tests
        run: pnpm --filter desktop test:e2e
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: desktop/test-results/

关键文件清单

需要创建的文件

文件路径 用途
desktop/tests/e2e/specs/data-flow.spec.ts 数据流深度验证测试
desktop/tests/e2e/specs/store-state.spec.ts Store 状态验证测试
desktop/tests/e2e/specs/edge-cases.spec.ts 边界情况测试
desktop/tests/e2e/fixtures/mock-gateway.ts Gateway Mock 工具
desktop/tests/e2e/fixtures/test-data.ts 测试数据工厂
desktop/tests/e2e/fixtures/store-inspectors.ts Store 检查工具
desktop/tests/e2e/utils/network-helpers.ts 网络拦截工具
desktop/tests/e2e/utils/store-assertions.ts Store 断言工具
desktop/tests/e2e/utils/user-actions.ts 用户操作模拟

需要修改的文件

文件路径 修改内容
desktop/tests/e2e/playwright.config.ts 添加新测试配置
desktop/src/store/*.ts 添加测试辅助方法(可选)

成功标准

覆盖率目标

层级 目标 度量方式
E2E 用户流程 100% 10 个模块全覆盖
数据流路径 100% UI → Store → API → UI 验证
边界情况 80% 网络/数据/状态边界
错误处理 100% 所有错误状态

质量门槛

  • 所有测试在 CI 中通过
  • 正常流程无控制台错误
  • 网络故障优雅处理
  • 刷新后状态持久化
  • 所有用户输入有验证
  • 加载状态正确显示