返回 AICAP-180
B6 · Day 59真 MCP server (2026-07-28 spec)

进程内 mock vs 真 server 等价回归

B6 一路把 AML 工具从「进程内函数」迁到「网络 server」(Day 56 上网、Day 57 客户端、Day 58 零信任校验)。迁移最隐蔽的风险不是功能缺失,而是行为漂移:同一批请求经两条路径(进程内 handle() vs HTTP server)输出悄悄不一致,上线即回归。今天用 differential testing(对拍) 把这个风险钉死。这是 B6 从「能跑」收口到「可上线

阶段: B6 · 真 MCP server (2026-07-28 spec)(Day 51-60) 标签: #differential-testing #regression #json-rpc #migration

今日导引(由浅入深)

B6 一路把 AML 工具从「进程内函数」迁到「网络 server」(Day 56 上网、Day 57 客户端、Day 58 零信任校验)。迁移最隐蔽的风险不是功能缺失,而是行为漂移:同一批请求经两条路径(进程内 handle() vs HTTP server)输出悄悄不一致,上线即回归。今天用 differential testing(对拍) 把这个风险钉死。这是 B6 从「能跑」收口到「可上线」的最后一道工程闸门,紧接 Day 58 的安全闸门。承接全 B6 的 server 实现;明天(Day 60)做 Inspector 端到端验收并归档里程碑。今日「最小可判定产出」:20 例对拍两侧输出一致(目标 20/20),且全量测试维持绿。

一句话定位:前面几天是「把能力搬上网」,今天是「证明搬上网没搬坏」——能力曲线上从「实现」到「可信迁移」的关键一步,也是把质量纪律从手工验证升级成可自动回归的起点(直通 B7 的 CI gate)。

1. 机理精读

迁移上网的最大风险是行为漂移。把一段逻辑从「进程内函数调用」搬到「HTTP server」,中间经过序列化、传输、反序列化、错误码映射等多个环节,任何一环的细微差异都可能让输出不一致——尤其是错误路径边界值,恰恰是 happy-path 测试覆盖不到的地方。

differential testing(对拍)是验证迁移一致性的最稳手段。它的逻辑朴素到无可辩驳:同一批输入,分别喂给「参照实现」(进程内 toolRegistry.tshandle(),B6 全程当对照基线)和「被测实现」(HTTP server),逐字节/逐结构 diff 两侧的 result / error。一致则迁移无漂移,不一致则定位到具体输入。对拍不需要预先写「期望输出」——参照实现自己就是 oracle(判定标准),这正是它比传统断言式测试更省力、覆盖更全的原因。

对拍的价值集中在错误码和边界,而非 happy-path。SOTA 检查的硬告警就是「只测 happy-path」的反模式:两条路径在「合法输入返回正确结果」上往往天然一致,真正容易漂移的是——非法入参时进程内抛 McpCallError(-32602)、HTTP 侧是否也精确返回 -32602?缺字段时两侧的 error.message/error.data 是否一致?边界值(恰好等于 minimum、空数组、超长字符串)两侧处理是否一致?所以 20 例必须覆盖合法 / 非法 / 边界三类,而非 20 个 happy-path。

为什么进程内 handle() 能当 oracle? 因为它是 B6 的「真相源」——toolRegistry.ts 头注(第 12-16 行)明确它实现的是 MCP 2026-07-28 stateless 语义的协议形状(JSON-RPC 信封 + tools/list + tools/call + schema 校验),且全确定性(第 22 行「无时钟/随机/网络」)。HTTP server 应当是它「加了 transport 层」的等价物——业务语义必须逐字一致,只多了网络外壳。对拍就是把这个「应当等价」变成可执行断言。

迁移漂移的常见来源,逐个对号入座(解释「为什么非要对拍」):

  • 序列化往返:进程内传 JS 对象,HTTP 传 JSON。undefined 字段在 JSON 里会消失、NaN/Infinity 不是合法 JSON、Map/Set 不能直接序列化——这些在进程内无碍,过 HTTP 就变形。
  • 错误码映射:进程内抛 McpCallError(code),HTTP 侧要手工映射到 JSON-RPC error.code;漏映射一个错误类型就漂移。
  • 字段顺序/缺省toEqual 深比较不在意 key 顺序,但若 HTTP 侧补了进程内没有的默认字段(如 message: ''),就会不一致。
  • 排序不稳定tools/list 若两侧排序逻辑不同(一侧 localeCompare、一侧默认),工具顺序漂移。

对拍的 20 例正是用来把这些隐性差异在上线前逼出来。

对拍 vs 传统断言测试的区别,值得讲清:

  • 传统断言测试:人工写「输入 X → 期望输出 Y」,Y 由人脑算出,写 20 例要算 20 个 Y,且人脑算错则测试本身就错。
  • 对拍:只需写「输入 X」,期望输出由参照实现(handle())自动生成。人不需要预知 Y,只需保证参照实现可信。
  • 代价:对拍依赖「参照实现是正确的」这一前提。若参照实现本身有 bug,对拍只能保证「被测实现和参照一样错」,逮不到共性 bug。所以对拍验证的是一致性(迁移无漂移),不是绝对正确性——后者仍需 Day 58 那种带 reference 的断言测试兜底。

与相邻概念的边界:今天测「两条路径输出一致」,不测模型能力(Day 57)、不测安全拦截率本身(Day 58,但对拍会顺带验证非法输入两侧都返回 -32602)、不做端到端 Inspector 验收(Day 60)。

2. 推导 / 手算 / 代码走读

Day 59 seed 要求「仿 src/agent/eval/tasks.ts 风格写对拍测试」。逐条走读(Read 验证):

  • src/agent/eval/tasks.ts(第 1-23 行头注)是 P1 的 29-task eval 套件范式:每个 CategorizedTaskid / category / prompt / reference / codeCheck。第 42 行 const has = (re) => (out) => ({ pass: re.test(out) }) 是轻量代码型检查工厂。对拍测试借用它「结构化用例 + 确定性判定」的写法,但判定标准换成「两侧 diff 一致」而非「正则匹配」。
  • 参照实现McpToolRegistry.handle(req: JsonRpcRequest): JsonRpcResponsetoolRegistry.ts 第 199 行)。它对 tools/list 返回 {tools: this.list()}(第 202 行),对 tools/call 成功返回 {content: result}(第 209 行),失败折成 error(第 211-214 行),永不抛错(第 197 行注释)。这是对拍的 oracle。
  • 错误一致性锚点:缺 params.name 时返回 -32602(第 206 行);McpCallErrorcode/message/data 原样进 error(第 212 行)。HTTP server 必须在这些点上逐字一致——这是 20 例里「非法」用例的重点。
  • 稳定排序锚点list()(第 169 行)按 name localeCompare 稳定排序(第 170 行)。对拍 tools/list 时,两侧工具顺序必须一致,否则 diff 误判(呼应 Day 54 的「排序稳定是契约」)。
  • 确定性保证validate(第 112 行)、call(第 183 行)、handle(第 199 行)全是纯逻辑,无随机/时钟/网络,所以同一输入两次运行结果恒等——对拍才有意义(不确定的实现无法对拍)。
  • 测试接入:仿 tasks.ts 把 20 例构造成数组,每例 { req, label },分别走 registry.handle(req) 与 HTTP server,expect(httpResult).toEqual(handleResult),接入 pnpm test

20 例的覆盖矩阵(合法 / 非法 / 边界三类,错误码是漂移高发区):

#类别输入要点两侧应一致的关键字段
1-3合法3 工具各一次正常 tools/callresult.content 结构
4合法tools/listresult.tools 顺序(稳定排序)
5-7非法·缺必填3 工具各删 required 字段error.code=-32602 + error.data
8-10非法·错误类型string 位填 number/arrayerror.code=-32602
11-13非法·越界值maximum / 不在 enumerror.code=-32602
14-15边界恰好 =minimum / =maximumresult(应通过,不报错)
16-17边界空数组 / 空对象入参resulterror 一致
18边界未知工具名error.code=-32001(TOOL_NOT_FOUND)
19边界params.nameerror.code=-32602(handle 第 206 行)
20边界未知 methoderror.code=-32601(METHOD_NOT_FOUND)

第 14-15 行(恰好等于边界值)最易漂移:进程内 validate 第 127-128 行用 </>(含等于边界为合法),HTTP 侧若误用 <=/>= 就会两侧不一致——对拍正是要逮这种细节。

3. 今日实战

  1. 仿 src/agent/eval/tasks.ts 写对拍测试文件:构造 20 例 JsonRpcRequest,覆盖三类——
    • 合法:3 工具各若干正常 tools/call + tools/list
    • 非法:缺必填、错误类型、越界值(复用 Day 58 的恶意输入,断言两侧都 -32602)。
    • 边界:恰好等于 minimum/maximum、空数组、未知工具名(断言两侧 TOOL_NOT_FOUND = -32001)。
  2. 每例分别走 toolRegistry.tshandle()(参照)与 HTTP server(被测)。
  3. diff 两侧 result / error,用 toEqual 深比较。
  4. 接入 pnpm test,确保新增对拍后全量测试维持绿。

对拍核心循环的代码形状(伪代码,落地到 Vitest):

for (const { req, label } of CASES /* 20 例 */) {
  const viaProc = registry.handle(req)          // 参照实现(oracle)
  const viaHttp = await httpServer.dispatch(req) // 被测实现
  expect(viaHttp).toEqual(viaProc)               // 逐结构深比较
}

落地前的自检清单(避免对拍本身失真):

  • 两侧都关闭了非确定性来源(无时间戳/随机 id 进 result)。
  • HTTP 侧的 JSON 反序列化没丢字段、没把 undefinednull
  • tools/list 两侧排序都走 localeCompare(稳定)。
  • 20 例里非法 + 边界 ≥ 合法(错误路径是漂移高发区,要重点压)。

4. 今日实测 / 产出

  • 状态:待建——目标:20/20 输出一致的对拍测试
  • 当前仓库全量 378 tests 绿tsc clean,本日新增对拍后须维持全绿。
  • 纯确定性逻辑,无需 key

(按诚信纪律:「待建」与「378 tests 绿 / tsc clean」逐字保留;「20/20」是目标,未臆造为已测得。)

对拍如何接入 CI(为 B7 的 CI gate 铺垫):对拍测试是确定性的(无 key、无网络、无随机),天生适合做 CI 必过项——它可以直接挂进 pnpm test,让每次 server 改动都自动验证「和进程内参照仍等价」。这正是 B7「CI gate」的雏形:把「20/20 对拍 + 9/9 拦截 + 378 全绿」固化为 merge 前的硬门槛,任何引入漂移的改动都会在 CI 红灯,进不了主干。本日只建对拍用例,把它升级成 gate 是 B7 的事。

5. 常见误区 / 陷阱

  • 只测 happy-path:SOTA 检查明确反模式。对拍价值正在错误码/边界的一致性上,合法输入两侧天然容易一致。
  • 不确定的实现拿来对拍:若 server 输出含时间戳/随机 id/非确定排序,对拍会误判。必须先确保两侧确定性(list() 的稳定排序就是为此)。
  • 只比 result 不比 error:迁移漂移最常发生在错误路径——缺字段、越界、未知工具时两侧的 code/message/data 必须逐字一致。
  • 对拍通过就放松全量回归:对拍只覆盖这 20 例,仍须维持 378 tests 全绿 + tsc clean,二者互补不互替。
  • 误把参照实现的 bug 当「正确」:对拍只保证两侧一致,不保证两侧都对;共性 bug 逮不到,仍需 Day 58 那类带 reference 的断言测试兜底。
  • 边界用 <=< 导致两侧不一致validate 第 127-128 行边界判定用严格 </>(等于边界为合法),HTTP 侧若手滑写成 <=/>=,恰好等于边界的输入就会两侧分叉——这正是第 14-15 例要逮的漂移。

6. 学习资源(每条带 YYYY-MM)

  • 仓库 src/agent/eval/tasks.ts(P1 产物,2026)—— 29-task eval 套件,对拍测试借鉴其「结构化用例 + 确定性判定」写法。
  • 仓库 src/agent/mcp/toolRegistry.ts(2026)—— handle()/list()/call()/validate() 进程内参照实现(对拍 oracle)。
  • differential testing 方法论(McKeeman, 1998;长期有效的软件工程技术)—— 用参照实现作 oracle 的对拍原理。
  • MCP 规范(2026-07-28,硬复查点 07-28)—— tools/list/tools/call 的 result/error 结构契约(对拍的比较基准)。
  • JSON-RPC 2.0 规范(2010-03)—— -32602/-32601/-32001 错误码语义(对拍非法/边界用例的断言依据)。
  • Vitest 文档(2026)—— expect().toEqual() 深比较语义(对拍断言落地工具)。

SOTA检查 (2026-06 更新)

  • 当前主流方案:differential testing 是迁移回归的标准做法,无时效问题(成熟工程技术,不会过时)。
  • 是否仍 SOTA:是。用进程内确定性实现作 oracle 对拍 HTTP server,是验证「上网无漂移」的最稳路径。
  • 过时黑名单 / AVOID:禁止只测 happy-path——对拍价值正在错误码/边界的一致性上。禁止对不确定实现做对拍(先消除随机/时钟/非稳定排序)。
  • 下次复查点:07-28 MCP 规范定稿后,重验 result/error 字段是否有新增(如 content 结构变化),同步更新对拍比较基准;server 上网部分实现后补跑 20/20。

衔接

  • 昨天:Day 58 — 工具输入零信任校验(9/9 恶意输入被 -32602 拦截,目标待建)。
  • 今天:对拍进程内 handle() vs HTTP server,验证迁移上网无行为漂移(目标 20/20,378 tests 须维持绿)。
  • 明天:Day 60 — 收尾 + Inspector 端到端验收,整块复盘 stateless 收益与剩余缺口。