# 验收 这份文档把"能不能用"拆成可以亲手做的检查。先说清楚哪些是已经做的、哪些还得在现场做。 ## 本次实际执行过的检查 - `node --check lib/index.js`、`node --check lib/client.js`:语法通过。 - `node --test tests/contract.test.mjs tests/integration.host.mjs`:15 项全部通过。其中 11 项是纯函数测试,4 项用假的 Cordis 上下文驱动**真实的** `lib/index.js`,覆盖注册面、工具往返、原生卡片 label→id 映射、HTTP 收件箱与鉴权、答案投递与状态落盘、明文提取、取消与禁用。 - 在本机 DSH Desktop `0.2.0-rc.2` 里真实安装(`0.1.0` 与 `0.1.1` 各一次):profile 的依赖与 `dsh.profile.bundles` 均写入,`node_modules/dsh-question-bridge` 落地;运行中的宿主返回 `/plugins/question-bridge/state` 401(已注册、但 curl 过不了浏览器鉴权),未注册的路径返回 404。 - **没有**跑真实模型判定(`classifier: "model"` 默认关闭),**没有**做真人点击的端到端联调。 想自己跑: ```bash cd dsh-question-bridge npm run check # 只做语法检查 npm test # 单元 + 集成,共 15 项 ``` `tests/contract.test.mjs` 覆盖纯函数:配置归一化、问题规范化(ID/label 唯一/上限)、答案校验(缺题、越界选项、单选互斥、跳过冲突)、文本识别(行内代码/围栏/引用跳过、取命中行作标题、列表项变选项)、指纹稳定性、原生答案 label→id 映射、答案消息结构、公开批次裁剪、会话文件名、客户端产物是不是合法工厂。 `tests/integration.host.mjs` 用假 ctx(Proxy 出服务属性、假 session/agents/userQuestions/webServer)把插件装配起来,断言:四条 `ctx.inject` 都注册了、工具参数是 JSON Schema 方言、工具立刻返回 pending、卡片拿到的是 label 形状、未鉴权请求 401、答案落盘先于投递、`agent.steer` 只发一次且信封正确、投递后批次离开收件箱、重复提交幂等、对已投递批次取消返回 `ALREADY_ANSWERED`、turn 边界能识别普通文本并开批次、`enabled: false` 时什么都不注册。 ## 八项验收 | # | 验收点 | 期望 | 结果 | | --- | --- | --- | --- | | 1 | 工具立即返回 | `ask_interactive_question` 返回 `{status:"pending",batchId}`,回合不被人类拖住 | 集成测试通过 | | 2 | 原生卡片可答 | 卡片出现;提交后批次进入 `answer-committed` | 集成测试通过(假 `userQuestions`);真机 UI 待现场 | | 3 | 答案真的进会话 | 日志里出现 `source.kind==="question-bridge-answer"` 的 user/message,Agent 被唤醒 | 集成测试通过 | | 4 | 重启不丢 | 宿主重启后未答批次仍在,`/plugins/question-bridge/state` 能列出来 | 待现场 | | 5 | 不伪造已读 | 关卡片、超时、切会话、点通知,都不会让批次变成 delivered | 集成测试部分覆盖;通知/切会话待现场 | | 6 | 不重复提问 | 同一 `toolCallId`、同一问题指纹重复提交,只产生一个批次 | 单元测试 + 幂等断言通过 | | 7 | 原生问答不受影响 | 宿主原生 `ask_user_question` 与 plan review 照常工作,插件不接管、不改 callId | 插件不写宿主事件、不传 `wait.callId`;待现场 | | 8 | 卸载干净 | 从 bundles 移除并重装后,工具、路由、面板全部消失,不残留定时器 | 待现场 | ## 手动走查脚本 1. 装好插件、重启、开一个新会话。 2. 输入:"用 ask_interactive_question 问我:先写测试还是先写文档?给两个选项。" 观察工具是否立刻返回 pending。 3. 卡片出现后**先不答**,直接在该会话继续发一条无关消息("顺便列一下仓库目录")。期望:Agent 继续干活,批次仍是 open,提示词里明确写着 pending 不是授权。 4. 打开右下角面板,在卡片里选一个选项提交。期望:会话里多一条 `answer_to_question_bridge` 用户消息,Agent 顺着答案继续。 5. 再起一个批次,答到一半关掉卡片/切到别的会话,然后**完全退出并重启 DSH**。期望:重启后面板里仍有该批次,可以继续答。 6. 两个窗口打开同一会话,同时提交不同答案。期望:一个成功,另一个收到"已被回答"的冲突提示,不会重复投递。 ## 六种故障切点与代码对应 | 切点 | 场景 | 代码行为 | | --- | --- | --- | | committed 后未 steer | 落盘成功、发消息前进程崩溃 | 重启后批次停在 `answer-committed`;`retryDelivery` 会检查日志幂等再 `steer`,不会双投 | | steer 后未 queued | `steer()` 成功、状态没来得及写 | 重新提交时先 `findAnswerMessage` 查日志;命中就直接标 `delivered` | | user-message 后未 delivered | 消息入日志、确认没写 | `session/event` 观察者收到该 source kind 就补写 `delivered` | | Stop 清 inbox | 用户中断,答案停在 inbox 被清 | 插件不私自重投,只保留可重试状态;要不要补投由人决定 | | 双窗口冲突 | 两个页面同时提交 | 串行队列 + `revision` 比对,后到者得到 `ALREADY_ANSWERED` | | 会话删除/换实例 | 会话已销毁或 agent 实例被换掉 | `agents.get(id) === agent` 不一致时抛 `SESSION_NOT_LIVE`,拒绝投递到错误会话 | 这几个分支都在 `lib/index.js` 的 `submit` / `retryDelivery` / `markDelivered` 里,没有隐藏补偿逻辑:拿不到 durable 证据就不声称成功。 ## 现场验证时要顺手记录 - 宿主的 `cordis.patch.yml` 是否被插件动过(不该动)。 - `~/.dsh/question-bridge/state/` 下文件大小是否随会话数线性增长(正常);`maxBatches` 会淘汰已结束批次。 - 面板轮询在页面隐藏时是否暂停(`document.hidden`),避免后台耗电。 - 系统通知权限状态;拒绝后是否仍能用面板。