OpenHands Agent Canvas 测试矩阵:Install × OS × Agent 的发布验收体系与自动化测试落地
本文以仓库中 docs/TESTING_MATRIX.md 定义的测试矩阵为主线,解读 OpenHands Agent Canvas 如何在"安装方式(npm / Docker)× 操作系统 × Agent 后端"的交叉维度上组织发布前的验收工作:P0/P1/P2 优先级如何落到具体检查项、四套自动化测试(vitest 单测、mock-LLM E2E、Docker mock-LLM E2E、live E2E)分别覆盖矩阵中的哪些格子、以及当前 CI 尚未覆盖的空白区域。读完后你可以直接按矩阵执行手工验收、复现各条 E2E 流水线,并理解每一项背后的工程实现。
优先级定义:P0 / P1 / P2
矩阵文档开篇给出全篇的优先级键(priority key),它决定了哪些格子必须在发布前通过:
| 优先级 | 含义 |
|---|---|
| P0 | 任何 release 之前必须通过(must pass before any release) |
| P1 | GA(正式发布)之前必须通过(must pass before GA) |
| P2 | best-effort,尽力而为 |
这一约定贯穿后文所有矩阵:手工矩阵(Install × OS × Agent)里每个格子都是一次"冒烟测试"(smoke test),而 Automations 与 Auth Modes 矩阵则直接标注了格子对应的优先级。
Install × OS × Agent:安装冒烟矩阵
矩阵文档的第一张表定义了最基础的验收单元:每个格子 = 一次完整冒烟流程:安装 → onboarding → 发起对话 → agent 有回复(install → onboard → start conversation → agent replies)。矩阵的行是"安装方式 × Agent 后端"的组合,列是操作系统:
| macOS | Linux | Windows | |
|---|---|---|---|
| npm — OpenHands | ☐ | ☐ | ☐ |
| npm — Claude Code | ☐ | ☐ | ☐ |
| npm — Codex | ☐ | ☐ | ☐ |
| npm — Gemini CLI | ☐ | ☐ | ☐ |
| npm — Custom ACP | ☐ | ☐ | ☐ |
| Docker — OpenHands | ☐ | ☐ | ☐ |
| Docker — Claude Code | ☐ | ☐ | ☐ |
| Docker — Codex | ☐ | ☐ | ☐ |
| Docker — Gemini CLI | ☐ | ☐ | ☐ |
| Docker — Custom ACP | ☐ | ☐ | ☐ |
行中的 Agent 后端对应两条接入路径:
- OpenHands:Agent Canvas 的原生 agent-server 路径。从 config/defaults.json 可以看到,npm 与 Docker 两条安装路径共用同一份版本钉(version pin):当前仓库锁定
agentServer: 1.44.0、agentCanvas: 1.16.0、automation: 1.9.0,最低兼容minimumAgentServer: 1.28.0。该文件在注释中明确说明自己是"npm 和 Docker 安装路径共享的版本、端口、路径与默认值的唯一事实来源",被 scripts/dev-safe.mjs、scripts/dev-with-automation.mjs 和 docker/entrypoint.sh 共同读取——这保证了矩阵中 npm 与 Docker 两行验收的是同一版本语义。 - Claude Code / Codex / Gemini CLI / Custom ACP:通过 Agent Client Protocol(ACP)接入的第三方 CLI agent。仓库中 src/constants/acp-providers.ts 及其测试 tests/constants/acp-providers.test.ts 维护了这些 provider 的定义;ACP 冒烟路径还受到
config/defaults.json中constraints.agentClientProtocol(agent-client-protocol<0.11)这一临时上界的约束——注释解释了 acp 0.11.0 重排了prompt()参数会破坏 SDK 的 ACP 客户端,因此在 openhands-sdk 修复前必须钉住<0.11。
"Custom ACP"一行的验证路径在 tests/e2e/live-acp/README.md 中有更具体的说明:该 e2e 证明容器化 ACP 凭据路径通过 Canvas 自身代码走通——每个凭据先像 onboarding 一样存进 agent-server 的 secret store,buildStartConversationRequest 再以 LookupSecret 的形式引用它,由服务端在 spawn 时解析。这是对 tests/api/agent-server-adapter.test.ts 单元测试的"真实跑通"配套:单测断言请求形状,live-acp 断言真实 agent 回复。
Automations × Install × Agent:需要完整栈的验收
第二张矩阵要求 automation backend 完整运行(requires full stack),并直接标注了优先级:
| npm | Docker | |
|---|---|---|
| OpenHands | ✅ P0 | ✅ P0 |
| Claude Code | ✅ P1 | ✅ P1 |
| Codex | ✅ P1 | ✅ P1 |
| Gemini CLI | ✅ P2 | ✅ P2 |
每个格子的验收动作是:创建 automation → 派发一次 run → run 到达 COMPLETED 状态 → 对话链接可用(create automation → dispatch run → run reaches COMPLETED → conversation link works)。
从源码结构看,这条链路的后端组件是 automation backend:端口 18001(见 config/defaults.json 的 ports.automation),SQLite 数据库位于 automation/automations.db(paths.automationDb)。mock-LLM E2E 配置 playwright.mock-llm.config.ts 对状态目录的清理逻辑也印证了这一点:自动化 DB 存放在 $parent_of_STATE_DIR/automation/automations.db,镜像了 docker/entrypoint.sh 使用 $HOME/.openhands/automation/automations.db 的布局,STATE_DIR 与 AUTOMATION_DB_DIR 在每次运行之间都必须清理以避免脏数据。
Auth Modes:本地自动密钥与公共模式
第三张矩阵覆盖两种会话鉴权模式:
| npm | Docker | |
|---|---|---|
| Local(auto-generated key) | ✅ P0 | ✅ P0 |
Public(--public + user key) |
✅ P1 | ✅ P1 |
- Local 模式:安装时自动生成会话 API key。mock-LLM 的 Playwright 配置正是这样工作的——playwright.mock-llm.config.ts 在未设置
MOCK_LLM_SESSION_API_KEY时用randomBytes(32).toString("hex")生成随机密钥,并通过环境变量LOCAL_BACKEND_API_KEY注入给bin/agent-canvas.mjs启动的完整栈。 - Public 模式:以
--public(对应 static-server 的--auth-required,不烘焙 session key)启动第二个静态服务器实例,复用同一份build/产物与同一后端。在 playwright.mock-llm.config.ts 中这是 webServer 数组的第 3 个进程:node scripts/static-server.mjs --dir build --port 18301 --auth-required,并把/api/automation、/api、/server_info、/sockets分别路由到 18001/18000 端口。Docker 路径则由容器内 entrypoint 在设置了PUBLIC_MODE_PORT时启动同样的第二实例(见 playwright.mock-llm-docker.config.ts 中传入的-e PUBLIC_MODE_PORT=…)。 - 这两类格子在 mock-LLM 测试集中的具体验证位于 tests/e2e/mock-llm/backends/mock-llm-auth-modes.spec.ts。
Feature Checklist:npm 与 Docker 的 12 项功能清单
矩阵文档为 npm 与 Docker 各列出一张功能清单(两表行项一致),这是手工验收时逐格勾选的检查单。以下完整保留原文档的 12 个功能项("LLM profiles"一行在 Claude Code / Codex / Gemini CLI 列中标注为 "—",表示该功能不适用于这些 ACP 接入路径,因为它们的模型与鉴权由各自的 CLI 管理):
npm
| Feature | OpenHands | Claude Code | Codex | Gemini CLI |
|---|---|---|---|---|
| Onboarding | ☐ | ☐ | ☐ | ☐ |
| Conversation — start, resume, history | ☐ | ☐ | ☐ | ☐ |
| Terminal tool | ☐ | ☐ | ☐ | ☐ |
| File editor tool | ☐ | ☐ | ☐ | ☐ |
| Browser tool | ☐ | ☐ | ☐ | ☐ |
| LLM profiles — create / switch | ☐ | — | — | — |
| Secrets — add / delete / forwarded | ☐ | ☐ | ☐ | ☐ |
| Automations — create, dispatch, COMPLETED | ☐ | ☐ | ☐ | ☐ |
| Files tab + Changes/diff tab | ☐ | ☐ | ☐ | ☐ |
| MCP server install | ☐ | ☐ | ☐ | ☐ |
| Image upload in chat | ☐ | ☐ | ☐ | ☐ |
| Key rotation | ☐ | ☐ | ☐ | ☐ |
Docker
| Feature | OpenHands | Claude Code | Codex | Gemini CLI |
|---|---|---|---|---|
| Onboarding | ☐ | ☐ | ☐ | ☐ |
| Conversation — start, resume, history | ☐ | ☐ | ☐ | ☐ |
| Terminal tool | ☐ | ☐ | ☐ | ☐ |
| File editor tool | ☐ | ☐ | ☐ | ☐ |
| Browser tool | ☐ | ☐ | ☐ | ☐ |
| LLM profiles — create / switch | ☐ | — | — | — |
| Secrets — add / delete / forwarded | ☐ | ☐ | ☐ | ☐ |
| Automations — create, dispatch, COMPLETED | ☐ | ☐ | ☐ | ☐ |
| Files tab + Changes/diff tab | ☐ | ☐ | ☐ | ☐ |
| MCP server install | ☐ | ☐ | ☐ | ☐ |
| Image upload in chat | ☐ | ☐ | ☐ | ☐ |
| Key rotation | ☐ | ☐ | ☐ | ☐ |
Automated Coverage:四套自动化如何支撑矩阵
矩阵文档的最后一节把自动化测试与矩阵的四个维度(Install / OS / Agents / Automations)对齐:
| Suite | Install | OS | Agents | Automations |
|---|---|---|---|---|
vitest(unit) |
— | Linux | — | partial |
test:e2e:mock-llm |
npm | Linux | OpenHands, ACP(mock) | ✅ full |
test:e2e:mock-llm:docker |
Docker | Linux | OpenHands, ACP(mock) | ✅ full |
test:e2e:live |
npm | Linux | OpenHands | ❌ |
CI 尚未覆盖的部分:真实 ACP 凭据(Claude Code / Codex / Gemini)、macOS、public 鉴权模式、订阅登录路径、Windows。
说明:矩阵文档中
test:e2e:mock-llm的 "Automations ✅ full" 与手工矩阵中 Automations 的优先级标注互为印证——自动化已经把 npm 与 Docker 两条路径上的 OpenHands 全栈(含 automation)跑满,剩下的手工格子集中在真实凭据的 ACP agent 与多 OS 组合上。
下面结合仓库源码逐个说明四套测试的实际形态。
1. vitest 单元测试:Linux 上的单元层
package.json 中的脚本为 npm test → npm run make-i18n && vitest run。Vitest 的配置内嵌在 vite.config.ts 的 test 段:jsdom 环境、setup 文件 vitest.setup.ts、并显式 exclude: [...configDefaults.exclude, "tests"]——这解释了为什么 live E2E(位于 tests/ 下)不属于单测套件。几个对测试行为有实际影响的配置细节:
testTimeout/hookTimeout提到 30s:注释说明大量 DOM 密集型测试并行时,userEvent驱动的测试在繁忙机器上可能超过 Vitest 默认 5s 超时,提高全局超时让测试保持确定性而不改变任何生产行为;coverage.include限定为src/**/*.{ts,tsx},报告输出到coverage/,由npm run test:coverage触发;- vitest.setup.ts 统一桩掉了
VITE_SESSION_API_KEY("test-session-key")、canvas 的getContext、HTMLElement.scrollTo,并为 Node.js 25+ 提供了内存版localStorage桩(否则 zustand 的 persist 中间件无法工作)。
此外仓库还配置了变异测试:stryker.config.mjs 以 vitest 为 testRunner,对 src/**/*.{ts,tsx}(排除测试、类型声明、fixtures/mocks)执行变异,related: true 意味着 Stryker 用 vitest related 只运行与被变异文件相关的测试;入口命令是 npm run test:mutation(另有 test:mutation:diff / test:mutation:incremental 走 scripts/stryker-diff.mjs)。
2. test:e2e:mock-llm:npm 全栈 + 模拟 LLM
对应脚本 playwright test --config=playwright.mock-llm.config.ts。playwright.mock-llm.config.ts 的文件头注释把三进程架构写得非常清楚:
- Mock LLM 服务器(Python):tests/e2e/mock-llm/scripts/mock-llm-server.py 基于 openhands-sdk 的
TestLLM提供 OpenAI 兼容的/v1/chat/completions接口,agent-server 的 litellm 层与之对话而无需真实 LLM 凭据。脚本内置一条单轨迹:一次 terminal 工具调用(标记MOCK_LLM_E2E_BASH_OK)加一段文本回复(MOCK_LLM_E2E_REPLY_OK),并识别 agent-server 的 LLM profile 预检 ping(PREFLIGHT_PING_TEXT = "ping")以免污染脚本化轨迹; - 完整 agent-canvas 栈:通过
bin/agent-canvas.mjs启动(agent-server + automation backend + 静态前端 + ingress 代理),注释明确说明这镜像的是生产npx @openhands/agent-canvas路径;配置在启动前会清理.tmp/mock-llm-state与 automation DB 目录,并在build/index.html不存在时先npm run build:app; - Public 模式静态服务器:见上文 Auth Modes 一节。
关键端口分配(均可用环境变量覆盖):mock LLM 在 9999,ingress 在 18300,public 模式在 18301——刻意与 dev / live E2E 错开避免端口冲突。值得注意的是 webServer 的就绪探测 URL 是 http://localhost:18300/api/automation/v1 而不是 ingress 根路径:注释解释了 automation backend 经 uvx 启动最后才完成、可能耗时 30–60 秒,只探测 ingress 根或 /server_info 会让测试在栈未完全就绪时开始,因此要探测"最后启动"的服务。另外 gracefulShutdown: { signal: "SIGTERM", timeout: 15_000 } 也是刻意为之:Playwright 默认的 SIGKILL 组杀无法被 scripts/dev-process-utils.mjs 派生的 detached 服务捕获,会造成孤儿进程占着端口,改用 SIGTERM 才能触发 scripts/dev-with-automation.mjs 中的关停处理器。
运行约束:workers: 1、fullyParallel: false、单用例超时 60s、CI 下 10 分钟全局硬上限;仅 chromium。测试规格按功能域组织在 tests/e2e/mock-llm/:onboarding/、conversations/(含 image upload)、automations/、backends/(auth modes、cross-connect、partial stack)、files/(files & git)、mcp/、settings/(ACP agent、模型切换、profile 管理)、skills/、home/(folder workspace)、canvas-extensions/、regressions/。
3. test:e2e:mock-llm:docker:同一套规格打 Docker 镜像
playwright test --config=playwright.mock-llm-docker.config.ts 与上一节复用同一批 test specs(testDir: "./tests/e2e/mock-llm"),只是把宿主上的 bin/agent-canvas.mjs + uvx 换成 all-in-one Docker 镜像(默认 ghcr.io/openhands/agent-canvas:latest,可用 MOCK_LLM_DOCKER_IMAGE 覆盖,与 config/defaults.json 的 images.agentCanvas 一致)。playwright.mock-llm-docker.config.ts 的几个工程细节值得注意:
- 网络:Linux 上用
--network host使容器共享宿主网络栈,容器内 agent-server 可以像 npm 路径一样直接访问127.0.0.1:9999的 mock LLM;macOS/Windows 的 Docker Desktop(bridge 网络)则需设置MOCK_LLM_AGENT_URL=http://host.docker.internal:<port>; - 卷挂载:把宿主上的 mock ACP 脚本(
tests/e2e/mock-llm/scripts/mock-acp-server.py→ 容器内/opt/mock-acp-server.py)、技能仓库、用户技能目录(/home/openhands/.openhands/skills)和 folder-workspace 测试目录挂进容器,保证与 npm 路径可测同一批场景; - 环境注入:容器以
-e PORT=18300 -e SESSION_API_KEY=… -e OH_SESSION_API_KEYS_0=… -e PUBLIC_MODE_PORT=18301启动,容器内还会额外起一个--auth-required的 public 模式 static-server(见 docker/entrypoint.sh 对PUBLIC_MODE_PORT的处理); - 清理:容器使用唯一随机名 +
docker rm -f前置清理 +--rm,Playwright 退出时 webServer 命令收到 SIGTERM,docker run --rm自动收尾。
运行前提:已构建的 Docker 镜像和正在运行的 Docker daemon。CI 下全局超时默认 20 分钟(MOCK_LLM_DOCKER_GLOBAL_TIMEOUT_MS 可覆盖)。
4. test:e2e:live:真实 LLM 的 npm 路径
脚本为 node --env-file-if-exists=.env tests/e2e/live/scripts/run-live-e2e.mjs,配合 playwright.live.config.ts。与 mock-LLM 路径不同,live 路径需要真实 LLM 凭据:tests/e2e/live/scripts/run-live-e2e.mjs 从 LIVE_E2E_LLM_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY / LLM_API_KEY 中取第一个可用者,未配置时回落到内置的 LLM proxy 默认值(模型默认 openhands/claude-haiku-4-5-20251001)。webServer 命令用 npm run dev:minimal 起最小编译的前端(端口 3101)加本地 agent-server(默认 http://127.0.0.1:18100,由 OH_CANVAS_SAFE_BACKEND_PORT 控制),并在启动前清理 .tmp/live-e2e-state 与 node_modules/.vite。矩阵表中该套件对 Automations 一栏标 ❌——live 路径不覆盖 automation 全栈,这与 mock-LLM 路径"✅ full"形成互补。
受影响测试选择:变更驱动的 E2E 收窄
在 CI 中跑 E2E 成本高,仓库用一个映射文件实现"改了哪些源码就只跑哪一组测试"。tests/e2e/mock-llm/test-mapping.json 把源码 glob 映射到 mock-LLM 测试子目录,例如:
src/components/features/settings/**、src/routes/llm-settings*.tsx等 →settings测试组;src/components/features/chat/**、src/routes/conversation.tsx等 →conversations;src/components/features/automations/**、src/manifests/**等 →automations;alwaysRun固定包含regressions组;runAllSources列出"触碰即跑全量"的高风险文件,如src/api/agent-server-adapter.ts、src/root.tsx、playwright.mock-llm.config.ts、package.json以及 CI workflow 文件本身。
tests/e2e/mock-llm/scripts/resolve-affected-tests.mjs 读取该映射,根据变更文件集合决定本次运行哪几个测试目录;其自身行为由 tests/e2e/resolve-affected-tests.test.ts 覆盖。这对维护矩阵很有意义:手工矩阵关注"发布前各格子绿",而这份映射保证日常 PR 阶段的自动化成本与风险成正比。
覆盖缺口与矩阵维护建议
矩阵文档明确列出当前 CI 未覆盖的区域,这也是一份现成的手工验收 TODO:
- 真实 ACP 凭据(Claude Code / Codex / Gemini):mock 路径只覆盖 OpenHands 与 mock ACP;真实凭据路径可参考 tests/e2e/live-acp/README.md 的手工 e2e——它要求
agent-server:1.25.0-python或更新镜像(旧镜像会在首轮 ACP 冷启动死锁),凭据取自宿主(Codex 的~/.codex/auth.json、Claude Code 的 macOS keychain OAuth token、Gemini 的 gcloud ADC),凭据缺失的 provider 自动跳过,且凭据绝不打印; - macOS / Windows:所有自动化均跑在 Linux,跨 OS 只能靠 Install × OS × Agent 手工矩阵;
- public 鉴权模式在 CI 中的覆盖依赖 mock-LLM 的 18301 public 实例,矩阵中该模式整体仍是 P1;
- 订阅登录路径(subscription login paths)尚无自动化。
维护该矩阵时的实践要点(均可从源码直接验证):
- 新增功能后同步更新 tests/e2e/mock-llm/test-mapping.json,否则变更驱动的测试选择可能漏跑相关 E2E;
- 修改
config/defaults.json的端口/版本钉时,注意它同时被 npm 启动器、Docker entrypoint 与 CI workflow 消费,属于矩阵表中 npm 与 Docker 两行共同的"地基"; - 任何触碰
runAllSources中文件的改动,CI 会退化为跑全量 mock-LLM 测试组,这是设计好的保守策略而非故障。
常用命令速查
| 目的 | 命令 |
|---|---|
| 单元测试 | npm test(vitest run,Linux,jsdom) |
| 单测 + 覆盖率 | npm run test:coverage |
| 变异测试 | npm run test:mutation / npm run test:mutation:diff |
| mock-LLM E2E(npm 全栈) | npm run test:e2e:mock-llm |
| mock-LLM E2E(Docker 镜像) | npm run test:e2e:mock-llm:docker(需先构建/拉取镜像,Linux 建议 host 网络) |
| live E2E(真实 LLM) | npm run test:e2e:live(配置 LIVE_E2E_LLM_API_KEY 等凭据) |
| MSW 驱动的开发服务器(基础 e2e 默认 webServer) | npm run dev:mock(playwright.config.ts 的 webServer 即调用 npm run dev:mock -- --port 3001) |
以上命令均可在 package.json 的 scripts 段与四个 Playwright 配置文件(playwright.config.ts、playwright.mock-llm.config.ts、playwright.mock-llm-docker.config.ts、playwright.live.config.ts)中逐条对照。
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 StartedRust0622
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