Skip to content

fix(runtime,metadata-protocol): every dispatcher domain gates on handlerReady, not slot occupancy (#4058 step 2) - #4086

Merged
os-zhuang merged 2 commits into
mainfrom
claude/dispatcher-handler-ready-1odd28
Jul 30, 2026
Merged

fix(runtime,metadata-protocol): every dispatcher domain gates on handlerReady, not slot occupancy (#4058 step 2)#4086
os-zhuang merged 2 commits into
mainfrom
claude/dispatcher-handler-ready-1odd28

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #4058. 接 #4082(step 1) 落地 step 2。

#4000/analytics 执行了 ADR-0076 D12 结论第 3 条,其余域仍只判槽位占用 —— 自述 handlerReady: false 的占位者照样被当真实现调用,编造的答案以 200 发出。step 1 把两类实现分开了,这个 PR 是读这个区分的门。

#4082 的分工:本分支开着的时候 #4082 落地了分类那一半,而且做得更好 —— 一张可审的 DEV_STUB_SELF_INFO 表、连 kernel 自己的 fallback 一起覆盖(_fallback: true 那个没人认识的标记,本分支曾另开 #4089,现已在源头修掉并关闭)、彻底去掉 _dev: true 而不是留作来源标记、还给无 HTTP 面的 cache/queue/job/realtime 显式设了 handlerReady: false(本分支漏了这层)。所以本分支只留 #4082 明确留开的那半:门。分类实现与其测试已按 main 的版本收敛。

门(一个谓词)

新增 packages/runtime/src/service-serveable.tsisServiceServeable(吸收了 #4000isAnalyticsServiceServeable),由以下全部读取:

  • 六个 dispatcher 自有域:/analytics/automation/notifications/ai/storage/i18n
  • 路由挂载门(analytics)
  • discovery 的 routes / features
  • metadata-protocol discovery builder 的 route 广告

自述 handlerReady: false 的占位者,得到的回答与空槽完全一致 —— 各域原本的 404,或 /storage/i18n 用的那个显式 501。

判据是 handlerReady 而不是 status,这正是它能安全推广的原因:真干活的实现声明 degradedhandlerReady 默认 true,继续服务。所以 file-storage / i18n / metadata / workflow 没有任何回退,只有编造的那类被当空槽。

discovery.services.* 保持按存在与否判定:已注册的 stub 仍报 { enabled: true, status: 'stub', handlerReady: false } 且不带 route —— 比塌缩成 unavailable 说得更多。

顺手修好的一处

/ai 在 stub 占槽时原本更差:因为 stub 是 truthy,请求会穿到 !routes 的 503「AI service routes not yet initialized」,同时丢掉 console 每次导航轮询的 GET /ai/agents 空列表礼遇。视同空槽后两者都回来了。

刻意没动的部分

验证

  • 合并 main 后:runtime 923 / plugin-dev 8 / objectql 1183 / metadata-protocol 99 / core 415 全过;pnpm build 71/71;eslint 干净
  • 端到端(真实 plugin-dev 装配 + dispatcher,跑在 step 1 的分类之上):
services:  automation   stub      handlerReady=false route=—
           notification stub      handlerReady=false route=—
           ai           stub      handlerReady=false route=—
           file-storage degraded  handlerReady=true  route=/api/v1/storage
           i18n         degraded  handlerReady=true  route=/api/v1/i18n
requests:  GET  /automation    handled=false      POST /ai/chat      404
           GET  /notifications handled=false      GET  /ai/agents    200 {"agents":[]}
           GET  /i18n/locales  200

三个 stub 仍然注册着,但已经拿不到 route、请求得到空槽的回答 —— 这就是"不删也诚实"的证据,也是把删除留给 #4093 的依据。

新增测试:域级(stub → 空槽答案 且不被调用;degraded → 照常服务)、discovery 级(routes/features 与域一致,services 仍报 stub)、metadata-protocol builder 级(含非 dispatcher 自有槽位保持按存在判定的边界)。

关联的域外 issue(Prime Directive #10

🤖 Generated with Claude Code

https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B

…, and give each dev stub its own honesty class (#4058)

#4000 made `/analytics` execute ADR-0076 D12's third conclusion ("consumers
treat only `handlerReady: true` as a real capability") and left the rest of the
class open, because adopting it per-domain needed a decision the issue could not
make yet: every plugin-dev stub carried the single `_dev: true` marker, which
normalizes to `status: 'stub', handlerReady: false`. One label for "invents
answers" AND for "really does the work, in memory" — so no gate could tell the
two apart without either 404ing working in-memory implementations or keeping
fabricated data on the wire.

So the classification comes first, and the gate follows it.

Classification — each dev stub declares its own class via the standard
`__serviceInfo` marker:

- `degraded` (really does the work, reduced capability; `handlerReady` defaults
  true): `cache`, `queue`, `job`, `file-storage`, `search`, `i18n`, `realtime`,
  `workflow`, `metadata`. Their domains keep serving them — which is why
  `/storage` and `/i18n` do not regress.
- `stub` (fabricates): `data`, `auth`, `security.*`, and the shapeless
  placeholder used for a slot with no factory.
- Retired outright, joining `analytics`: `automation`, `notification`, `ai`.
  Each one's headline method reported success for work that never happened —
  `execute()` → `{ success: true }` with no flow run, `send()` → a messageId for
  a message nobody receives, `chat()` → a placeholder answer. An empty slot is
  one less layer of indirection than a slot that must be ignored, and it is
  what production already has without the plugin.

`_dev: true` stays as the plugin-dev provenance tag (AppPlugin's i18n
diagnostic reads it); `__serviceInfo` decides the class.

Gate — one predicate, `isServiceServeable` (new `service-serveable.ts`, which
absorbed `isAnalyticsServiceServeable`), read by the `/analytics`,
`/automation`, `/notifications`, `/ai`, `/storage` and `/i18n` domains, the
route-mount gate, discovery's `routes`/`features`, and the metadata-protocol
builder's route advertisement. A slot whose occupant self-declares
`handlerReady: false` is answered exactly as an empty slot is — the domain's own
404 or 501 — so what is advertised and what is served cannot disagree.
`handlerReady`, not `status`, remains the test.

`discovery.services.*` stays presence-gated: a registered stub still reports
`{ enabled: true, status: 'stub', handlerReady: false }` with no `route`, which
says strictly more than collapsing it to `unavailable` would.

`/ai` gets better for the stub case: an occupied-but-unserveable slot used to
fall through to the `!routes` 503 and lose the `GET /ai/agents` empty list the
console polls for. Both are restored.

Deliberately NOT gated: `data` / `auth` / `security.*`. Those stubs back the dev
stack's own core loop, so gating their domains would 404 the dev stack itself —
a separate decision. They are honestly labelled either way. `search` /
`realtime` are unaffected: neither has a dispatcher domain.

Verified end to end against a real plugin-dev slate: the four retired slots
report `unavailable` with no route and 404, `/ai/agents` answers `{ agents: [] }`,
and the `degraded` slots keep their routes and serve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 9:57am

Request Review

…cile with #4082 (#4058 step 1)

#4082 landed the classification half of #4058 while this branch was open, and did
it better: `DEV_STUB_SELF_INFO` is one reviewable table, it covers the kernel's
OWN fallbacks too (the `_fallback: true` marker no consumer recognized — filed
from this branch as #4089, now fixed at the source), it drops the blanket
`_dev: true` from plugin-dev rather than keeping it as a provenance tag, and it
sets `handlerReady: false` on the surfaceless `degraded` fallbacks
(`cache`/`queue`/`job`/`realtime`) — a refinement this branch missed.

So this branch keeps only what #4082 explicitly left open: step 2, the gate.

Dropped from this branch as superseded:
- the per-stub `devDegraded`/`devStub` markers and their tests (main's table wins);
- retiring the `automation` / `notification` / `ai` stubs. Step 1 classes them
  `stub`, so the gate already answers their HTTP surface as an empty slot without
  deleting anything — verified end to end below. Whether the fabricating class
  should be registered at all is the wider evaluation in #4093 (A tier), which is
  where that deletion belongs alongside `data`/`auth`/`security.*`.

Kept: `isServiceServeable` + the six domain gates, the discovery `routes`/
`features` alignment, the metadata-protocol route suppression, their tests, and
the ADR/docs/changeset text — the ADR annotation now reads as step 2 on top of
step 1 instead of re-claiming the classification.

The gate is compatible with step 1's table by construction, because it reads
`handlerReady` and not `status`: `file-storage` / `i18n` / `metadata` / `workflow`
declare `degraded` with `handlerReady` defaulting to true and keep serving, while
the surfaceless `degraded` fallbacks have no dispatcher domain to gate.

Verified against a real plugin-dev slate after the merge: `automation` /
`notification` / `ai` are registered and classed `stub` ⇒ no route advertised,
`GET /automation` and `GET /notifications` unhandled, `POST /ai/chat` 404, and
`GET /ai/agents` still answers `{ agents: [] }`; `file-storage` / `i18n` /
`workflow` / `metadata` keep their routes and serve. runtime 923, plugin-dev 8,
objectql 1183, metadata-protocol 99, core 415 all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/runtime.

19 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang os-zhuang changed the title fix(runtime,plugin-dev): gate every dispatcher domain on handlerReady, and give each dev stub its own honesty class (#4058) fix(runtime,metadata-protocol): every dispatcher domain gates on handlerReady, not slot occupancy (#4058 step 2) Jul 30, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 10:10
@os-zhuang
os-zhuang merged commit bb192c4 into main Jul 30, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/dispatcher-handler-ready-1odd28 branch July 30, 2026 10:11
os-zhuang pushed a commit that referenced this pull request Jul 30, 2026
#4058 step 2 (#4086) landed in between and touched the same ground: it gave
every dispatcher domain the shared `isServiceServeable` predicate, `/storage`
among them. Resolution:

- `domains/storage.ts` — deletion stands. #4086 gated the bridge; it did not
  fix the call, which is still `upload(file, { request })` against
  `upload(key, data, options?)`. Gating a handler that cannot serve either
  answer only changes which failure a caller gets.
- `http-dispatcher.ts` discovery — take main's `isServiceServeable` and drop
  the local `filesServeable` this branch had introduced. Same predicate, better
  home. The comment records why `file-storage` stays gated even though its
  surface is no longer a dispatcher domain: `handlerReady` is not standing in
  for anything there, it IS the fact the advert needs.
- The two `/storage` cases #4086 added to the handlerReady block go with the
  domain. The `degraded store keeps serving` one asserted a 200 off `upload`
  mocked to resolve `{ key }` — a return value `Promise<void>` does not have,
  the same imagined contract every other retired storage test encoded.
- `metadata-protocol` — `file-storage` stays in `DISPATCHER_GATED_SERVICES`;
  its comment no longer cites a `/storage` bridge that exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015T5CeitwV1jXLvsL1A84tw
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…, empty slots stay empty (#4093) (#4103)

The evaluation #4093 asked for, recorded as a decision. The founding
premise of plugin-dev's fill-every-slot design is refuted by production
semantics; the design is retired and plugin-dev converges to a pure
assembly plugin. Tiered disposition ratified into two implementation
PRs (Tier A+C+guard+auto-wire, then Tier B+#4089+docs), sequenced
behind the in-flight #4086. Implementation tracked in #4104.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BEM82RNKyyyTNPYHK1VJJS
os-zhuang pushed a commit that referenced this pull request Jul 30, 2026
…ped body

Semantic conflict, not a textual one. #4058 step 2 (#4086) landed on main while
this branch was open and added `http-dispatcher.test.ts`'s "a stub slot 404s per
route and keeps the /ai/agents empty list", which asserts the body is exactly
`{ agents: [] }` — the shape #4053 enveloped one commit earlier. The two merge
cleanly and the test fails, which is why CI caught it and the merge did not.

The courtesy that test pins is unchanged: a stub-occupied AI slot still answers an
empty list rather than a fault. The list just travels under `data` now, and the
assertion says so — plus that no `agents` key survives at the top level, so a
revert to the bare body fails here too.

Also swept the repo for any other reader of the old shape. Three hits, all
correct: the two negative assertions above, and `client/src/index.ts`'s
`body?.agents` — which is exactly what the relocation variant keeps working.

Merged main in rather than rebasing, per the branch's history so far.

Verified against the full suite the way Test Core runs it —
`turbo run test --filter='!@objectstack/dogfood'`: 129/129 packages, runtime 924.

Two worktree-staleness traps on the way, both reading as code failures:

  - `plugin-auth` could not resolve `hono` — the merge moved the lockfile and I
    had not reinstalled. `pnpm install --frozen-lockfile` fixed it.
  - `runtime` failed under turbo but passed standalone — the gitignored
    `.objectstack/data/memory-driver.json` accumulating rows again.

Neither is CI-visible; CI installs clean and checks out fresh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…d envelope (#4053) (#4124)

* fix(runtime): the /ai/agents degraded fallback answers in the declared envelope (#4053)

The last unenveloped SDK-addressable route. The framework's degraded fallback —
what an open-source runtime with no service-ai answers — now returns
`{ success: true, data: { agents: [] } }` via `deps.success`, and the guard's last
ratchet retires with it: 0 ratcheted on both surfaces.

## Why `data: { agents }` and not `data: []`

#3983 set the precedent that `data` carries the payload directly, and following it
here would have looked consistent. It would also have been wrong, silently.

`AiAgentsResponseSchema` is a DECLARED payload schema; share-links' `{ links }` was
an ad-hoc wrapper with none. So this is the #3843 relocation — the declared payload
moves under `data` unchanged, as SettingsNamespacePayload did — not a reshape.

That decides the blast radius. `unwrapResponse` returns `body.data` when a body has
a boolean `success` AND a `data` key, so `data: { agents }` keeps
`client.ai.agents.list()` reading `.agents` off it, while `data: [...]` would make
`.agents` undefined and the method answer `[]`.

An empty list is not a visible failure on this route: `useAiSurfaceEnabled` gates
the ENTIRE AI surface on `agents.length > 0`, and an empty catalog is the correct
answer for a seat-less user (ADR-0068) or a Community-Edition deployment. Broken
and legitimate look identical — no error, no 403, no log.

## Consequence: no lockstep

Because the SDK reads both shapes identically, each surface converts on its own
schedule. Cloud's service-ai still answers unenveloped and keeps working unchanged;
objectui already reads all four shapes (objectui#2992). The "three repos in one
batch" framing #4053 opened with does not apply to this variant — which is the
finding, not a shortcut around it.

Five tests in @objectstack/client pin both shapes, including the road not taken:
the flattened body asserts [], so the cost of choosing it is recorded rather than
rediscovered. The dispatcher's own fallback test now asserts the envelope and that
no `agents` key survives at the top level.

runtime 914, client 205, rest 505, spec 6987. All ten typecheck-job gates and the
six check scripts pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z

* fix(runtime): re-point #4058's new /ai/agents assertion at the enveloped body

Semantic conflict, not a textual one. #4058 step 2 (#4086) landed on main while
this branch was open and added `http-dispatcher.test.ts`'s "a stub slot 404s per
route and keeps the /ai/agents empty list", which asserts the body is exactly
`{ agents: [] }` — the shape #4053 enveloped one commit earlier. The two merge
cleanly and the test fails, which is why CI caught it and the merge did not.

The courtesy that test pins is unchanged: a stub-occupied AI slot still answers an
empty list rather than a fault. The list just travels under `data` now, and the
assertion says so — plus that no `agents` key survives at the top level, so a
revert to the bare body fails here too.

Also swept the repo for any other reader of the old shape. Three hits, all
correct: the two negative assertions above, and `client/src/index.ts`'s
`body?.agents` — which is exactly what the relocation variant keeps working.

Merged main in rather than rebasing, per the branch's history so far.

Verified against the full suite the way Test Core runs it —
`turbo run test --filter='!@objectstack/dogfood'`: 129/129 packages, runtime 924.

Two worktree-staleness traps on the way, both reading as code failures:

  - `plugin-auth` could not resolve `hono` — the merge moved the lockfile and I
    had not reinstalled. `pnpm install --frozen-lockfile` fixed it.
  - `runtime` failed under turbo but passed standalone — the gitignored
    `.objectstack/data/memory-driver.json` accumulating rows again.

Neither is CI-visible; CI installs clean and checks out fresh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z

* fix(tooling): drop the wildcard ledger's entry for the mount #4112 removed

NOT part of this branch's work — `main` is red on it and every open PR fails the
same check, so this unblocks rather than waits.

`origin/main` @ 857a6cf fails `check:wildcard-fallthrough` on its own, verified
in a detached worktree with no branch involved:

  ✗ packages/adapters/hono/src/index.ts:all `${prefix}/storage/*`
      DECLARED but not found by the scan.

Semantic conflict between two commits that both landed, not a bug in the guard.
#4112 retired that mount outright — the adapter now carries a comment where
`app.all(prefix + '/storage/*')` was, because the handler spoke a storage contract
that does not exist and the wildcard claimed the whole `/storage` subtree. #4116's
MOUNTS ledger (#4122) was written against a tree that still had it and landed
after the removal, so the entry was stale on arrival. Git merged both cleanly
because they touch different lines; the guard is what noticed, which is it working
on day one.

Dropping the entry is the whole fix — there is nothing left to ratchet when the
mount it names does not exist. Its `${prefix}/auth/*` sibling is untouched and
still correctly ratcheted to #4117. After: 6 yielding / 1 ratcheted / 5 exempt
(12 namespace-claiming mounts).

Flagged on #4122 as well, so whoever owns it can take it back if they would
rather land it themselves.

Also enumerated the ESLint job properly this time — it runs ELEVEN checks, not the
six I had been running (`doc-authoring`, `authz-resolver`, `release-notes` and
`node-version` were the ones I was missing, plus `pnpm lint` itself). All eleven
pass, and `pnpm lint` is clean once `.cache/` — the objectui console build artifact
an earlier `.objectui-sha` bump left in this worktree — is removed. Those 61
"errors" were entirely that directory; CI checks out fresh and never had them.

Full suite: 129/129 packages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z

---------

Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…login (#4113) (#4179)

`domains/auth.ts` carried a `mockAuthFallback` that answered
`POST /auth/sign-up/email`, `/register`, `/sign-in/email`, `/login`,
`GET /get-session` and `POST /sign-out` with 200 and a fabricated user plus a
24-hour `mock_token_*` session — for ANY email and ANY password, which was
never read. It shipped in `@objectstack/runtime`, not behind a dev-only
plugin, and gated on nothing but an empty `auth` slot, so
`os serve --preset minimal` and any embedder that mounts the dispatcher
without plugin-auth served it.

It was never a bypass: no session store backs that token, so
`resolve-execution-context.ts` still resolved anonymous and
`shouldDenyAnonymous` still denied data access. It was wrong in a different
way — it told the client the one thing a server must never lie about, that it
had authenticated someone, while discovery simultaneously reported
`auth: unavailable` and advertised no `routes.auth`.

Its stated justification ("MSW/browser-only environments") had no consumer in
this repo or in objectui, whose auth tests mock at the HTTP client layer. The
only things pinning it were nine tests asserting the mock itself.

ADR-0115 retired this class of fabricating fallback inside plugin-dev. This
was its last surviving member and the only one that shipped to production;
the lineage before it (#3891's analytics shim, #4000's dev stub, the three in
#4058/#4086, #4126's security trio) was retired the same way: deleted, not put
behind a flag, which would keep the code path in every release and become the
next thing nobody audits.

501, not 404, following `/i18n` — the nearest precedent in shape: a core
capability, a dispatcher-owned domain, an optional plugin behind it, and a
route discovery already declines to advertise when the slot is empty. The
route IS mounted; what is missing is the implementation behind it, which is
what 501 states and 404 would misdescribe. It also keeps faith with the one
true observation the mock was built on — its comment said it existed to keep
sign-in "from 404ing" — without the lie it answered that concern with. The
message names the remedy. A wrong-shaped occupant (a service without the
contract's `handleRequest`) takes the same 501, and that is the sharper case:
the slot is FILLED, so discovery advertises `routes.auth` while the request
used to get a fabricated session.

The `randomUUID` helper goes with it: a CSPRNG built solely to mint mock
session ids, dead the moment the mock is. `domains/auth.ts` drops 141 → 81
lines.

Two things the deletion left behind, both caught after the fact: the
route-envelope ledger's `handBuilt: 4` exemption for `auth.ts` drops to 0 —
those four better-auth-shaped bodies were the MOCK's, not the bridge's, since
the real service returns its own `Response` built nowhere in this file — and
`content/docs/permissions/authentication.mdx`, which documented all six
fabricating endpoints as a feature, now documents the 501 and points anyone
who relied on it at mocking in the HTTP client layer or loading AuthPlugin
(which needs no HTTP server).

Tests replace the nine that pinned the mock: all six formerly-mocked paths
parametrized so a partial re-introduction names itself, no `mock_`/user/session
in the body, the remedy named, an identical answer for any credentials (a 501
that varied by password would be an oracle), a registered service still
delegated to, and the wrong-shaped-occupant case.

Verified: runtime 940, plugin-hono-server 149, plugin-auth 579, rest 512 pass;
build 71/71; pnpm lint and all ten check:* scripts clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dispatcher 其余服务域仍只判槽位占用、不读 handlerReady —— #4000 在 analytics 一域落地后剩下的类推面

2 participants