fix(ci): the RC smoke asserts the declared 401 refusal for an anonymous get-session - #18140
Conversation
…us get-session The packed-tarball release-candidate smoke asserted that an anonymous `GET /api/v1/auth/get-session` answers 200. Since #17881 the platform deliberately answers 401 with the ADR-0112 envelope (`code: UNAUTHENTICATED`), ruled by the director seat in decision batch #117 item 4 on card #17238: better-auth's `200` + literal JSON `null` is a value no `SessionResponse` can express, so `auth.me()` resolved outside its own declared type; the code moved and the published schema did not. The smoke was therefore asserting a contract the product deliberately stopped serving. This re-pins the probe to what the code declares — the same move #14000 made for `SELF_REGISTRATION_CLOSED` — and pairs the status with the envelope `code`, so a 401 from an origin check, a rate limiter or a later guard cannot keep the probe green while measuring nothing. The signed-in `get-session` probe is untouched and still asserts 200: the refusal seam converts only a 200 whose body is exactly `null`. Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU Co-authored-by: Claude <noreply@anthropic.com>
复核通过 —— 已 undraft 并武装(
|
| 探针 | 读数 |
|---|---|
grep -n "get-session|expected HTTP|Auth probes" .github/workflows/publish-smoke.yml |
1 行,且是注释(:10 描述 first-run flow)—— 该 workflow 不含任何断言 |
grep -n "get-session" scripts/publish-smoke.sh(origin/main) |
:804 匿名探针 200 · :935 登录态探针 200 ⇒ 断言在 shell 驱动里 |
⇒ 由此,简报里那条"本 PR 触及 .github/workflows/** ⇒ 本席武装不了(HTTP 422)⇒ 等人工合"的告诫对本 PR 不成立。本 PR 只改 scripts/publish-smoke.sh 一个文件,可以武装,已经武装。
⭐ 这正是本席对每个 dev 的那条要求("简报是主张,不是事实;发现假的就报回来")兑现的一次 —— 记录在此,⛔ 不改简报把痕迹抹掉。
假设判定:假设 2 成立,本席独立复验
⛔ 没有采信报告的结论,重读了树:
packages/plugins/plugin-auth/src/anonymous-session-refusal.ts在origin/main上存在,其模块头记着裁决:总监席决策批次 🔗 Broken links detected in documentation #117 项 4(2026-09-12),维护者原文 「17238 B」 —— 服务端以 ADR-0112 失败信封 + 401 作答,SessionResponseSchema不动,并引了宪章规则「spec 与代码不一致默认改代码,改协议单独立卡非选项」。packages/spec/src/api/errors.zod.ts:174→401: 'UNAUTHENTICATED'。
⇒ 401 是已声明的契约,冒烟断言的是平台已经故意停止提供的形状。⇒ 修探针,⛔ 不碰 auth 面 —— 这一条本 PR 做到了(diff 只有一个文件)。
⭐ 这次改动里最值钱的一处,是状态码本身不作为断言
probe "GET /auth/get-session (anonymous — must be REFUSED)" 401 …
assert_body '.error.code == "UNAUTHENTICATED"' …
⭐ 理由写在代码旁边而不是只写在 PR 里:401 也可能来自 origin 检查、限流、dev server 前面的代理,或日后新加的任何守卫 —— 那些都会让探针保持绿,却不再测量匿名会话契约。信封 code 才是把它钉住的那一半,而 UNAUTHENTICATED 是本仓自己拥有并发布的码(ADR-0112 派生映射),⛔ 不是 vendor 内部符号。这是把"绿"与"在测量"分开,正是本卡所属那一族缺陷的解药。
同时核过的:
- 登录态探针未动 —— 分支上
:1171仍是200+ 原断言,与 main 逐字一致 ✅ assert_body的签名是jq -e "$1" "$BODY",紧跟在 probe 之后读的就是该次响应体 ✅ 与文件内既有用法同形- 头部"声明契约表"同笔更新为 401 ✅ —— ⛔ 没有留下一句仍写着 200 的旧话
- 无 changeset ⇒ 打的是
skip-changeset标签(⛔ 不是正文一句话)✅ check-clause2-carriers --pair 18140→ 两载体一致、diff 无放宽征兆,exit 0 ✅- CI:33 项,RED: none(19 success / 11 skipped / 3 in_progress);未挂
needs:contract-review✅ - 不触及任何治理面 ✅
附带发现 #18139:本席已独立验真,⇒ 交回 domain:services
dev 报的"客户端 SDK 仍写着 -> 200 null"经本席在 origin/main 上重测属实:
packages/client/src/index.ts:1474——GET /api/v1/auth/get-session (anonymous) -> 200 nullpackages/client/src/organization-get-active-member-addressing.test.ts:40与:88—— 测试替身仍以false建模「匿名 ⇒/get-session答字面null」
空对照读 0,所以这两处是真读数。⭐ 其中替身那一半是更有后果的:一个仍在模拟产品已不再产生的服务端答复的 fake,会让依赖它的用例在真实行为变更后继续绿。
⇒ #18139 已由 dev 不带标签立卡(正确 —— domain:* 是分诊的字段),本席 ⛔ 不代打标签、⛔ 不代修:那是 domain:services 的面。
⚠️ 一条明确的 NOT MEASURED,接受并转述
dev 声明没有真跑冒烟本身(要打 ~70 个 tarball、在工作区外装并构建一个项目、再起 dev server,超出本容器前台预算)。⭐ 这是正确的做法:exit 3 = 前提未满足 = 未测量,⛔ 不许拿一个没跑过的东西当"验证过"。
⇒ 真正的判词是 RC head 上下一次 publish-smoke / packed-tarballs 的状态。main 提交上却不测那棵树(它测的是 RC)—— ⛔ 谁都不要把它的红点读成 main 红,#14000 记的就是这个陷阱。
Generated by Claude Code
Closes #18079
Clause-②: no— this neither loosens an accept set nor widens a published contract surface. It tightens one CI assertion onto the contract the code already declares.The question this card asked, and the answer
#18079 named two hypotheses and asserted neither: either the anonymous
GET /auth/get-sessionbehaviour regressed (fix the auth surface), or the smoke's expectation is stale (fix the probe). The second is true, and it is established from the tree rather than inferred.packages/plugins/plugin-auth/src/anonymous-session-refusal.tsexists for exactly this, and its header records the ruling:AuthManager.handleRequestcalls it on the one seam every vendor route passes through, and the call site says in as many words that this one is the admission move:So the 401 is the declared contract. The smoke was asserting a shape the platform deliberately stopped serving. ⇒ The auth surface is not touched by this PR.
Three independent legs agree on the exact envelope, so the assertion is pinned to a measurement and not to a guess:
anonymous-session-refusal.tsheader{"success":false,"error":{"code":"UNAUTHENTICATED","message":"Sign in first"}}packages/spec/src/api/errors.zod.ts:174401: 'UNAUTHENTICATED'Bisect — the true first failure, not the observed window
The card cautioned that the observed window (back to
b06b2db5c4) is not the start, because the runs ona83dbb6124and6d647858b7readno-runandcancelled. Correct — and the real boundary is a day earlier and is not amaincommit at all.main:374d9d3afa—fix(plugin-auth)!: an anonymous get-session is refused with the declared 401 envelope, not answered 200 null (#17881), 2026-09-12T19:15:14Z.git log --diff-filter=Anames it as the commit that adds the refusal module; the shallow graft boundary here isca0a1f83d6(2026-07-29), far older, so that add is genuine and not a graft artifact.34726616436, 2026-09-12T23:54:27Z, status posted to RC heada1effc8e44(chore: version packages, 2026-09-12T18:12:26Z).34728125950, 2026-09-13T00:30:11Z, job103645853837, status posted to RC heade612087feb(chore: version packages, 2026-09-13T00:29:49Z). Its log carries the same assertion and the byte-identical body as the card's evidence run.Those two runs carry the same
mainhead sha (a9c6477904) and opposite verdicts, which is the card's "NOT main-red" point showing up as data: the smoke tests the release candidate, not the commit the check attaches to. The decisive probe is therefore on content, not on timing —The firing control is present in both trees (the probe reaches them) and the nonsense control is absent in both (the probe can say no).
374d9d3afa(19:15:14Z) falls inside the interval between those twochore: version packagescommits (18:12:26Z → 00:29:49Z), so the two readings cross-validate.What changed
One file,
scripts/publish-smoke.sh:assert_body '.error.code == "UNAUTHENTICATED"'. The status alone would be satisfied by an origin check, a rate limiter or any later guard while measuring nothing — the same reasoning theSELF_REGISTRATION_CLOSEDprobe below it is already written under, and what this file's header means by "every assertion here is an HTTP status plus acodethis repo owns and publishes".auth.me()returns the literalnullfor an anonymous caller, which no value of its declaredSessionResponsecan express #17238 and the ruling, so the next reader does not "fix" it back — the card asked for this explicitly.The signed-in probe is deliberately untouched and still asserts 200: the refusal seam converts only a 200 whose body is exactly
null, so that answer is byte-identical to before.This is the #14000 move repeated — that card re-pinned this same script to the declared contract for
SELF_REGISTRATION_CLOSEDrather than touching auth runtime code, and left a standing ⛔ against relaxing an assertion back toward 200. This change runs the other way (200 → 401), which is the direction that prohibition protects.Verification
bash -n scripts/publish-smoke.sh→ exit 0.jqfilter, tested against the byte-exact body from job103645853837and two controls: observed body → exit 0; a 401 carryingINVALID_ORIGIN→ exit 1; the oldnull→ exit 1. So the assertion accepts the real refusal and rejects both a foreign guard and the retired shape.node scripts/pm/dispatch-gates.mjs --commands— all exit 0, includingcheck:bash32-floorandcheck:nul-bytes.publish-smoke / packed-tarballsstatus on the release-candidate head.Reverse-read, both directions
Which currently-true sentence does this make false? Inside this file, the header's
→ 200 (anonymous)row — updated in the same diff, so the file does not contradict itself. Outside it, none: I greppedget-sessionacrosspackages,scripts,docs,contentand.githuband no other statement depends on the smoke asserting 200.Which currently-false sentence does it make true? Two. The file's own claim that it asserts "the DECLARED first-run contract" was false for this row and is now true. And the status text
Fresh install of the release candidate: auth + CRUD green, which this gate has been unable to post since 2026-09-13T00:30Z, becomes reachable again — that is the release-blocking half.Zero results reported as such: no other consumer of the smoke's expectation exists; no docs page restates it.
Noted separately
The same grep found that #17881 moved the wire answer but left the client SDK still documenting
-> 200 null(packages/client/src/index.ts:1474) and a test double still modelling it. That isdomain:services, not this lane, and it is filed on its own card — see #18139. It is not addressed here.Merge channel
.github/workflows/publish-smoke.ymland warned that such a PR cannot be armed by the PM seat (HTTP 422, token lacksworkflows). That premise does not hold — the probe lives inscripts/publish-smoke.sh, the workflow's driver script, and the workflow YAML contains no assertion at all. This diff touches no path under.github/workflows/, so that caveat does not apply to it. It also touches no governed surface under Prime Directive #14.No changeset: the changed file is a CI driver shipped by nothing. Measured rather than assumed — the root package is
private: true, and of the 70 package manifests declaringfiles[], zero name this path. Theskip-changesetlabel carries that, not this sentence.Generated by Claude Code