# Gitea Webhook Agent 测试设计 > 本文与 [PRD.md](./PRD.md) 的 FR/AC 以及 > [ARCHITECTURE.md](./ARCHITECTURE.md) 的字段、状态和错误矩阵对齐。独立协议 > 回归入口为: > > ```bash > 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 演练 | 离线脚本是最小门禁;当前仓库已提供以下专项门禁: ```bash 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。字段约定: ```json { "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 测试必须验证以下不变量: 1. `deliveryKey`、`sessionKey`、`taskId+turnId`、`commentKey` 唯一;重复请求 返回原记录且不创建新 turn/comment。 2. 同一 `repoKey` 同时最多一个 `preparing/running/aborting`;不同 `repoKey` 可以并行;pause 只阻止领取,不强制终止运行中任务。 3. 重启扫描用 `stateVersion`/`leaseId` CAS,旧 worker 不能覆盖新租约; `running` 最多自动恢复一次,超过上限进入 `failed`。 4. `waiting_user` 保留 thread,新评论创建新 task 但复用 `sessionKey/threadId`; 所有终态不再自动执行。 5. `blocked_workspace` 不执行 reset/clean;管理员解除后才可重新入队。 故障注入至少覆盖进程在“intent 已落库、外部调用未完成”和“外部返回成功、 结果未确认”两个窗口崩溃,恢复后必须通过幂等键继续,而非盲目重复修改。 ## 5. 回帖重试测试 按以下顺序模拟: 1. MCP comment 返回 accepted,但 Gitea 查询无匹配标记:状态进入 `verifying_reply`,只允许一个 `reply_retry` turn。 2. `reply_retry` 仍无评论:调用一次 REST 兜底,body 复用 `taskId/turnId/resourceKey` 标记;查询成功为 `succeeded_with_rest_fallback`。 3. REST 返回 2xx 但查询仍为空:状态必须是 `failed_reply`,不能虚报成功,也不得 无限重试。 4. 原 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. 执行清单 ```bash 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 关联链和 最终状态写入审计报告,作为发布批准依据。