8.1 KiB
Gitea Webhook Agent 测试设计
本文与 PRD.md 的 FR/AC 以及 ARCHITECTURE.md 的字段、状态和错误矩阵对齐。独立协议 回归入口为:
timeout 60s node scripts/gitea-webhook-regression.js脚本只使用 Node 内置模块和
fixtures/gitea-workflow/*.json,不启动server.js、不访问网络、不写业务 Store,适合实现落地前后的离线验收。
1. 测试金字塔
| 层级 | 目标 | 本轮入口 |
|---|---|---|
| 契约/纯函数 | HMAC、事件规范化、mention/Bot 过滤、幂等键 | gitea-webhook-regression.js |
| 状态模型 | queued/running/retry_wait/终态恢复和有限重试 | 同上 + 实现方状态机单测 |
| 协议集成 | HTTP header、Gitea REST、Codex JSON-RPC、MCP stdio | mock server/adapter 集成测试 |
| 端到端 | 真实 Gitea 测试仓库到最终评论 | staging 手工/CI smoke,不纳入离线脚本 |
| 运维验收 | 重启、备份恢复、pause/disable/abort、告警脱敏 | staging runbook 演练 |
离线脚本是最小门禁;当前仓库已提供以下专项门禁:
node scripts/gitea-webhook-regression.js
node scripts/gitea-webhook-workspace-unit.js
node scripts/gitea-workflow-codex-unit.js
node scripts/gitea-workflow-core-unit.js
node scripts/gitea-workflow-management-unit.js
仍需在 staging 用真实 Gitea 验证出站回帖确认;纯函数通过不能替代真实凭据和 网络链路验收。
2. Fixtures 约定
所有 fixture 均为 JSON,放在 fixtures/gitea-workflow/,不含真实 Secret、Token、
源码或生产 URL。字段约定:
{
"name": "valid-issue-comment",
"headers": {
"x-gitea-event": "issue_comment",
"x-gitea-delivery": "delivery-001"
},
"payload": {
"repository": {"owner": "acme", "name": "widget"},
"issue": {"number": 42},
"comment": {"id": 101, "body": "@ccweb-bot 请修复"},
"sender": {"login": "alice", "is_bot": false}
},
"expected": {"accepted": true, "resourceKind": "issue"}
}
脚本运行时用固定测试 Secret 计算签名;签名不落盘。需要拒绝签名时复制 payload 后篡改 raw body 或签名头。fixture 变更必须同步场景 ID 和预期结果。
3. 需求覆盖矩阵
| 场景 ID | 验证内容 | Fixture/断言 | 对应 AC |
|---|---|---|---|
| WH-01 | 合法 Issue 评论 mention 触发,规范化 owner/repo/number/comment | valid-issue-comment.json |
AC-01 |
| WH-02 | 合法 PR 评论进入 pull_request 资源,保留 base/head |
valid-pr-comment.json |
AC-01/03 |
| WH-03 | HMAC 缺失/错误不写业务任务,常量时间比较 | valid-issue-comment.json 篡改签名 |
AC-02 |
| WH-04 | 相同 instanceId:deliveryId 重放返回 duplicate,不增 task |
valid fixture 两次提交 | AC-02 |
| WH-05 | 无 mention、非评论事件、Bot 自评论、停用仓库均 ignored/rejected | ignored-events.json、bot-self-comment.json |
AC-01/10 |
| Q-01 | 重启后 queued 保留;running 变 retry_wait(interrupted_by_restart) |
queue-recovery.json |
AC-09 |
| Q-02 | 已有重试次数达到上限后进入 failed;waiting_user/终态不重跑 | 同上 | AC-09 |
| Q-03 | 同仓库运行锁互斥;跨仓库可以并行,不设置额外全局活动上限 | 脚本内最小状态快照 | AC-04 |
| R-00 | 合法触发仅产生一条 giteabot: 已收到 起始回执;成功路径不产生运行中/等待/已完成状态评论 |
server.js 回执策略断言 | AC-07 |
| R-01 | MCP 回帖成功但查询缺失只创建一个 reply_retry | reply-failure.json |
AC-08 |
| R-02 | 补发仍失败才 REST 兜底,结果为 succeeded_with_rest_fallback | 同上 | AC-08 |
| MCP-01 | thread/start.config.mcp_servers.gitea 为线程级 stdio 配置 |
mcp-thread-config.json |
AC-06 |
| MCP-02 | collaborationMode 字段放 settings,顶层无 model/effort 重复字段 | 同上 | AC-06 |
| MCP-03 | gitea-mcp 命令缺失、进程启动即退、initialize 返回 error、握手超时分别映射稳定错误码 | gitea-mcp-probe-unit.js + mock stdio fixture |
AC-06/07 |
| MCP-04 | 预检失败后任务只产生一条 giteabot: 失败终态回执;线程级注入仍保留且不自动安装二进制 |
gitea-mcp-probe-unit.js 源码守门 |
AC-06/07/11 |
| SEC-01 | Secret/token 不进入日志、评论、clone URL 或 fixture | 全部 fixture + 输出扫描 | AC-02/11 |
| SEC-02 | 路径穿越、任意命令、越权控制参数被拒绝 | 实现方安全单测 | AC-03/10/11 |
4. 状态、恢复与并发测试
实现方 Store 测试必须验证以下不变量:
deliveryKey、sessionKey、taskId+turnId、commentKey唯一;重复请求 返回原记录且不创建新 turn/comment。- 同一
repoKey同时最多一个preparing/running/aborting;不同repoKey可以并行;pause 只阻止领取,不强制终止运行中任务。 - 重启扫描用
stateVersion/leaseIdCAS,旧 worker 不能覆盖新租约;running最多自动恢复一次,超过上限进入failed。 waiting_user保留 thread,新评论创建新 task 但复用sessionKey/threadId; 所有终态不再自动执行。blocked_workspace不执行 reset/clean;管理员解除后才可重新入队。
故障注入至少覆盖进程在“intent 已落库、外部调用未完成”和“外部返回成功、 结果未确认”两个窗口崩溃,恢复后必须通过幂等键继续,而非盲目重复修改。
5. 回帖重试测试
按以下顺序模拟:
- MCP comment 返回 accepted,但 Gitea 查询无匹配标记:状态进入
verifying_reply,只允许一个reply_retryturn。 reply_retry仍无评论:调用一次 REST 兜底,body 复用taskId/turnId/resourceKey标记;查询成功为succeeded_with_rest_fallback。- REST 返回 2xx 但查询仍为空:状态必须是
failed_reply,不能虚报成功,也不得 无限重试。 - 原 MCP 评论已确认后重复 webhook/重启:不得再发送起始、状态、最终或兜底评论。
测试日志只输出 taskId/turnId/commentId/errorCode,不得输出正文中的 Secret 或
完整 Authorization。
6. MCP/JSON-RPC 协议测试
mock app-server 必须断言初始化顺序为:initialize → initialized → best-effort
experimentalFeature/enablement/set、collaborationMode/list → thread/start。
能力探测失败只记录降级,不阻断启动。
另需运行 node scripts/gitea-mcp-probe-unit.js,它会以独立 mock stdio 进程验证
gitea-mcp 的 initialize 预检,不需要真实 Gitea、Token 或 Docker。
thread/start 断言:
config.mcp_servers.gitea每个线程单独携带 host/token 引用和 stdio command;CC_WEB_SOURCE_SESSION_ID等来源上下文不能只存在进程环境;- 使用 collaboration mode 时,
model、reasoning_effort、developer_instructions只在collaborationMode.settings,顶层不存在重复model/effort;拒绝 collaborationMode 时可降级普通 turn。
7. 安全与运维验收
- 验签使用 raw body + HMAC-SHA256 + 常量时间比较;缺失/错误签名 401 且无业务写入。
- 事件正文、Issue 描述、代码和 PR 内容均视为不可信 prompt;不能改变系统约束、 读取 Secret、关闭审计或注入任意命令。
- Bot 自评论、停用仓库、非评论事件和无 mention 事件不启动 Agent;每个决定有 审计 action/errorCode。
- clone/fetch 使用不含 Token 的 remote URL;工作区路径必须在允许根目录内, 路径穿越和脏目录覆盖均拒绝。
- 控制 API 需要管理员身份、reason 和 idempotency key;取消/中止幂等。
- 日志和审计脱敏检查覆盖 Secret、Bot Token、Authorization、MCP args/env。
8. 执行清单
node --check scripts/gitea-webhook-regression.js
timeout 60s node scripts/gitea-webhook-regression.js
git diff --check
CI 失败时保留 stdout/stderr、fixture 名称和场景 ID;不得重试隐藏不稳定失败。 真实 Gitea staging 验收完成后,应将 delivery/task/thread/turn/comment 关联链和 最终状态写入审计报告,作为发布批准依据。