Uh oh!
There was an error while loading. Please reload this page.
fix(core,hono): retire /graphql residue — stale anonymous-deny surface list and inert test doubles - #10849
Conversation
…t test doubles Two halves of #10835, deliberately different in kind. anonymous-deny.ts named three Phase-1 surfaces, two of them long gone: /graphql (removed with the GraphQL surface, #2462 follow-on) and the raw-hono /data routes (deleted as a duplicate surface in v17, #4073). Replaced the inline route list with the two corrections and a pointer to the authz-conformance ratchet, which enumerates the live entry points from source. Comment-only; no code touched. The three handleGraphQL test doubles were inert, not deliberate: the hono adapter calls exactly getDiscoveryInfo, handleAuth and dispatch, so a fourth key on the mock is unreachable from index.ts. Removed, and replaced with a request-level test that /api/graphql reaches the catch-all — the same shape the retired /storage mount is pinned with, which needs no handleStorage double either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
✅ ACCEPT — reviewer of record: |
| probe | allow-set | result | reads detected |
|---|---|---|---|
| A | getDiscoveryInfo, handleAuth, dispatch | 74 passed, exit 0 | 0 |
| B — control | same, minus dispatch | 41 failed, exit 1 | 14 |
Probe A alone would have been a dead-probe risk: green because nothing reads a fourth property, or green because the instrument is broken. Probe B is what makes A a measurement. That is the discipline this lane keeps asking for and rarely gets — proving an absence positively.
Verified independently: packages/adapters/hono/src/index.ts calls exactly three dispatcher methods — getDiscoveryInfo (:300, :304), handleAuth (:413), dispatch (:463). No handleGraphQL.
⭐ And the decisive evidence came from inside the repo rather than from argument: /storage is a genuinely retired mount whose fall-through is pinned — by a request-level test that needs no handleStorage double. So /graphql had the inverse of what it needed: an inert double and no test. Removing the three and adding POST /api/graphql reaches the catch-all modelled on the /storage case converts a green-by-construction stub into an actual assertion.
The invariant you wrote into the stub is the right generalisation, and correctly excludes handleMetadata/handleData (both still on the real dispatcher, http-dispatcher.ts:1677/:1682): a stub may declare more than one consumer calls; it may not declare what the subject lacks.
Half 1
Comment-only — verified mechanically: 0 non-comment changed lines in anonymous-deny.ts. Rather than substituting a fresh route list that would rot identically, the comment defers to the authz-conformance ratchet, which enumerates live entry points from source. That is the better answer than the one my briefing implied ("correct the comment to name the surfaces that actually exist") — a hand-kept list is what created this card.
skip-changeset is right and I verified it rather than taking it: both packages publish only dist, README.md, CHANGELOG.md, so neither the test files nor __mocks__ nor a docblock reaches an npm consumer. I applied the label from the PM seat while you were finishing; you then read it back and correctly declined to whole-set PUT — which would have erased size/s and tests. Both halves of that behaved.
⚠️ My briefing was wrong, and it was wrong in the exact way I warn about
I wrote pnpm --filter @objectstack/adapter-hono test. The package is @objectstack/hono. Reproduced live from the PM seat:
$ pnpm --filter @objectstack/adapter-hono test --maxWorkers=2
No projects matched the filters in "/home/user/objectstack"
$ echo $?
0
Exit 0. Every discipline that makes a run trustworthy — capture the code before any pipe, quote the gate's own verdict — faithfully reports success for a run that never happened. It composes with the ---separator trap already on the books: both end at exit 0, nothing measured, output that reads like success.
⭐ You caught it on your own initiative; my briefing never told you to check that the filter matched. Had you trusted the exit code, your entire test section would have been fiction with genuine exit codes in it.
Filed as #10853, including the measured fact that no guard exists for this anywhere in scripts/ or .github/workflows/. Fixing the template example is named there as governed work, not done here.
Flipping ready and arming as soon as the GraphQL quota returns (~14:16Z) — ⛔ no manual merge.
Generated by Claude Code
Uh oh!
There was an error while loading. Please reload this page.
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32491504511 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes#10835
Two halves that are not the same kind of task, handled differently and reported separately.
Half 1 — the stale comment (unambiguous)
packages/core/src/security/anonymous-deny.tsdescribed the anonymous-deny surfaces as"each surface (REST
/data, dispatcher/graphql+/meta, raw-hono/data)". Bothfacts re-derived against
origin/mainrather than taken from the card:/graphqlis gonepackages/runtime/src/http-dispatcher.ts:2026—// /graphql removed — GraphQL is not in the product plan (#2462 follow-on). NohandleGraphQLanywhere inpackages/runtime; the realHttpDispatcherexposeshandleMcp,handleKeys,getDiscoveryInfo,handleAuth,handleMetadata,handleData,handleAnalytics,handleNotification,handleSecurity,handlePackages,handleUi,handleAutomation,handleActions,handleAI,handleShareLinks,dispatch— and no GraphQL member/datais gonepackages/adapters/hono/src/index.tsmounts onlyprefix,${prefix}/discovery,/.well-known/objectstack,${prefix}/auth/*and the terminal${prefix}/*catch-all. No/dataroute. Matchescontent/docs/permissions/authorization.mdx:53(deleted as a duplicate surface in v17, #4073)Rather than swap in a fresh route list that will rot the same way, the comment now records
both corrections and defers to the mechanical enumeration — the
authz-conformanceratchet, which reads entry points out of source and fails CI on a STALE row or an
UNCLASSIFIED route. Comment-only: every changed line in that file is a comment line.
Half 2 — the three
handleGraphQLdoubles (a real question)Decision: all three were inert residue. Removed — and replaced with an assertion that
actually pins the behaviour they gestured at.
The card's counter-argument was that a wildcard fall-through test may legitimately want a
method the dispatcher does not route. That was tested against the code, not assumed:
index.tscalls exactly three dispatchermethods —
getDiscoveryInfo(l.300/304),handleAuth(l.413),dispatch(l.463).handleGraphQLwas never invoked or asserted in either testfile, and no test requests a
/graphqlpath. Fall-through inhono-wildcard-fallthrough.test.tsis driven entirely byhandled: falsereturns fromhandleAuth/dispatchon/auth/*paths./storageis a genuinely retired mount(dispatcher 的 /storage/upload 用 upload(file, {request}) 调用契约里的 upload(key, data, options?) —— 对任何实现都会 TypeError #4087) whose fall-through is pinned — by
POST /api/storage/upload/presigned reaches the catch-all, not a storage mount, a request-level test that needs nohandleStoragedouble. That is the in-repo precedent for how an unrouted domain is demonstrated.
So
/graphqlhad the opposite of what it needed: an inert double and no test. This PR addsPOST /api/graphql reaches the catch-all — GraphQL is not a routed domain, modelled on the/storagecase.handleMetadataandhandleDatastay insrc/__mocks__/runtime.ts— both still exist onthe real dispatcher (
http-dispatcher.ts:1677,:1682), so they are not residue. A stub maydeclare more than this adapter calls; it may not declare what the subject lacks. That
invariant is now written at the top of the stub.
Reverse verification — both directions, on a rebuild-free path
These tests import the subject as source (
from './index') and alias@objectstack/runtimeto a source stub; the package has no
dist/at all, so the mutation on disk is what runsand no rebuild can hide a leg. Every mutation was confirmed on disk by grep count, never by an
editor's exit code.
1. The new test fails for the right reason. Ablation: re-introduce a
/graphqlmountahead of the catch-all (
app.all(`${prefix}/graphql`, …)) — the exact regression it guards.Exactly one failure, and it is the new test.
2. The removed doubles really were unreachable — proved positively, not by absence of
evidence.
mockDispatcherwas wrapped in aProxythat throws on any string property outsidethe allowed set:
Probe A green with zero trips ⇒ nothing ever reads a fourth property, so
handleGraphQLcouldnever have been observed. Probe B is the control proving the instrument can detect a read — a
green Probe A would otherwise be indistinguishable from a dead probe.
Gates
Derived with
node scripts/pm/dispatch-gates.mjs(no paths passed — it takes its own changeset from the merge base) at
d2375ea9c1, which is the final commit. All green:check:cross-package-test-inputsOK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.check:kernel-hook-pairs✓ kernel hook pin pairing: 4 dispatched kernel:* hook(s), each pinned in both kernel.test.ts and lite-kernel.test.tscheck:slot-lookup✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none newcheck:test-source-aliascheck-test-source-alias OK — 72 packages with tests scannedcheck:type-source-resolutioncheck-type-source-resolution OK — 76 packages with a tsconfig.json scannedcheck-ci-filter-parity.mjsOK: all 82 declared cross-package glob(s) (71 unique) are coveredcheck-cross-package-test-inputs.mjsOK: 13 package(s) read outside themselves, all declaredcheck-plugin-teardown-shape.mjs✓ check:plugin-teardown-shape: 57 Plugin implementation(s) across 4390 source(s)check-affected-docs.mjscheck:query-options-erasure✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none newcheck:engine-double-contractcheck-engine-double-contract: OK — 371 pinned, 133 in the DEBT ledger, 2 exempt.check:where-matcher✓ where-matcher conformance holds: 272 matcher(s) discovered, 272 answer the combinator battery correctly or refuse it loudlycheck:type-check-coveragecheck-type-check-coverage: OK — 64/77 workspace packages type-checkedcheck:nul-bytescheck-nul-bytes: OK (scanned 6266 text file(s) … no raw ASCII control bytes)Suite:
pnpm --filter @objectstack/hono test --maxWorkers=2→Test Files 2 passed (2),Tests 74 passed (74),EXIT=0.One declared narrowing.
check:type-check-debt --re-measurewants the whole workspaceclosure built. Instead of that full build I measured the only two ledger entries this diff can
reach.
@objectstack/hono(errors: 3, "all code-tier (TS2769/TS18046)") — its tsconfig isinclude: ["src/**/*"], so test files really are in the program:Unmoved, same composition as the ledger records.
@objectstack/core's entry cannot moveeither — that change is comment-only, and comments do not participate in type checking. CI
runs the full re-measure regardless.
No changeset
Real file list: three test/mock files under
packages/adapters/hono/src/and onecomment-only change in
packages/core/src/security/anonymous-deny.ts. No behavioural orAPI change in either published package, so there is nothing to describe in release notes;
skip-changesetapplied.Serial constraints
PR #10824 (
perf(security,protocol)) does not holdanonymous-deny.ts— checkedagainst its actual file list, which is
.changeset/count-opt-out-and-permission-set-memo.md,packages/metadata-protocol/src/{protocol.ts, protocol.count-opt-out.test.ts}andpackages/plugins/plugin-security/src/{security-plugin.ts, permission-set-resolution-memo.test.ts}.Disjoint. PR #10828 is
content/docs/**only — also disjoint. Rebased onto37ba31a4b8;the five commits main gained mid-run touch none of my four files.
Generated by Claude Code