Skip to content

测试替身比真实实现宽松:四个缺陷因此带着绿灯发布——需要一条把替身钉在真实契约上的闸门 #4550

Closed

Description

@os-zhuang

v17 验收批次(#4482)修完后回看,四个缺陷共享同一个根因,而且这个根因本身没有被修:测试替身(假引擎、夹具、桩件)比它替代的真实实现更宽松,于是缺陷在既有测试全绿的情况下发布。

这不是四次巧合。它是一类可复现的失效模式,且当前没有任何闸门看得见它。

四个实例

缺陷 替身 它比真实实现宽松在哪
#4434 plugin-sharing 测试里的假引擎 deleteRule 用谓词删 sys_record_share,既无标量 id 也无 multi:true——正是真实 engine dispatch 唯一拒绝的形状。既有测试 deleteRule drops rule + all its grants 一直在对着一个真服务器必然 500 的调用断言成功。
#4441 dogfood field-zoo 夹具 注释直言 // FK enforcement is off in this harness, so this asserts value fidelity,并往 lookup 写 acc_synthetic_0001 等不存在的 id——夹具依赖着平台正要禁止的那个缺陷。
objectui#3134 — 设计器标签解析无对照
objectui#3129 ObjectTimeline.test.tsx 把 ./renderer 换成只渲染 item.title 的桩件 分桶结果只存在于真实渲染器输出的表头里,于是「uses spec-compliant startDateField」这条用例无论日期绑定解析成功与否都通过。同文件 ListView.test.tsx 只断言切换器里出现 Timeline 选项,从不检查转发下去的绑定。

另有一条同形态的问题在 #4433 修复中暴露:rule-rebind.test.ts 的 skips system-context writes 用例把缺陷行为本身钉成了断言(用的是真实路径根本不会发送的 mock session)。

为什么这类问题格外贵

三重代价:

  1. 它让绿灯失去意义。 缺陷发布时门禁是绿的,而且绿得理直气壮——测试确实跑了、确实通过了。
  2. 它自我复制。 写新测试的人(尤其是 agent)会模仿既有测试的 mock 方式。一个宽松的假引擎会长出第二个、第三个。
  3. 它专挑最需要覆盖的路径。 替身之所以被引入,正是因为真实实现在那里「不好测」——而不好测的地方往往就是契约最密的地方。

已有的两处正确做法,可作为方向

本批次的修复里已经出现了对的形状,可以推广而非从零设计:

建议方向(未实现,待讨论)

不是要禁用测试替身——那不现实。目标是让「替身与真实实现的契约差异」可见且会报警:

  1. 给关键替身加契约钉:假引擎/假 driver 的拒绝面必须与真实实现的守卫对齐,并有测试证明「放松替身 → 某条既有断言变红」。sharing: DELETE /sharing/rules/:idOrName answers 500 for both address forms — rules cannot be deleted over REST #4434 的修复已经演示了这个形状。
  2. 禁止在断言目标本身上使用桩件:objectui#3129 的教训——断言分桶结果就不能把渲染器换掉。可考虑一条 lint 规则或评审清单项。
  3. 夹具中「关闭平台约束」的注释视为欠债标记:像 FK enforcement is off in this harness 这种,应当进入台账并被要求给出理由与到期条件,而不是长期沉默存在。

第 3 条与仓库已有的 UNWIRED_RULE_LEDGER(#4449)、NO_DATA_REACH(objectui#3144)是同一个模式:已知的不完整必须被声明,而不是被默认。

范围

本 issue 只记录模式与方向,未认领。真正的闸门形态需要设计讨论——尤其第 1 条如何在不同包的替身之间通用,可能需要一个共享的「契约一致性」测试工具而非逐包手写。

发现自 #4482 v17 验收批次。

Activity

  1. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 2, 2026
  2. os-zhuang commented on Aug 3, 2026

    @os-zhuang
    ContributorAuthor

    裁决(PM 代决,维护者可否决):方向采纳,排低优先 ——

    1. 关键替身加契约钉:假引擎的拒绝面对齐真实守卫,并证明「放松替身 → 既有断言变红」(sharing: DELETE /sharing/rules/:idOrName answers 500 for both address forms — rules cannot be deleted over REST #4434 已演示此形状);
    2. 禁在断言目标本身上用桩(objectui#3129 教训),先做评审清单项,lint 可行性由执行 agent 评估;
    3. 夹具里「关闭平台约束」的注释一律进欠债台账(同 UNWIRED_RULE_LEDGER / NO_DATA_REACH 模式:已知不完整必须声明)。

    纪律直接复用 #4118 已落地的「守卫必须证明会红」方法,不另起炉灶。两轴:测试替身宽于真实契约 = AI 拿绿灯当"done"的最大盲区,四个缺陷带绿灯发布即证据;但相比契约收紧批次不具窗口压力,排后。

    摘除过期的 pm:dispatched(无 agent 报告),重新入队,优先级低于 v17 契约收紧批次。


    Generated by Claude Code

  3. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Aug 3, 2026
  4. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    domain:devxDomain lane — seat card indexed by label:pm:seat
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Aug 3, 2026
  5. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

    认领(PM 分派)

    • session:session_018iARDqtrhQgz6fVHDeDkbQ
    • 分支:claude/issue-4550-test-double-contract-gate
    • 车道:domain:devx

    本单是「测试替身比真实实现宽松,四个缺陷因此带着绿灯发布」。派发时要求先分片:四个已知实例里挑出判据最清楚的一类先立闸门,不要一次覆盖所有替身 —— 一条覆盖面模糊的闸门,和没有闸门在外部看起来一样。范围收窄的理由要写进 PR。


    Generated by Claude Code

  6. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

    审核通过 — PR #4948 已取消草稿并挂上 auto-merge(SQUASH)

    分片判断正确,而且议题正文(再次)与实情不符:四个实例里两个在 objectui 仓(本仓闸门够不到),一个(objectui#3134)根本不是"替身宽松"而是"没有对照测试" —— 把它算进覆盖面会让覆盖面变虚。本仓内判据可机械判定的只有 #4434 一个,切这一刀是对的:ObjectQL.delete 的 dispatch 是 options → 三个判决(by-id / multi / throw)的全函数,"替身是否更宽松"有 yes/no 答案。

    历史坏状态的双重判红是本 PR 最硬的证据:恢复 #4434 修复前的假引擎,闸门当场点名判红;反过来保持替身钉住、恢复当年的服务代码,8 条测试判红,抛的正是当年线上 500 的那条错误 —— 当年有这条闸门,#4434 进不来。

    规模比议题大得多,处理方式对:全仓 39 个假 ObjectQL 引擎,本 PR 钉住 9 个,其余 30 条进只减不增的基线($comment 明确写"只声称结构上更宽松,不声称当下无害" —— 没有伪装成全量实测)。3 个手抄守卫的替身是最有价值的发现:if (!opts?.where?.id && !opts?.multi) 会放行 where:{id:{$in:[…]}},而真实引擎按多行谓词拒绝 —— 手抄对齐不是对齐,这正是共享谓词而非"要求体内含 multi 与 throw"的理由。

    共享谓词放 objectql 而非 spec 的论证成立:spec 只出纯数据,可执行谓词进去踩 PD#2;"假谁就 devDepend 谁的契约"是正解。engine.ts 的改动我核过 diff:行为保持的抽取(内联判定 → engine-delete-dispatch.ts,reject 文案改共享常量),objectql 1757 测试全绿;此属 devx 闸门牵动的最小生产侧抽取,已在 #4604 向 engine 车道 PM 通报。

    CI 唯一红是 Validate Package Dependencies(main 上先存在的 OSV 公告,#4945 修复在飞),不阻断审核;#4945 落地后本 PR 随队列自动转绿。

    遗留(dev 已列,按包另立):plugin-approvals(6)+runtime(1)已依赖 objectql,一行可清;23 个需加 devDependency;update/find 的孪生谓词各需先在生产侧抽取;#4441 那类"夹具关约束"需要声明式台账而非扫描器。这些进 backlog,不扩本单。


    Generated by Claude Code

  7. 2 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

domain:devxDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch looptoolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions