For the complete documentation index, see llms.txt. This page is also available as Markdown.

测试策略

OpenHuman 如何测试其产品——Vitest、cargo test、WDIO E2E。每种测试放在哪里。

OpenHuman 如何测试其产品。关于“我的测试该放哪儿?”的事实来源。配套于 TEST-COVERAGE-MATRIX.md.


层级

存放位置
测试内容
驱动层

Rust 单元

#[cfg(test)] mod tests 在同一个 *.rs 文件中,或相邻的 tests.rs,或 tests/ 领域下的子目录中(例如 src/openhuman/channels/tests/)

纯领域逻辑、schema、RPC 处理器形态、内存状态机

cargo test

Rust 集成

tests/*.rs 在仓库根目录

使用真实 Tokio 运行时、模拟外部服务、端到端 JSON-RPC 的完整领域联通(tests/json_rpc_e2e.rs),领域 × 领域交互

pnpm test:rust (它会调用 bash scripts/test-rust-with-mock.sh)

Vitest 单元

与源码同目录,作为 *.test.ts(x) 放在源文件旁边,位于 app/src/**,或位于 app/src/**/__tests__/

React 组件、hooks、store 切片、纯工具函数、服务层适配器

pnpm test:unit

WDIO E2E

app/test/e2e/specs/*.spec.ts

完整桌面流程:UI → Tauri → 进程内核心 → JSON-RPC;用户可见行为

所有平台:使用 Appium Chromium 驱动(端口 4723)对 CEF 运行时进行测试。参见 E2E 测试.

手动冒烟

驱动无法断言的 OS 级表面:TCC 权限弹窗、Gatekeeper、代码签名、DMG 安装、OS 原生 toast

在发布截点由人工执行,并在发布 PR 中签字确认


决策树 - 我的测试该放哪儿?

变更是否位于 JSON-RPC 边界之后(在 `src/` 中)?
├─ 是 - 它是否跨领域或与外部服务交互?
│   ├─ 是 → Rust 集成(tests/*.rs)
│   └─ 否 → Rust 单元(在源码旁边)
└─ 否 - 变更位于 `app/` 中
    ├─ 它是否是一个纯函数、hook、slice,或独立组件?
    │   └─ 是 → Vitest 单元(*.test.tsx 同目录)
    └─ 它是否用户可见,并且跨越 UI ⇄ Tauri ⇄ sidecar ⇄ JSON-RPC?
        ├─ 是 → WDIO E2E(app/test/e2e/specs/*.spec.ts)
        └─ 它是否属于 OS 级(TCC、Gatekeeper、安装、OS toast)?
            └─ 是 → 手动冒烟检查清单

如果变更涉及其中多个层级,请在 每个 它触及的层级中编写测试。不要用一个替代另一个。


失败路径要求

覆盖矩阵中的每个功能叶子都必须有 至少一个失败 / 边界 断言,除了正常路径之外。示例:

  • 文件写入工具:正常 = 写入字节;失败 = 路径限制拒绝。

  • OAuth 流程:正常 = 颁发令牌;边界 = 过期刷新令牌恢复。

  • 内存存储:正常 = 已存储 + 已召回;边界 = 先忘记再召回时返回空。

只断言正常路径的 spec 是不完整的。


模拟政策

  • 单元 / 集成 / E2E 中不使用真实网络。 使用共享的模拟后端(scripts/mock-api-core.mjs, scripts/mock-api-server.mjs, app/test/e2e/mock-server.ts).

  • 测试用管理端点: GET /__admin/health, POST /__admin/reset, POST /__admin/behavior, GET /__admin/requests.

  • 外部服务 (Telegram、Slack、Gmail、Notion、Ollama、OpenAI 等)都在模拟后端层被 stub;测试通过以下方式断言请求形状: getRequestLog().

  • 唯一可接受的例外是有文档记录的发布截点手动冒烟步骤。


确定性规则

  • 不要使用真实时间等待,请使用 waitForApp, waitForAppReady, waitForWebView 辅助函数,或显式的元素就绪谓词。

  • 不共享文件系统状态,每个 E2E spec 都运行在隔离的 OPENHUMAN_WORKSPACE (由 app/scripts/e2e-run-spec.sh).

  • 不要有依赖顺序的 spec,每个 spec 单独运行都必须通过。

  • 不要依赖绝对坐标或动画时序。

  • 更倾向于通过以下方式合成键盘输入: browser.execute(...) 而不是 browser.keys() (参见 command-palette.spec.ts 中的模式)。


现有测试框架提供了什么

  • 模拟后端启动: startMockServer / stopMockServerapp/test/e2e/mock-server.ts.

  • 认证快捷方式: triggerAuthDeepLink / triggerAuthDeepLinkBypasshelpers/deep-link-helpers.ts 跳过真实 OAuth。

  • 元素辅助函数: clickNativeButton, waitForWebView, clickTogglehelpers/element-helpers.ts,请使用这些而不是直接使用 XCUIElementType* 选择器。

  • 共享流程: completeOnboardingIfVisible, navigateViaHash, navigateToSkills, walkOnboardinghelpers/shared-flows.ts.

  • 从 spec 调用核心 RPC: callOpenhumanRpchelpers/core-rpc.ts,当某个 UI 步骤会很脆弱时,直接驱动 sidecar。

  • 平台守卫: isTauriDriver, isMac2, supportsExecuteScripthelpers/platform.ts (前两个是遗留兼容垫片——现在一切都运行在 Appium Chromium 驱动上)。

  • 失败时捕获工件: captureFailureArtifactswdio.conf.ts运行,截图 + DOM 转储会保存到 app/test/e2e/artifacts/.


命名 + 结构约定

  • WDIO specs: <feature-area>-flow.spec.ts 用于端到端产品流程; <feature>.spec.ts 用于更窄的表面。

  • Vitest 同目录放置:优先使用 Component.tsx + Component.test.tsx 作为相邻文件;仅在需要分组多个相关测试时使用 __tests__/ ,仅在需要将多个相关测试分组时使用。

  • Rust 集成测试:使用与表面对应的 snake_case 文件名, <feature>_e2e.rs 用于由 JSON-RPC 驱动的流程, <feature>_integration.rs 用于跨领域。

  • 每个 describe / mod tests block 映射到一个 feature-list ID 范围时,如果映射不明显,请在注释中链接对应的矩阵行。


合并前门禁

在打开 PR 之前运行。CI 会运行相同的集合,但本地运行更快:


无法通过驱动自动化 - 需要手动冒烟

某些表面无法由 WDIO / Appium 驱动,因为它们跨越 OS 级信任边界或硬件路径。完整检查清单 + 签字确认区位于 docs/RELEASE-MANUAL-SMOKE.md,该文件是每个发布必须验证内容的事实来源。它覆盖的示例包括:

  • macOS TCC 权限弹窗(辅助功能、输入监控、麦克风)

  • 首次启动时的 Gatekeeper 签名验证

  • 代码签名完整性(codesign --verify --deep --strict)

  • DMG 安装 / 拖到 Applications 流程

  • 自动更新下载 + 重新启动

  • Linux 上的 OS 原生通知 toast(除 Xvfb 外,驱动看不到显示服务器)

如果某个功能既没有自动化覆盖,也不在手动冒烟列表中,就将其视为未测试,并打开一个覆盖缺口。


将覆盖矩阵作为契约

中的每个功能叶子 覆盖矩阵 都映射到:

  1. 一个或多个测试路径,

  2. 一个有正当理由的 🚫 ,并有一条手动冒烟条目。

当你添加 / 删除 / 重命名某个功能时, 请在同一个 PR 中更新矩阵行。一旦 #965 落地,CI 将会守护这一契约。


拿不准时

  • 尽可能把测试往更低的层级堆栈推(Rust 单元 > Rust 集成 > Vitest > WDIO)。更低层级更快、更确定、运行成本更低。

  • WDIO 用于真正跨越 UI ⇄ Tauri ⇄ sidecar ⇄ JSON-RPC 的行为。不要仅仅因为有 UI,就把本可单元测试的关注点通过 WDIO 驱动。

  • 正常路径失败是回归。缺少失败路径测试是缺口。两者都是 bug。

最后更新于