diff --git a/.claude/agents/os-dev.md b/.claude/agents/os-dev.md index d921140b82..3f522ef27d 100644 --- a/.claude/agents/os-dev.md +++ b/.claude/agents/os-dev.md @@ -206,6 +206,32 @@ all real: forcing the template; #4984 is the family origin — fixtures spelling rejected aliases kept the tests green while the rule was dead). +**Rejection-class cases assert the envelope, not the throw.** For any case whose +point is that bad input is *refused*, the minimum assertion set is the error's +**`code` AND `status`** (the ADR-0112 envelope). `expect(...).toThrow()` / +`rejects.toThrow()` on its own is not a rejection test: it carries one bit where +the defect has two, and PR #6142 (#6050) measured both ways it goes blind — +opposite directions, same hole: + +- **A bare `Error` ⇒ permanently green.** Deleting the new refusal gate turned + 22 of `driver-sql`'s 28 cases red, and *most* of those reds were the driver + throwing knex's bare `Undefined binding(s)` — an `Error` whose `code` and + `status` are both `undefined`. The unfixed driver already throws; only the + envelope is missing. A throw-only assertion therefore stays **green on the + very driver the issue targets**. +- **A transport that never throws ⇒ red, but pointing away from the defect.** + The same deletion turned 20 of `driver-turso`'s 29 remote cases red, and all + 20 failed by *answering* — that transport never throws. A throw-only + assertion reports "the promise resolved", which names the absence of a throw + and never the absence of an envelope, so it cannot separate "refused with the + wrong envelope" from "did not refuse at all" — and those are exactly the two + defects. + +Where the wording is itself contract (#5240, one condition ⇒ one wording), +assert the message's first sentence **on top of** `code`+`status`, never instead +of them. A rejection test that cannot go red on a missing envelope reads as +coverage and is not. + **Key-vs-value reachability criterion.** Match a fixture guard's assertion to what the rule guards. Guarding that a **key** is a real authoring surface → assert the schema reports no `unrecognized_keys` on the fixture. Guarding a diff --git a/.claude/skills/pm-dispatch/SKILL.md b/.claude/skills/pm-dispatch/SKILL.md index a68f272173..0528ba3425 100644 --- a/.claude/skills/pm-dispatch/SKILL.md +++ b/.claude/skills/pm-dispatch/SKILL.md @@ -1195,6 +1195,28 @@ prompt: 实现面逐层收口」这类工作上,没有第二个实现面的活(纯 UI、文档、单面脚本)这条无处可 绑,该省掉而不是改写它。 +**拒收类用例的最低断言集是 `code` + `status`,不是「它抛了」—— 派发令的标准条款。** +适用判据:本单会**新增或改写拒收 / 错误类用例**(验收点里出现「应当被拒收」的活)。 +满足时派发令带这一句(原话): + +> 拒收类用例最低断言**错误的 `code` 与 `status`**(ADR-0112 信封)。 +> `expect(...).toThrow()` / `rejects.toThrow()` 单独使用**不构成**拒收测试;措辞本身 +> 是契约时(#5240「一个条件一种措辞」),首句断言**加在** `code`+`status` **之上**, +> 而不是代替它。本条约束你**新写或改写**的用例 —— 顺手回填存量套件不在本单范围内。 + +出处是 #6142(#6050)的反向验证实测。两种失明机制方向相反,同一个洞: + +- **裸 `Error` ⇒ 恒绿。** 删掉拒收闸后 `driver-sql` 28 例红 22,**多数红在抛出 knex 的 + 裸 `Undefined binding(s)`** —— 一个 `code` / `status` 均为 `undefined` 的 Error。未修的 + 驱动本来就抛,缺的只是信封:只断言「它抛了」的用例,**在本单所针对的那个驱动上保持 + 绿色**。 +- **从不抛的 transport ⇒ 红,但红得不指向缺陷。** 同一次删闸,`driver-turso` remote + 29 例红 20,**20 个全部**红在「本该拒收却编译出了 SQL」—— 该 transport 从不抛。只断言 + 抛出的用例在这里报的是「promise 没有 reject」,说的是**没抛**而不是**没信封**,分不开 + 「拒收了但信封错」与「根本没拒收」—— 而这正是这一族的两个缺陷。 + +一句话:**一个在缺信封的实现上无法转红的拒收用例,读起来是覆盖,实际不是。** + **Issue 正文是线索,不是规格 —— and the dispatch wording is what makes an honest "the premise is dead" cheap to return.** Step 1's stale-premise check is the PM's sample; the dev's verification is the real thing, so the prompt @@ -1478,6 +1500,12 @@ against the report's own claims: genuinely invalid shapes are still there (step 5's two lines). #5365 slipped through exactly this review layer and was caught by CI instead: CI does catch it, at the price of one extra lap. +- **拒收类用例的绿,是不是「它抛了」的绿?** 判据:本单验收点含「应当被拒收」。抽查 + diff 里的拒收用例有没有断言 `code` 与 `status`(ADR-0112 信封)—— 只写 `toThrow()` / + `rejects.toThrow()` 的用例,在**未修实现本来就抛裸 Error** 的那一族上恒绿(#6142 + 实测:`driver-sql` 删闸后 22 红中多数是裸 knex Error,`code` / `status` 均 + `undefined`),于是「28 例全绿」这种报告读起来是覆盖、实际证不了拒收。缺断言判 + REWORK 补齐,而不是接受绿色输出。本条是 step 5 那条标准条款在复核侧的对账。 - **Did the dev verify the issue's premise?** The report's `premise_still_valid` field makes the answer explicit — a `false` there reopens triage rather than failing review. A report that falsifies the diff --git a/skills/objectstack-pm-dispatch/SKILL.md b/skills/objectstack-pm-dispatch/SKILL.md index 0cc704f1f6..6b05131100 100644 --- a/skills/objectstack-pm-dispatch/SKILL.md +++ b/skills/objectstack-pm-dispatch/SKILL.md @@ -390,6 +390,12 @@ against the report's own claims**: present. - Test evidence in the report shows the **actual commands and passing output**, not a bare "tests pass". +- Rejection-class tests in the diff — those whose point is that bad input is + **refused** — assert the error's identity (its `code` and `status`, or + whatever fields the project's error envelope declares), not merely that + something was thrown. A throw-only assertion is green on any producer that + already throws a bare error, which is what an unfixed producer usually does, + so it reads as coverage while being unable to fail on the defect it names. - The diff plausibly satisfies the issue's acceptance criteria. Verdict per issue: @@ -586,6 +592,20 @@ Definition of done, in order: written in the language the repository's PRs use. - Tear down anything you started (dev servers, temporary processes) by PID. +Rejection-class tests assert the envelope, not the throw. For any test whose +point is that bad input is REFUSED, the minimum assertion set is the error's +identity — its `code` and its `status`, or whatever fields your project's error +envelope declares. "It threw" alone (`expect(...).toThrow()`, +`rejects.toThrow()`) is not a rejection test, and it goes blind in two opposite +directions. An unfixed producer usually throws ALREADY — a bare error carrying +neither field — so the assertion stays GREEN on the very defect the test names. +And a producer that answers instead of throwing fails it with "nothing was +thrown", naming the absence of a throw rather than the absence of an envelope, +so it cannot separate "refused with the wrong envelope" from "did not refuse at +all". Assert the message's wording on top of the envelope fields only where the +wording is itself contract — never instead of them. A rejection test that +cannot go red on a missing envelope reads as coverage and is not. + When to STOP instead of coding. If the issue underspecifies a decision that shapes a public contract — a schema, API shape, naming, metadata semantics — or two readings of the issue lead to different architectures: make no guess,