OpenHuman 测试策略实战:从 Vitest 前端单测、Rust 集成测试到 tauri-driver E2E 的完整测试体系
OpenHuman 是一个本地优先的开源个人 AI 应用(Mac / Windows / Linux),其前端为 React + Vite + Tauri,后端核心为 Rust 工作区。仓库中的 test-agent 子代理定义 集中描述了该项目的测试策略骨架:前端单元测试(Vitest + Testing Library)、Rust 单元测试、集成测试与 E2E 配置、移动端测试以及 CI 集成。本文以该文档为主线,逐节继承其中的配置、命令与代码示例,并结合仓库中真实落地的配置文件(如 app/test/vitest.config.ts、scripts/test-rust-with-mock.sh)补充默认值、端口与隔离策略等细节,帮助读者掌握一套可直接复现的前后端全栈测试方案。
test-agent 子代理:职责与能力边界
test-agent 以 Claude Code 子代理(subagent)的形式定义在 .claude/agents/test-agent.md 中,其 YAML frontmatter 声明了运行身份:
---
name: test-agent
description: Manages testing strategies for both frontend and backend code across all platforms
model: sonnet
color: yellow
---
其声明的四项核心能力与后文各章节一一对应:
- 运行前端单元测试(Vitest);
- 运行 Rust 单元测试(cargo test);
- 搭建集成测试;
- 配置 E2E 测试(tauri-driver / WebDriver 路线)。
值得注意的是,该代理覆盖的是"前端 + 后端"双栈测试策略:OpenHuman 的仓库实际是 pnpm workspace 与 Cargo workspace 的混合体(根目录 pnpm-workspace.yaml、Cargo.toml),前端位于 app/ 子包,Rust 核心位于仓库根的 src/,Tauri 壳位于 app/src-tauri/。理解这一布局是理解下文所有测试命令的前提。
前端测试(Vitest + Testing Library + jsdom)
依赖安装
文档给出的最小安装命令如下,用于引入测试运行器、React 测试工具链与 jsdom 环境:
# Install testing dependencies
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
对照仓库实际依赖(app/package.json 的 devDependencies),当前版本基线为 vitest ^4.0.18、@testing-library/react ^16.3.2、@testing-library/jest-dom ^6.9.1、jsdom ^28.0.0,并额外包含 @testing-library/user-event(真实用户交互模拟)、@vitest/coverage-v8(V8 覆盖率)、@playwright/test、webdriverio 与整套 @wdio/* 包(供 E2E 使用)。仓库使用 pnpm 而非 npm,实际安装时应使用 pnpm 对应命令。
Vitest 配置
文档中的最小配置模板:
// vitest.config.ts
import react from '@vitejs/plugin-react';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [react()],
test: { environment: 'jsdom', setupFiles: './src/test/setup.ts', globals: true },
});
而仓库中的实际配置落在 app/test/vitest.config.ts,由 pnpm test 通过 --config test/vitest.config.ts 显式指定。相对最小模板,实际配置增加了几个关键工程决策:
- Node polyfills 注入:通过
vite-plugin-node-polyfills为 jsdom 环境注入buffer、process、util、os、crypto、stream等全局对象,解决前端代码依赖 Node 内置模块的问题; - 路径别名:
@指向src/,并将 workspace 内的tauri-plugin-ptt-api解析到 packages/tauri-plugin-ptt/guest-js/index.ts,保证跨包导入在测试中可用; - 单 worker 串行执行:
maxWorkers: 1, minWorkers: 1,配合全量 V8 覆盖率插桩时牺牲速度换取确定性; - mock 重置策略:
clearMocks: true但mockReset: false、restoreMocks: false——注释明确说明mockReset会清除 setup.ts 中共享 mock 的实现(如getBackendUrl),因此只清调用历史、保留实现; - 超时放宽:
hookTimeout与testTimeout均为 30000ms; - 覆盖率范围:
provider: "v8",include 为src/**/*.{ts,tsx},并显式排除类型声明文件、src/test/**与仅用于开发调试的src/pages/dev/**;reporter 输出text、text-summary、html、lcov四种格式。
全局 setup:把"模拟一个 Tauri 应用运行时"做成基础设施
文档中的最小 setup 只 mock 了 invoke:
// src/test/setup.ts(文档最小模板)
import '@testing-library/jest-dom';
import { vi } from 'vitest';
// Mock Tauri APIs
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn() }));
仓库中真正的 app/src/test/setup.ts(约 390 行)则把这套思路做成了完整的基础设施,其要点包括:
- 内置 mock 后端:setup 启动阶段直接导入 scripts/mock-api-core.mjs 并调用
startMockServer(port, { retryIfInUse: true }),默认端口 5005(可由VITEST_MOCK_API_PORT/MOCK_API_PORT覆盖),随后把VITEST_MOCK_API_URL、VITE_BACKEND_URL写入环境变量,使所有单元测试天然对着一个本地 HTTP mock 后端运行;afterAll中stopMockServer()收尾。 - jsdom 缺失 API 的成批 polyfill:
window.matchMedia(Rive / 媒体查询 hooks)、ResizeObserver(cmdk / Radix)、scrollIntoView(cmdk)、HTMLElement.prototype.scrollTo(assistant-ui 线程视口)、Range.getBoundingClientRect/getClientRects(Lexical 选区测量)、Pointer Capture 三件套(Radix 的 Select / Slider / Toggle / DropdownMenu 在 pointerdown 时无条件调用)、IntersectionObserver(Radix 懒挂载)、以及 jsdom 28 缺失的PointerEvent构造器(否则@testing-library/user-event只能回退到 MouseEvent,Radix 的 pointer 处理器永远不触发)。 - Tauri 运行时代替:向
window.__TAURI_INTERNALS__注入一个 no-op 的invoke,使isTauri()的 IPC-ready 检查默认通过;同时vi.mock掉@tauri-apps/api/core(invoke+isTauri: () => false)、@tauri-apps/api/event、@tauri-apps/plugin-deep-link、@tauri-apps/plugin-opener、@tauri-apps/plugin-os(固定返回macos),以及项目自己的utils/tauriCommands模块(storeSession、getAuthState、openhumanService*等服务命令全部返回确定性的成功值)。 - 重量级第三方库降级:
redux-persist被 mock 为直接返回基础 reducer 的 no-op persistor(规避 CJS/ESM 问题),redux-logger、@sentry/react同样整体替换为空实现。 - 测试隔离钩子:
afterEach中清空 mock 后端的请求日志(clearRequestLog)、执行 Testing Library 的cleanup()、并重新播种__TAURI_INTERNALS__(因为部分测试会delete它来走 CEF 缺失分支);beforeEach中重置 mock 行为(resetMockBehavior)与 MCP 限流器的模块级计数器。 - 输出治理:未设置
DEBUG_TESTS=1时,console.log/info/debug/warn/error全部静音,保持测试输出干净。
从源码结构看,这种"setup 即环境"的写法解释了为什么 vitest 配置敢开全量 V8 覆盖率:每个测试文件启动即获得一个隔离的 mock 后端与完整的 jsdom 补丁,测试用例本身几乎不需要关心环境搭建。
编写测试
文档给出的组件测试示例,核心是"渲染 + 断言 + mock IPC 调用"三步:
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { invoke } from '@tauri-apps/api/core';
import App from './App';
describe('App', () => {
it('renders greeting button', () => {
render(<App />);
expect(screen.getByText('Greet')).toBeInTheDocument();
});
it('calls greet command on click', async () => {
vi.mocked(invoke).mockResolvedValue('Hello, World!');
render(<App />);
fireEvent.click(screen.getByText('Greet'));
expect(invoke).toHaveBeenCalledWith('greet', { name: expect.any(String) });
});
});
这里 vi.mocked(invoke) 的类型收窄技巧值得保留:因为 setup.ts 已全局 mock @tauri-apps/api/core,invoke 天然是 vi.fn(),vi.mocked() 让 TypeScript 能识别它的 mockResolvedValue / toHaveBeenCalledWith 等 mock API。仓库中的真实用例(如 app/AppRoutes.redirects.test.tsx、app/src/components 下大量 *.test.tsx)延续了同样的模式:用 screen.getBy* 定位、用 expect(invoke).toHaveBeenCalledWith('命令名', 参数) 验证前端是否正确发起 Tauri IPC 调用,而不是真实调用 Rust 侧。
运行命令
文档给出的运行方式:
npm test # Run all tests
npm test -- --watch # Watch mode
npm test -- --coverage # Coverage
在 app/package.json 中,这些能力被拆分为显式脚本(注意仓库使用 pnpm):
{
"scripts": {
"test": "vitest run --config test/vitest.config.ts",
"test:unit": "vitest run --config test/vitest.config.ts",
"test:unit:watch": "vitest --config test/vitest.config.ts",
"test:watch": "vitest --config test/vitest.config.ts",
"test:coverage": "vitest run --config test/vitest.config.ts --coverage"
}
}
即 test 等价于"一次性跑完",test:watch 才是文档中 --watch 的对应物。
Rust 测试
单元测试写法
文档中在 Tauri 壳 src-tauri/src/lib.rs 内联 #[cfg(test)] mod tests 的示例展示了两种基础形态:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greet() {
let result = greet("World");
assert!(result.contains("World"));
}
#[tokio::test]
async fn test_async_command() {
let result = fetch_data("https://example.com").await;
assert!(result.is_ok());
}
}
同步逻辑用 #[test],异步逻辑用 #[tokio::test],这一原则在 OpenHuman 的 Rust 核心中同样成立。从源码结构看,仓库的 Rust 核心把测试组织为与源码同目录的独立 *_tests.rs 文件(而非仅内联 mod),例如 src/core/auth.rs 有 20 个 #[test]、src/core/cli_tests.rs 23 个、src/core/all_tests.rs 86 个且其中包含大量 #[tokio::test](如 src/core/all_tests.rs#L722-L736)。这种"实现文件 + 伴生测试文件"的布局让 cargo test --lib 可以统一编译并运行所有核心模块的单元测试。
运行命令与仓库增强
文档的基本运行方式:
cd src-tauri
cargo test
cargo test -- --nocapture # 带 stdout 输出
cargo test test_greet # 运行指定测试
这些命令对 app/src-tauri 这个 Tauri 壳仍然成立。但对 OpenHuman 的产品级 Rust 测试面,仓库提供的是 pnpm test:rust(见 app/package.json),它委托给 scripts/test-rust-with-mock.sh。该脚本的工程要点值得逐个展开:
- 共享 mock 后端:脚本先启动 scripts/mock-api-server.mjs(默认
MOCK_API_PORT=18505,日志写到/tmp/openhuman-mock-api.log),以__admin/health端点做 30 秒健康轮询,随后导出BACKEND_URL/VITE_BACKEND_URL指向该 mock——Rust 集成测试因此全程 hermetic(无外部网络依赖); - 栈空间保护:
RUST_MIN_STACK默认提升到 16MB(scripts/test-rust-with-mock.sh#L50),注释说明 agent harness 的异步 future 在 debug 构建下非常大,Apple Silicon 上默认测试线程栈会栈溢出; - 产品特性集:从 scripts/ci/product-features.txt 读取特性列表,以
cargo test --workspace --features "${PRODUCT_FEATURES},bin-tools"运行,避免四个required-features集成目标(json_rpc_e2e、raw_coverage_all、observability_smoke、x402_twit_sh_live)被静默跳过; - 原生测试模块构建:按固定子模块构建 TinyMemory / TinyJuice / TinyConnectors 的测试
.so并通过TINYMEMORY_TEST_MODULE等环境变量注入;钱包模块则从校验和固定的 release 归档下载(SHA256 校验后解压),保证测试可复现; - 进程级隔离:
raw_coverage类模块与json_rpc_e2e用例会逐个在独立的 cargo 进程中以--test-threads=1运行(如 tests/raw_coverage/ 下 70 余个模块、tests/json_rpc_e2e.rs),因为这类用例会修改进程级全局状态,串行独立进程是保证确定性的手段; - 用法:无参运行完整套件;
--test <name>透传给cargo test跑指定目标,还支持--test raw_coverage_all/--test json_rpc_e2e两个特殊入口分别走隔离执行路径。
集成测试本体集中在仓库根的 tests/ 目录(agent_harness_e2e.rs、memory_tree_summarizer_e2e.rs、mcp_registry_e2e.rs 等),它们通过 mock 后端驱动完整的 JSON-RPC / 记忆 / 编排链路,属于"后端集成测试"层。
集成 / E2E 测试
文档给出的 tauri-driver 路线
文档建议通过 cargo install tauri-driver 安装 WebDriver,然后用 WebDriver 协议驱动应用:
const { Builder, By } = require('selenium-webdriver');
describe('App E2E', () => {
let driver;
beforeAll(async () => {
driver = await new Builder().usingServer('http://localhost:4444').forBrowser('tauri').build();
});
afterAll(async () => {
await driver.quit();
});
it('shows greeting', async () => {
const button = await driver.findElement(By.css('button'));
await button.click();
const message = await driver.findElement(By.css('.message'));
expect(await message.getText()).toContain('Hello');
});
});
仓库的落地实现沿用了同一条技术路线(tauri-driver 监听 127.0.0.1:4444 的 WebDriver 协议),但驱动框架从 selenium-webdriver 换成了 WebDriverIO + Mocha(app/package.json 中的 webdriverio ^9.24.0 与 @wdio/* 系列),配置位于 app/test/wdio.conf.ts。从源码结构看,其关键设计有:
- 单一 WebDriver 会话:
maxInstances: 1且不做跨 spec 拆会话——所有 spec 顺序运行在同一个应用进程中,省去重启成本,测试因此被设计为顺序依赖、由每个 spec 自行负责需要的状态重置; - mock 状态按 spec 文件重置:每个 spec 文件首次执行时向 mock 后端的
__admin/reset发一次 POST(端口来自BACKEND_URL或E2E_MOCK_PORT,默认 18473),防止某个 spec 失败后污染下一个文件; - 失败产物保留:
trace: 'retain-on-failure'策略下自动截图/录屏/保存 trace,并在失败时调用captureFailureArtifacts(app/test/e2e/helpers/artifacts); - 运行入口:app/scripts/e2e-run-spec.sh 是薄壳,接收 spec 路径后
exec到 app/scripts/e2e-run-session.sh,由后者负责启动 tauri-driver、等待其/status端点就绪,再调用 wdio;spec 文件位于 app/test/e2e/specs/。
网页端 E2E(Playwright)
除原生桌面端外,app/ 还提供独立的 web 端 E2E 通道:app/playwright.config.ts 将 testDir 指向 test/playwright/specs,baseURL 默认 http://127.0.0.1:4173(Vite preview 端口),单 worker、CI 下 2 次重试与 90s 超时。对应脚本为 pnpm test:e2e:web(构建 web 目标后跑 app/scripts/e2e-web-session.sh),以及按流程拆分的 test:e2e:login、test:e2e:auth、test:e2e:mega(app/scripts/e2e-login.sh、app/scripts/e2e-run-spec.sh 等)。
移动端测试
文档给出的两条命令分别覆盖 Android instrumented test 与 iOS XCTest:
# Android:运行 instrumented tests
cd src-tauri/gen/android
./gradlew connectedAndroidTest
# iOS:运行 XCTest
xcodebuild test \
-project src-tauri/gen/apple/tauri-app.xcodeproj \
-scheme tauri-app \
-destination 'platform=iOS Simulator,name=iPhone 15'
结合仓库现状:Tauri 生成的 gen/android、gen/apple 工程由初始化脚本产出(scripts/android-init.sh、scripts/ios-init.sh),app/package.json 中也有 tauri:ios:init、tauri:android:init、tauri:ios:build、release:android:play 等配套脚本,并依赖 IPHONEOS_DEPLOYMENT_TARGET(默认 16.0)。devDependencies 中同时存在 @wdio/appium-service,说明移动端 UI 自动化走的是 Appium + WebDriverIO 通道(配套解析脚本见 app/scripts/e2e-resolve-node-appium.sh)。文档中的 gradlew / xcodebuild 命令在本地生成工程后可直接使用;iOS 模拟器名称需按本机已安装的模拟器调整。
测试脚本编排与 CI 集成
文档建议的 package.json 脚本集:
{
"scripts": {
"test": "vitest",
"test:watch": "vitest --watch",
"test:coverage": "vitest --coverage",
"test:rust": "cd src-tauri && cargo test",
"test:all": "npm test && npm run test:rust"
}
}
仓库实际编排(app/package.json)在此之上把 E2E 也纳入了 test:all:
{
"scripts": {
"test": "vitest run --config test/vitest.config.ts",
"test:rust": "bash ../scripts/test-rust-with-mock.sh",
"test:e2e": "pnpm test:e2e:web && pnpm test:e2e:mega",
"test:all": "pnpm test:coverage && pnpm test:rust && pnpm test:e2e"
}
}
即"前端覆盖率 + Rust mock 后端全量测试 + 双通道 E2E"构成完整门禁。
文档给出的 CI 示例(GitHub Actions):
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: dtolnay/rust-toolchain@stable
- run: npm ci
- run: npm test
- run: cd src-tauri && cargo test
对照仓库实际约定有两处适配:依赖安装用 pnpm install --frozen-lockfile(锁定文件为 pnpm-lock.yaml),Rust 测试一步替换为调用 bash scripts/test-rust-with-mock.sh(它自带 mock 后端、特性集与原生模块构建)。此外仓库提供了 scripts/test-ci-local.sh 用于本地复现 CI 行为,以及 scripts/ci/ 目录下的覆盖率存在性检查(assert-coverage-presence.sh、rust-coverage-changed.sh)等辅助门禁,可视为该 CI 思路的落地形态。
小结:三层测试金字塔在 OpenHuman 中的落点
| 层次 | 文档中的定位 | 仓库实际落点 |
|---|---|---|
| 前端单元测试 | Vitest + Testing Library + jsdom,mock invoke |
app/test/vitest.config.ts + app/src/test/setup.ts,内置 mock 后端(端口 5005),pnpm test / test:coverage |
| Rust 单元/集成测试 | cargo test + #[test] / #[tokio::test] |
伴生 *_tests.rs(如 src/core/all_tests.rs)+ tests/ 集成面,统一由 scripts/test-rust-with-mock.sh 驱动(mock 端口 18505) |
| E2E | tauri-driver + WebDriver(selenium-webdriver 示例) | WebDriverIO + tauri-driver(127.0.0.1:4444,单会话顺序 spec,app/test/wdio.conf.ts);web 端 Playwright(app/playwright.config.ts) |
| 移动端 | gradlew / xcodebuild 命令 | 由 scripts/android-init.sh / scripts/ios-init.sh 生成工程后执行,Appium 通道可选 |
test-agent 的价值在于把"前端 mock 策略、Rust 测试运行器、E2E 驱动方式、CI 编排"收敛成一个可被其他代理直接调用的策略清单;而仓库中 app/src/test/setup.ts 与 scripts/test-rust-with-mock.sh 则展示了这份清单在生产级工程中的真实深度——每一个看似简单的"mock 一下 Tauri API"背后,都是对 jsdom 能力边界、Rust 进程级状态与覆盖率确定性的系统性处理。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00