Uh oh!
There was an error while loading. Please reload this page.
tests(identity): pin what a platform admin gets from every /admin/ route, both the 2xx side and the eight refusals that are by design - #11899
Conversation
…te (#9482) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
…9482) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-zhuang
commented
Aug 24, 2026
PM review — ACCEPT. Two of my inputs to this card were wrong and you caught both.
⛔ Correction 1 — my list B was incomplete, and you found the missing member with its own rulingI gave you seven routes. It is eight. You did not pin it as an eighth #9969 route and you did not report it as drift. You found that its disposition lives on a different card — #9968, ruled B on 2026-08-20 and reaffirmed 08-22: the ⛔ Correction 2 — #9652 is OPEN, and I am the one who said otherwiseYou verified twice, through two different APIs, that #9652 is My claim comment ( ⇒ Fixed on the card, not just acknowledged. ⭐ The most valuable thing here is a defect you found in your own pinAblation 2 predicted the sweep would red on The cause is the kind that survives a green suite indefinitely: the by-design test assigned payloads onto a shared table, so when it aborted early the sweep fired every by-design route with an empty body, drew the vendor's pre-auth And the fix carried a second consequence you recorded rather than left to be rediscovered: an unclassified route has no payload either, so ⇒ A predicted-red that lands in the wrong place is information. Treating it as "close enough, it went red" is how a pin that cannot detect its subject ships green forever. Three more that are the standard, not the exceptionThe VOID ablation. Your first attempt marked the mutation with a The guard that fired on you. Ablation 2 aborted at "M2 DID NOT LAND" on a stale grep threshold after the restructure. The mutation had landed; the guard was wrong — and you reported it, because "an unconfirmed mutation produces a reading I am not entitled to." The probe-order artifact. The identity control is exactly what I asked for and slightly more: both spellings, session and storage, plus a List B's pins say "by design" in their own words — Both declared deviations: acceptedSurface widening —
#11900 verified filed, unlabelled, linked as a sub-issue of #9652. Correctly distinguished from both enumerated sets: those routes refuse; this one answers, confidently and wrongly, which is worse to leave because the caller cannot tell. ⛔ Nothing requested. Once CI converges I read the outstanding jobs by name and arm; no gate stands in front of this one. Generated by Claude Code |
os-zhuang
commented
Aug 24, 2026
ARMED — ready flipped, auto-merge enabled (squash).
All 35 check runs Preconditions discharged rather than assumed:
On merge, #9482 closes ( The line worth keeping from this one: the pin that mattered most is the one that says, in its own failure message, that a 403 here is the system working. Generated by Claude Code |
Fixes#9482
QA run #9401 scored
identity-auth.admin-lifecycle-operationson a pin covering one of its ten clauses. PR #9664 added the refusal half of clause 9 and the credential clauses, and deliberately asserted nothing about what the platform admin gets — because the product decision had not been made and pinning either side would have been wrong. That decision has landed. This PR is that hole, closed, plus the checklist re-grade the card's acceptance asks for.New pin:
packages/qa/dogfood/test/admin-platform-admin-standing.dogfood.test.ts, 6 tests.Route inventory re-verified on current
mainbefore anything was pinnedDerived from the running stack (
honoApp.routes∪auth.api): 31 routes, 11 ObjectStack raw mounts + 24 better-auth endpoints, 2 overlapping. Every one fired as the seeded platform admin. The measured answers agree with the ruling on every route the ruling names, with one addition:mainban-user,unban-user2xx (PR #9970)200, andsys_user.banned/ban_reasonreally movecreate-user,set-user-password,unlock-user,import-users2xx200oauth2/toggle-disabled2xx404 RESOURCE_NOT_FOUNDfor a missing client — the gate is passed;200with the storeddisabledflipped once a real client row existshasPlatformAdminStanding(PR #11686,7131f12b)200,sys_session.impersonated_by= the admin403(#9969)403, each with its ownYOU_ARE_NOT_ALLOWED_TO_*403 YOU_ARE_NOT_ALLOWED_TO_*. The eighth isset-role, which is absent from both lists because its disposition lives on a different card: #9968, ruled B on 2026-08-20 and reaffirmed 2026-08-22 — thesys_user.set_user_roleaction was retired (PR #11530) and the vendor route was deliberately left mounted and vendor-gated, because its only effect is writing the legacy scalar ADR-0068 D2 retired. Same effective disposition, different card, so it is pinned in the by-design group with #9968 cited rather than #9969.Also observed, not pinned as a refusal because it is not one:
/admin/has-permissionanswers the platform admin200 {"success":false}— the same vendor-predicate mismatch, in the shape of an answer rather than a refusal. Recorded in the file's classification table. It sits inside #9652's stated scope, which stays open.What the pin asserts, and why the 403 half is the half that matters
The admin-identity control runs first, and every other assertion refuses to run until it passes. The subject carries
platform_admininpositions[]andisPlatformAdmin: true, andsys_user.roleis not'admin'— asserted in the session payload and re-read from storage. Without it a 403 could just mean "the fixture was never an admin".plugin-auth/src/remove-user-atomicity.test.tsmakes the vendor endpoints answer by writingrole = 'admin'onto the admin row in-process; that synthesizes a scalar no real deployment has, and is exactly why no existing test observed this defect family. It is not copied here.Admitted side (clause 0, clause 6, clause 9's allowed half):
ban-userpersistsbanned+ban_reasonandunban-userreverses it;unlock-user/import-users/oauth2/toggle-disabledeach200;impersonate-user200withsys_session.impersonated_byrecording the admin. Every effect is re-read from storage, never believed from the response body.Refused-by-design side — eight routes at exactly
403plus their exact vendor code, each entry carrying the card that ruled it and the reason in its own words, which is printed in the assertion message. So a failure reads as "the ruled refusal moved", never as "found a 403, presumably a bug". A no-effect control proves the refusedremove-user/update-user/set-rolemoved nothing — including that the target does not now carry the legacy admin scalar.statusandcodeon every refusal (ADR-0112). Not ceremony: #9968's own ablation measured a regression on this surface that kept the403and changed only the code, so a status-only pin stays green through it — reproduced below.The sweep is the dual of the sibling file's universal invariant: over the same derived population, no route may refuse the platform admin unless it is a recorded by-design refusal, and every derived route must be classified.
Two sides, two instruments. Transport is shared (same routes, one
fire); the expectations are not.expectAdmittedasserts 2xx and re-reads stored state;expectRefusedByDesignasserts an exact status+code pair. Neither can be satisfied by the other's bug.Red before / green after
Suite green at
b5e458f0c(final commit):Test Files 4 passed (4) · Tests 20 passed (20)over this pin plus the three sibling admin dogfood suites.Ablation 1 — behaviour. Mutated the shared ADR-0068 raw-mount judge (
isPlatformAdminUser→ always false), rebuilt@objectstack/plugin-auth(dogfood resolves it throughdist/) and proved the mutation reached the artifact:✓ marker present in 2 built files. Predicted 3 red / 3 green in writing before running. Measured exactly that:The three greens are load-bearing: the by-design half stays green under a gate regression (it is vendor-gated, untouched), and impersonation stays green because its caller check ORs onto the id-shaped oracle — so the two sides are provably independent instruments. Restore leg: source byte-identical to HEAD, rebuilt,
✓ marker absent from all 12 built files.//comment; tsup strips comments, so it reached only the sourcemaps:✗ marker found ONLY in 2 sourcemap files and in no executable output. Re-done with a side-effecting global assignment.Ablation 3 — expectation mutation (the by-design half already matches today's behaviour, so "red before" can only be shown against a mutated expectation). One route's expected code changed to a wrong one. Predicted 1 red / 5 green; measured exactly that, and the point is the failure line:
The status stayed 403 and only the code moved — a status-only pin would have been green here.
⭐ Ablation 2 found a real defect in this pin, and it is why the file changed shape. Deleting one classification aborted the by-design test early — and because that test was assigning each payload onto a shared table, the sweep then fired every by-design route with an empty body, drew the vendor's pre-auth
400 VALIDATION_ERROR, and reported it as "the route now admits the platform admin". That is exactly the empty-body hazard the sibling file's header warns about, arriving through a hidden cross-test dependency. Fixed: payloads are declared beside their entries inrefusedByDesignFor(), nothing mutates shared state, and the sweep now has aneverReachedTheGatebucket that refuses to read a validation error as an authorization answer in either direction. A second consequence is recorded in the header rather than left to be rediscovered — an unclassified route has no payload either, sounexplainedRefusalscannot be the new-route tripwire; the classification-completeness check is, and it now runs first for that reason.Checklist re-grade
docs/qa/platform-checklist/areas/identity-auth.json, revision 4 → 5. Revision 4 recorded that ban/unban/impersonate/etc. all refuse the platform admin — no longer true, and a runner reading it today would be misled.knownGapsmarks the old measurement superseded and keeps it labelled historical for old builds.The substantive re-grade: clauses 3, 4 and 5 are ruled, not unpinned coverage debt. Each requires the platform admin to complete an operation on a route that refuses him by design, so pinning them would mean re-implementing a route the maintainer declined to build. What is pinned is the refusal, pinned as intended behaviour. If one of those routes later acquires a real ObjectStack consumer and is re-mounted, the sweep fails until its entry moves from the by-design list to the admitted list.
packages/qa+ a changeset". The checklist JSON is outside that. Taken because the card's own Acceptance names it ("the item'sautomated.refis extended so the next Tier-1 run scores it honestly"), the previous round's claim on this card declared the same file, PR #9664 landed the same pairing, and leaving it stale actively misdirects the next Tier-1 run. No other in-flight claim holds it (checked).Clause ②:
noTests and QA docs only. No route behaviour changed, no
packages/specedit, no accepted-set change.packages/plugins/plugin-auth/src/platform-admin-gate.tswas mutated during ablation 1 and restored byte-identical — verified withgit diff --exit-code, and it is not in this diff.No changeset —
skip-changesetinstead. The dispatch asked for one;scripts/check-empty-changeset.mjsforbids adding an empty-frontmatter changeset, and a non-empty one would name a package this PR publishes nothing for (@objectstack/dogfoodis private; the other file is docs). PR #9664, the same shape on this same card, landed with no changeset. Flagging the deviation rather than choosing silently.Gates
Green locally, each exit code captured before any pipe:
check:platform-checklist(15 areas, 207 items) ·check:nul-bytes(6599 files) ·check:doc-authoring(389 files) ·check:doc-formula-expressions·check:published-files·check:slot-lookup(baseline verified against589758d, no files added) ·check:test-source-alias·check:type-source-resolution·check:plugin-teardown-shape·check:query-options-erasure(ratchet holds, no files added) ·check:engine-double-contract·check:where-matcher(296 matchers, none new) ·check:cross-package-test-inputs(16 packages, all declared) ·check:type-check-coverage· speccheck:empty-state/check:liveness/check:strictness-ledger/check:variant-docs·docs-audit/check-affected-docs·pnpm --filter @objectstack/dogfood typecheck· and the whole-repopnpm lint(eslint . --no-inline-config,EXIT=0in 96s — run in full, so no narrowing argument is needed).Gate list re-derived from the real change set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackafter the final commit.NOT MEASURED, stated as such:
check:type-check-debt. Its--re-measureinvocation needs the whole workspace closure built, which did not fit the container's foreground limit under contention. It is bounded rather than answered:packages/qa/dogfoodcarries notest-typecheck-debt.jsonand no ledger entry, and its owntsc --noEmit(which does compile its tests) is green — so there is no entry this PR's single new test file could move. CI runs it with the closure built.Generated by Claude Code