> 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 如何使用 Vitest、cargo test 和 WDIO E2E 测试其产品，以及每种测试放在哪里。

本页回答“我的测试该放哪里？”。它是与之配套的页面 [`TEST-COVERAGE-MATRIX.md`](https://github.com/tinyhumansai/openhuman/blob/main/docs/TEST-COVERAGE-MATRIX.md).

***

## 层

| 层           | 存放位置                                                                                                                                                                                                                   | 测试内容                                                                          | 驱动                                                                                                                                    |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Rust 单元测试   | 同级 `<module>_tests.rs` 模块旁边的文件，位于 `crates/openhuman-core/src/<domain>/`，或者一个 `tests/` 某个领域下的子目录（例如 `crates/openhuman-core/src/channels/tests/`）；内联 `#[cfg(test)] mod` 块以及名为 `tests.rs`/`test.rs` 失败 `pnpm rust:layout` | 纯领域逻辑、schema、RPC 处理器结构、内存状态机                                                  | `cargo test`                                                                                                                          |
| Rust 集成测试   | `tests/*.rs` 位于仓库根目录下，每个都是一个显式的 `[[test]]` 目标项，位于 `crates/openhuman-cli/Cargo.toml` (`autotests = false`); `tests/raw_coverage/*.rs` 会被……通配收集到 `build.rs` 到单个 `raw_coverage_all` 目标                                    | 使用真实 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` （根目录）或 `pnpm --filter openhuman-app test:unit`                                                                            |
| WDIO 端到端测试  | `app/test/e2e/specs/*.spec.ts`                                                                                                                                                                                         | 完整桌面流程：UI → Tauri → 进程内核心 → JSON-RPC；用户可见行为                                   | 所有平台：Linux CI 驱动基于 Wry 的调试构建（在原生驱动出现之前，macOS 和 Windows 桌面 E2E 已禁用）。参见 [E2E 测试](/openhuman/zh/kai-fa/e2e-testing.md).                  |
| 手动冒烟测试      | [`docs/RELEASE-MANUAL-SMOKE.md`](https://github.com/tinyhumansai/openhuman/blob/main/docs/RELEASE-MANUAL-SMOKE.md)                                                                                                     | 驱动无法断言的操作系统级表面：TCC 权限弹窗、Gatekeeper、代码签名、DMG 安装、系统原生通知提示                       | 发布切分时由人工完成；已完成的签署块会作为 GitHub 提交评论粘贴到 `v<version>-staging` 经 QA 验证的带标签提交。晋升流程没有发布 PR。参见 [发布策略](/openhuman/zh/kai-fa/release-policy.md) |

***

## 我的测试该放哪里？

```
变更是否位于 JSON-RPC 边界之后（在 `crates/openhuman-core/src/`、`crates/openhuman-rpc/` 或 `crates/openhuman-embed/` 中）？
├─ 是 - 它是否跨领域或与外部服务通信？
│   ├─ 是 → Rust 集成测试 (tests/*.rs)
│   └─ 否 → Rust 单元测试（紧挨源码）
└─ 否 - 变更在 `app/` 中
    ├─ 它是纯函数、hook、slice，还是独立组件？
    │   └─ 是 → Vitest 单元测试（*.test.tsx 同级放置）
    └─ 它是否用户可见，且跨越 UI ⇄ Tauri ⇄ 嵌入式核心 ⇄ JSON-RPC？
        ├─ 是 → WDIO E2E（app/test/e2e/specs/*.spec.ts）
        └─ 它是否属于操作系统级（TCC、Gatekeeper、安装、系统通知）？
            └─ 是 → 手动冒烟检查清单
```

如果一个变更触及多个层，就在它触及的每一层都写测试。不要用一个层的测试替代另一个层。

### 测试归属

OpenHuman 测试组合、宿主策略、适配器、RPC 线协议约定和数据兼容性。模块行为在其归属的 `vendor/tiny*` 仓库中测试。调用供应商模块内部实现的宿主测试（例如， `tinychannels::providers::*` 或 `tinyinference_local::service::*`）应归上游所有。当代码移到上游时，在同一个上游 PR 中迁移其测试，然后在宿主 PR 中删除它们。每个模块保留一个宿主冒烟测试：加载它并通过总线调用一次。这足以测试宿主集成。

仅删除测试的 PR 不受变更行覆盖率约束，因为它没有供 diff-cover 度量的变更行。CI Lite 以 80% 的阈值运行该变更行检查。

***

## 失败路径要求

覆盖矩阵中的每个功能都至少需要一个失败或边界断言，以及一个成功路径。示例：

* 文件写入工具：成功 = 写入字节；失败 = 路径限制拒绝。
* OAuth 流程：成功 = 签发令牌；边界 = 过期刷新令牌恢复。
* 内存存储：成功 = 已存储 + 已取回；边界 = 先遗忘再取回返回空。

只断言成功路径的 spec 是不完整的。

***

## 属性测试回归

核心属性测试集 `failure_persistence: None` 在它们的 `ProptestConfig`；它们不会写入种子文件。当某个属性失败时，保留失败报告中显示的最小化输入以便复现，然后将该输入提升为属性测试旁边一个命名的单元测试。在发布前确认该显式案例在有缺陷的实现下会失败，并在修复后通过。

例如，租约模型将过期持有者重新获取的案例保留为 `reacquiring_at_expiry_advances_epoch_and_marks_previous_holder_unclean`，包含两个节点和以下步骤 `Acquire(0), Advance(1000), Acquire(0)`.

***

## 模拟策略

* 在单元、集成或 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`.
* 认证快捷方式：使用 `triggerAuthLoopbackBypass` 位于 `helpers/loopback-auth-helpers.ts`。它使用测试 JWT，但会在没有真实 OAuth 提供方的情况下触发回环监听器和 state-nonce 验证。
* 元素辅助函数： `clickNativeButton`, `waitForWebView` 和 `clickToggle` 位于 `helpers/element-helpers.ts`。请使用这些，而不要直接使用原始的 `XCUIElementType*` 选择器。
* 共享流程： `completeOnboardingIfVisible`, `navigateViaHash`, `navigateToSkills` 和 `walkOnboarding` 位于 `helpers/shared-flows.ts`.
* 从 spec 调用核心 RPC： `callOpenhumanRpc` 位于 `helpers/core-rpc.ts` 当 UI 步骤容易脆弱时，直接驱动嵌入式核心。
* 平台守卫： `isTauriDriver` 和 `supportsExecuteScript` 位于 `helpers/platform.ts`。桌面 E2E 通过……在 Xvfb 下运行调试二进制 `tauri-driver` 在 Linux 上。macOS 和 Windows 目前还没有自动化桌面会话。
* 失败时的制品捕获： `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` 块都映射到一个功能列表 ID 范围。如果映射不明显，请在注释中链接该矩阵行。

***

## 合并前门禁

在打开 PR 之前运行这些命令。CI 会运行同样的一组，但本地运行更快：

```bash
# Rust 核心
cargo fmt --check
cargo check --manifest-path Cargo.toml   # covers openhuman-core, openhuman-embed, openhuman-rpc, openhuman-tui
cargo clippy --manifest-path Cargo.toml -- -D warnings
cargo test --manifest-path Cargo.toml

# Tauri 外壳
cargo check --manifest-path crates/openhuman-app/Cargo.toml

# 前端
pnpm typecheck
pnpm lint
pnpm format:check
pnpm test            # vitest, via the app workspace

# 带模拟后端的 Rust 集成测试
pnpm test:rust

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

***

## 手动冒烟检查

WDIO 和 Appium 无法驱动某些表面，因为它们跨越操作系统级信任边界或硬件路径。 [`docs/RELEASE-MANUAL-SMOKE.md`](https://github.com/tinyhumansai/openhuman/blob/main/docs/RELEASE-MANUAL-SMOKE.md) 保存完整的检查清单和签署块。它是每个发布版本需要验证内容的事实来源。它涵盖的项目包括：

* macOS TCC 权限提示（Accessibility、Input Monitoring、Microphone）
* 首次启动时的 Gatekeeper 签名验证
* 代码签名完整性（`codesign --verify --deep --strict`)
* DMG 安装和拖拽到“应用程序”流程
* 自动更新下载和重新启动
* Linux 上的系统原生通知 toast（驱动在 Xvfb 之外看不到显示服务器）

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

***

## 将覆盖矩阵作为契约

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

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

当你新增、删除或重命名某个功能时，请在同一个 PR 中更新其矩阵行。

***

## 本地出现红色的 Rust 测试套件通常不是缺陷

这里的大多数本地 Rust 失败都来自环境，看起来和产品 bug 一模一样。在得出结论之前，先按这个列表逐项排查。每一步就是一条命令。每一步都对应在缺失它时你会看到的失败，因此你可以在 30 秒内检查它是否仍然成立。

不要相信直接告诉你原因的说明。写着“这些在本地失败，是预配问题”的一行话，会在调查开始前就把它结束。有一次它让某个工单去找预配缺口，而真实原因其实是产品缺陷（`agent definition \\`harness\` 未找到\`）。请按步骤来，不要靠传闻。

### 1. 有任何东西编译成功了吗？

`cargo` 在至少三种从 shell 看起来完全一样的情况下会退出 101：没有编译任何东西、某个测试断言为假，或者进程中止。

```bash
grep -c "^test result" run.log     # 0 = nothing ran
```

零 `测试结果` 行意味着问题出在编译或中止，而不是测试失败。请阅读第一条错误行：

* `无法加载依赖项 <x> 的源代码` 或 `没有这样的文件或目录` 表示某个子模块没有检出。
* `致命运行时错误：栈溢出` 带有 `signal: 6, SIGABRT` 且没有 `测试结果` 行意味着进程中止。参见第 3 步。

### 2. 所有深度的子模块都在吗？

有些 crate 依赖嵌套在其他子模块里的子模块。 `openhuman-tinyhumans` 是最明显的例子。它需要 `vendor/tinyhumans-sdk` （顶层）以及 `vendor/tinyagents/vendor/tinytools` （嵌套）。普通的 `git submodule update --init` 只会获取第一个并静默跳过第二个，因此你会得到看起来像测试失败的编译失败。

```bash
git submodule update --init
git -C vendor/tinyagents submodule update --init --recursive
```

没有这个嵌套模块时， `cargo test -p openhuman-tinyhumans` 会失败且没有 `测试结果` 行：

```
错误：无法将 `tinytools` 作为包 `openhuman-cli v0.63.31` 的依赖获取
  无法加载依赖项 `tinytools` 的源代码
  没有这样的文件或目录（操作系统错误 2）
```

检查内容，不要只看状态。 `git submodule status` 只会打印 `-` 仅针对未注册的模块，而注册发生在检出之前。因此，一个尚未完成的 init 可能看起来已就绪，而每个工作树其实都是空的。在 init 进行到一半时看到的情况：13 `+`，3 个空白，0 `-`，而 `du -sh vendor` 显示 120K，而 `ls vendor/tinychannels` 是空的。

```bash
du -sh vendor                      # 120K = empty; a real checkout is GBs
ls vendor/<module> | wc -l         # 0 = empty, whatever status says
```

### 3. 你的功能集和 CI 使用的是同一套吗？

`cargo test -p openhuman --lib` 在默认功能下构建出的产物比 CI 更小，而这种差异不仅会改变有哪些测试存在，还会改变测试结果。一个功能开关可以决定某个工具是否注册。测试可以通过检查构建后的二进制，将静态允许列表与实际发现的内容进行比较。然后这个开关会悄悄改变结果，而失败信息会指向某个产品概念，例如“带着工具但其提示词一个都没提到这些工具的代理”，却完全不提任何功能开关。

在一次比较中， `openhuman --lib` 在默认功能下失败了 5 个测试，在 CI 的功能集下失败了 3 个。不同的两个是 profile 产物： `every_prompt_names_at_least_one_tool_it_can_call` 依赖于其默认注册表，而 `the_withheld_block_renders_for_a_renamed_session_with_a_filter` 需要 `文档`。在断言任何内容之前，先完全复现 CI：

```bash
RUST_MIN_STACK=67108864 cargo test -p openhuman --lib \\
  --features "$(bash scripts/ci/product-features.sh --core)"
```

`RUST_MIN_STACK` 是按套件计算的，不是通用的。把它从 `openhuman-tinyhumans` 仍然会得到 `183 通过；0 失败`，所以那个 crate 不需要它。对于 `openhuman --lib`, `web_chat` 据称会以 `致命运行时错误：栈溢出` 没有它就会出错，这就是 CI 流水线导出它的原因。该中止在本地尚未复现，所以在其他地方依赖它之前要重新验证。

两个 crate，一个变量，相反的答案。不要条件反射式地添加前置条件。一个不必要的前置条件会让贡献者认为文档不可靠。

### 4. 到这一步才怀疑存在缺陷，并让沉默开口说话

如果它编译通过了，子模块就存在，而且功能集与 CI 一致，那么失败可能是真的。这个阶段的陷阱是某一层会正确地丢弃细节，比如 sanitizer、scrubber 或 redactor。症状是本应给出具体消息，却只出现了通用消息。

给丢弃那一行打补丁，让它打印出被丢弃的内容，运行一次，并读取底层错误。在一个案例中，一个经过净化的 `托管代理调用被策略拒绝` 隐藏了 `Validation("代理定义 \`harness\` 未找到")\`，这直接点出了缺陷。

### 为什么按这个顺序

便宜的检查要先做，因为前面出错，后面就看不出来了。一个从未编译成功的套件和一个失败的套件会报告同样的退出码，而一个看似合理却错误的解释会被确认，而不是被检验。运行 CI 的字面命令是避免这种情况的最可靠办法。

***

## 如有疑问

* 尽量把测试放在栈的更底层（Rust 单元测试，然后 Rust 集成测试，然后 Vitest，最后 WDIO）。更低的层更快、更确定，也更便宜。
* 只有在行为真正跨越 UI、Tauri、嵌入式核心和 JSON-RPC 时才使用 WDIO。不要仅仅因为有 UI，就把一个可以用单元测试覆盖的关注点交给 WDIO。
* 一个失败的“快乐路径”是回归。一个缺失的失败路径测试是漏洞。两者都是 bug。
