mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-28 21:44:38 +00:00
8.2 KiB
8.2 KiB
description, icon, lang
| description | icon | lang |
|---|---|---|
| OpenHuman 如何测试其产品 —— Vitest、cargo test、WDIO E2E。每种测试该放哪里。 | vial | zh-CN |
测试策略
OpenHuman 如何测试其产品。"我的测试该放哪里?"的权威答案。 companion 文档为 TEST-COVERAGE-MATRIX.md。
测试层级
| 层级 | 存放位置 | 测试内容 | 驱动方式 |
|---|---|---|---|
| Rust 单元测试 | 同一 *.rs 文件内的 #[cfg(test)] mod tests,或同级 tests.rs,或域名下的 tests/ 子目录(例如 src/openhuman/channels/tests/) |
纯领域逻辑、schema、RPC handler 形态、内存状态机 | cargo test |
| Rust 集成测试 | 仓库根目录的 tests/*.rs |
完整领域接线,含真实 Tokio 运行时、模拟外部服务、JSON-RPC 端到端(tests/json_rpc_e2e.rs)、领域 × 领域交互 |
pnpm test:rust(调用 bash scripts/test-rust-with-mock.sh) |
| Vitest 单元测试 | 与源码共存于 app/src/** 下的 *.test.ts(x),或 app/src/**/__tests__/ 下 |
React 组件、hook、store slice、纯工具函数、service 层适配器 | pnpm test:unit |
| WDIO E2E | app/test/e2e/specs/*.spec.ts |
完整桌面流程:UI → Tauri → core sidecar → JSON-RPC;用户可见行为 | Linux CI: tauri-driver(端口 4444)。macOS 本地: Appium Mac2(端口 4723)。详见 E2E 测试。 |
| 手动冒烟测试 | docs/RELEASE-MANUAL-SMOKE.md |
驱动程序无法断言的 OS 级表面:TCC 权限弹窗、Gatekeeper、代码签名、DMG 安装、OS 原生通知 | 发布切割时由人工执行,在发布 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 通知)?
└─ 是 → 手动冒烟清单
如果一项变更触及多个层级,在每个触及的层级都写测试。不要用一层替代另一层。
失败路径要求
覆盖矩阵中的每个功能叶子节点,除了 happy path 外,至少还要有一个失败 / 边界断言。例如:
- 文件写入工具:happy = 写入了字节;failure = 路径限制拒绝。
- OAuth 流程:happy = 签发了 token;edge = 过期刷新 token 恢复。
- 记忆存储:happy = 存储并召回;edge = 遗忘后再召回返回空。
只断言 happy path 的 spec 是不完整的。
Mock 策略
- 单元 / 集成 / E2E 中禁止真实网络。 使用共享 mock 后端(
scripts/mock-api-core.mjs、scripts/mock-api-server.mjs、app/test/e2e/mock-server.ts)。 - 测试用 admin 端点:
GET /__admin/health、POST /__admin/reset、POST /__admin/behavior、GET /__admin/requests。 - 外部服务(Telegram、Slack、Gmail、Notion、Ollama、OpenAI 等)在 mock 后端层面被 stub;测试通过
getRequestLog()断言请求形态。 - 唯一可接受的例外是记录在案的发布切割手动冒烟步骤。
确定性规则
- 禁止 wall-clock 等待,使用
waitForApp、waitForAppReady、waitForWebView辅助函数,或显式的元素就绪谓词。 - 禁止共享文件系统状态,每个 E2E spec 在隔离的
OPENHUMAN_WORKSPACE中运行(由app/scripts/e2e-run-spec.sh创建/清理)。 - 禁止顺序依赖的 spec,每个 spec 必须能独立通过。
- 禁止依赖绝对坐标或动画时序。
- 禁止在 tauri-driver 上通过
browser.keys()使用真实键盘,通过browser.execute(...)合成(参见command-palette.spec.ts中的模式)。
现有 harness 提供的能力
- Mock 后端引导:
app/test/e2e/mock-server.ts中的startMockServer/stopMockServer。 - Auth 捷径:
helpers/deep-link-helpers.ts中的triggerAuthDeepLink/triggerAuthDeepLinkBypass跳过真实 OAuth。 - 元素辅助函数:
helpers/element-helpers.ts中的clickNativeButton、waitForWebView、clickToggle,在 spec 中使用这些代替原始的XCUIElementType*选择器。 - 共享流程:
helpers/shared-flows.ts中的completeOnboardingIfVisible、navigateViaHash、navigateToSkills、walkOnboarding。 - 从 spec 调用 Core RPC:
helpers/core-rpc.ts中的callOpenhumanRpc,当 UI 步骤可能脆弱时直接驱动 sidecar。 - 平台守卫:
helpers/platform.ts中的isTauriDriver、isMac2、supportsExecuteScript。 - 失败时捕获工件:
captureFailureArtifacts从wdio.conf.ts运行,截图 + DOM dump 输出到app/test/e2e/artifacts/。
命名与结构规范
- WDIO spec:端到端产品流用
<feature-area>-flow.spec.ts;更窄的表面用<feature>.spec.ts。 - Vitest 同位置:优先
Component.tsx+Component.test.tsx同级;仅在组合多个相关测试时使用__tests__/。 - Rust 集成测试:文件名用 snake_case 匹配表面,JSON-RPC 驱动流用
<feature>_e2e.rs,跨领域用<feature>_integration.rs。 - 每个
describe/mod tests块对应一个功能列表 ID 范围,如果映射不明显,在注释中链接矩阵行。
合并前门禁
开 PR 前运行。CI 会跑同一套,但本地更快:
# 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
# 带 mock 后端的 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,该文件是每次发布必须验证内容的权威来源。涵盖示例:
- macOS TCC 权限弹窗(辅助功能、输入监控、屏幕录制、麦克风)
- Gatekeeper 首次启动签名验证
- 代码签名完整性(
codesign --verify --deep --strict) - DMG 安装 / 拖入 Applications 流程
- 自动更新下载 + 重启
- Linux OS 原生通知 toast(无显示服务器的 driver 无法看见 Xvfb 之外的 Linux)
如果一项功能没有自动化覆盖,也不在手动冒烟清单上,视为未测试,开一个覆盖缺口。
覆盖矩阵即契约
覆盖矩阵 中的每个功能叶子节点映射到:
- 一个或多个测试路径,或
- 一个合理的
🚫并附手动冒烟条目。
当你添加 / 删除 / 重命名功能时,在同一 PR 中更新矩阵行。CI 将在 #965 落地后守卫此契约。
不确定时
- 尽可能把测试推到层级栈的底层(Rust 单元 > Rust 集成 > Vitest > WDIO)。更低层级更快、更确定、运行成本更低。
- WDIO 用于真正跨越 UI ⇄ Tauri ⇄ sidecar ⇄ JSON-RPC 的行为。不要仅仅因为 UI 存在就通过 WDIO 驱动一个可单元测试的关注点。
- 失败的 happy path 是回归。缺失的失败路径测试是缺口。两者都是 bug。