> For the complete documentation index, see [llms.txt](https://tinyhumans.gitbook.io/openhuman/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tinyhumans.gitbook.io/openhuman/zh/kai-fa/testing-strategy.md).

# 测试策略

OpenHuman 如何测试其产品。关于“我的测试该放哪儿？”的事实来源。配套于 [`TEST-COVERAGE-MATRIX.md`](https://github.com/tinyhumansai/openhuman/tree/main/docs/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 测试](/openhuman/zh/kai-fa/e2e-testing.md). |
| **手动冒烟**      | [`docs/RELEASE-MANUAL-SMOKE.md`](https://github.com/tinyhumansai/openhuman/tree/main/docs/RELEASE-MANUAL-SMOKE.md) | 驱动无法断言的 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` / `stopMockServer` 在 `app/test/e2e/mock-server.ts`.
* **认证快捷方式**: `triggerAuthDeepLink` / `triggerAuthDeepLinkBypass` 在 `helpers/deep-link-helpers.ts` 跳过真实 OAuth。
* **元素辅助函数**: `clickNativeButton`, `waitForWebView`, `clickToggle` 在 `helpers/element-helpers.ts`，请使用这些而不是直接使用 `XCUIElementType*` 选择器。
* **共享流程**: `completeOnboardingIfVisible`, `navigateViaHash`, `navigateToSkills`, `walkOnboarding` 在 `helpers/shared-flows.ts`.
* **从 spec 调用核心 RPC**: `callOpenhumanRpc` 在 `helpers/core-rpc.ts`，当某个 UI 步骤会很脆弱时，直接驱动 sidecar。
* **平台守卫**: `isTauriDriver`, `isMac2`, `supportsExecuteScript` 在 `helpers/platform.ts` （前两个是遗留兼容垫片——现在一切都运行在 Appium Chromium 驱动上）。
* **失败时捕获工件**: `captureFailureArtifacts` 由 `wdio.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 会运行相同的集合，但本地运行更快：

```bash
# Rust 核心
cargo fmt --check
cargo check --manifest-path Cargo.toml
cargo clippy --manifest-path Cargo.toml -- -D warnings
cargo test --manifest-path Cargo.toml

# Tauri 外壳
cargo check --manifest-path app/src-tauri/Cargo.toml

# 前端
pnpm typecheck
pnpm lint
pnpm format:check
pnpm test:unit

# 使用模拟后端的 Rust 集成
pnpm test:rust

# E2E（慢 - 当行为有用户可见变化时运行）
pnpm test:e2e:build
bash app/scripts/e2e-run-spec.sh test/e2e/specs/<your-spec>.spec.ts <id>
```

***

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

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

* macOS TCC 权限弹窗（辅助功能、输入监控、麦克风）
* 首次启动时的 Gatekeeper 签名验证
* 代码签名完整性（`codesign --verify --deep --strict`)
* DMG 安装 / 拖到 Applications 流程
* 自动更新下载 + 重新启动
* Linux 上的 OS 原生通知 toast（除 Xvfb 外，驱动看不到显示服务器）

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

***

## 将覆盖矩阵作为契约

中的每个功能叶子 [覆盖矩阵](https://github.com/tinyhumansai/openhuman/tree/main/docs/TEST-COVERAGE-MATRIX.md) 都映射到：

1. 一个或多个测试路径， **或**
2. 一个有正当理由的 `🚫` ，并有一条手动冒烟条目。

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

***

## 拿不准时

* 尽可能把测试往更低的层级堆栈推（Rust 单元 > Rust 集成 > Vitest > WDIO）。更低层级更快、更确定、运行成本更低。
* WDIO 用于真正跨越 UI ⇄ Tauri ⇄ sidecar ⇄ JSON-RPC 的行为。不要仅仅因为有 UI，就把本可单元测试的关注点通过 WDIO 驱动。
* 正常路径失败是回归。缺少失败路径测试是缺口。两者都是 bug。
