Skip to content

test(runtime): e2e regression for notifications mark-read on the hono server (#3362) - #3388

Merged
os-zhuang merged 2 commits into
mainfrom
claude/mark-notification-read-404-jk16wp
Jul 21, 2026
Merged

test(runtime): e2e regression for notifications mark-read on the hono server (#3362)#3388
os-zhuang merged 2 commits into
mainfrom
claude/mark-notification-read-404-jk16wp

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes#3362.

TL;DR

#3362 does not reproduce on current main. The in-app notifications surface works end-to-end on the standalone os dev / os serve (hono) server — routes are mounted, the service resolves, and mark-read persists. #3354's fix is effective. What was missing is a test that actually proves it, so this PR adds the end-to-end regression test the issue asked for.

What the issue claimed vs. what actually happens

The issue reported that GET /api/v1/notifications and POST /api/v1/notifications/read[/all] return 404 on the hono server, blaming the dispatcher-plugin's route registrations for "not landing on the hono app."

I verified empirically by building main and booting the real CLI (objectstack dev --fresh) against both the CRM and showcase examples:

RequestResult on real os dev (hono)
GET /api/v1/notifications (unauth)401 — route reachable, self-gated (not a 404)
GET /api/v1/notifications (auth)200{ notifications, unreadCount }
POST /api/v1/notifications/read/all (auth)200{ success, readCount }
discovery services.notificationstatus: "available" (declared === enforced)
full cycle with a real digest notificationunread count drops 1 → 0, receipts flip to read

Supporting evidence that the dispatcher's routes do bind to the hono listener:

  • /ready (dispatcher-only route) → 200, already covered by dispatcher-plugin.ready.integration.test.ts.
  • /keys (dispatcher-only) → 401 (reaches the handler).
  • /mcp → the dispatcher's ownhandleMcp 501 ("MCP server is not available"), not a "hono-native handler" — so the premise that dispatcher registrations don't reach hono is incorrect.

The notification routes were added by #3354 (ee0a499) and have not been touched since, so main reflects post-#3354 behavior. The most likely cause of the original 404 is a stale @objectstack/runtime dist (a common monorepo gotcha: os dev loads packages/runtime/dist, which must be rebuilt after pulling source changes).

The real gap: coverage

The only prior guard (dispatcher-plugin.routes.test.ts) asserts route registration against a fake server. It cannot catch a real unmounting on hono, a notification-service resolution break, or a receipt-persistence regression — exactly the failure modes the issue feared.

Change

Adds packages/runtime/src/notifications.hono.integration.test.ts — a real end-to-end integration test (same style as the existing /ready integration test):

  • Boots the actual stack: in-memory driver + ObjectQLPlugin + MessagingServicePlugin + a minimal auth service + HonoServerPlugin + createDispatcherPlugin, on a real socket.
  • Delivers notifications through the real messaging pipeline, then drives mark-read over fetch exactly like the Console bell.
  • Asserts: discovery reports the service available; an unauthenticated request self-gates with 401 (route reachable, not a 404); listing shows the unread count; POST /read and POST /read/all flip the sys_notification_receipt rows to read and drop the unread count to 0.

It is a real guard, not a vacuous one: temporarily disabling the three server.<verb>() notification mounts in the dispatcher plugin makes it fail (404 instead of 401/200).

Also adds @objectstack/service-messaging as a devDependency of @objectstack/runtime (test-only; messaging does not depend on runtime, so there is no dependency cycle).

Testing

  • New test: 3/3 pass.
  • Full @objectstack/runtime suite: 40 files / 580 tests pass.
  • Related suites (dispatcher-plugin.routes, dispatcher-plugin.ready.integration, http-dispatcher): pass.
  • tsc --noEmit and eslint clean; @objectstack/runtime builds.

No changeset (test-only, no user-facing runtime change).

🤖 Generated with Claude Code


Generated by Claude Code

… server (#3362)
#3362 reported that `POST /api/v1/notifications/read[/all]` and
`GET /api/v1/notifications` 404 on the standalone `os dev` / `os serve`
hono server, so the unread badge never clears — claiming #3354's route
mounts never reached the hono listener.
This does NOT reproduce on current main. The dispatcher plugin's
`server.<verb>()` registrations DO land on the hono `IHttpServer` (the
same path the already-covered `/ready` probe takes), the `notification`
service resolves (backed by @objectstack/service-messaging), and mark-read
persists — verified end-to-end against the real CRM and showcase `os dev`
servers (unread count drops 1 -> 0, receipts flip to `read`). The /mcp 501
the issue cites is likewise the dispatcher's own handler, not a
"hono-native" one. The most likely cause of the original report is a stale
`@objectstack/runtime` dist built before #3354.
The real gap the issue exposes is COVERAGE: the existing guard only asserts
route *registration* on a FAKE server (dispatcher-plugin.routes.test.ts),
so it cannot catch a real unmounting on hono, a service-resolution break,
or a receipt-persistence regression. This adds the e2e test the issue asks
for — it boots the actual stack (ObjectQL + messaging + hono + dispatcher),
opens a real socket, delivers notifications, and drives mark-read over
`fetch`, asserting the `sys_notification_receipt` rows flip to `read` and
the unread count drops. Disabling the route mounts makes it fail (404 vs
401/200), so it is a real guard, not a vacuous one.
Adds @objectstack/service-messaging as a runtime devDependency (test-only;
messaging does not depend on runtime, so no dependency cycle).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QES3gEM1DdBkaK5maR4m57
@vercel

vercelBot commented Jul 21, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specCanceledCanceledJul 21, 2026 2:22pm

Request Review

@github-actionsgithub-actionsBot added size/m dependencies Pull requests that update a dependency file tests labels Jul 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

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

  • 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/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.

The e2e regression test in this PR is test-only (plus a test-scoped
devDependency) and changes no runtime code, so it releases nothing.
Add the sanctioned empty-frontmatter changeset to satisfy the
Check Changeset gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QES3gEM1DdBkaK5maR4m57
@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tooling and removed documentation Improvements or additions to documentation tooling labels Jul 21, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review July 21, 2026 14:23
@os-zhuang
os-zhuang merged commit 50cfb3d into mainJul 21, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/mark-notification-read-404-jk16wp branch July 21, 2026 14:23
os-zhuang pushed a commit that referenced this pull request Jul 21, 2026
… discovery (#3369)
Rebased onto main after the #3361 Server-Timing fix landed independently
(#3384) and the notifications mark-read e2e landed (#3388). This keeps only the
net-new route-parity work; the redundant #3361 fix is dropped in favour of
main's canonical version.
- plugin-hono-server: make the STANDALONE discovery honest. The static
`registerDiscoveryAndCrudEndpoints` list advertised metadata/packages/
analytics/workflow/automation/ai/notifications/i18n/storage/ui — none of
which HonoServerPlugin mounts on its own — so a truly standalone deployment
advertised a dozen routes that 404 (the literal `declared !== enforced` gap
#3369 cites). It now advertises only what it serves (`/data` + `/auth/me/*`).
Shadowed by the dispatcher/REST service-aware discovery under `os serve`, so
real deployments are unaffected.
- runtime: add the route-parity e2e gate. Boots the real hono app the way
`os serve` mounts it (hono server + dispatcher plugin, services provisioned),
reads `/api/v1/discovery`, and asserts every advertised/dispatcher route is
reachable (never 404/405/501) for anonymous AND admin principals, and that
discovery is service-aware in both directions (no dead advertisement). Closes
the class of "works in the dispatcher, dead on os serve" bugs (#3361/#3362/
MCP 501) with one boot-the-real-server test. Server-Timing and notifications
mark-read specifics are covered by #3384 / #3388; this gate does not duplicate
them.
- http-dispatcher: document why `routes.mcp` is advertised on the
`isMcpServerEnabled()` flag (the #2698 auto-load lockstep) rather than gated on
service presence — comment only, keeps the dispatcher symmetric with
@objectstack/rest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015si15Q1KWMKpVQWYsEvgcS
os-zhuang pushed a commit that referenced this pull request Jul 21, 2026
…3369)
Adds the "boot-the-real-server" route-parity gate #3369 calls for, rebased onto
main after the #3361 Server-Timing fix (#3384) and the notifications mark-read
e2e (#3388) landed independently. Keeps only the net-new, non-overlapping work.
- runtime: route-parity e2e gate (`route-parity.integration.test.ts`). Boots the
real hono app the way `os serve` mounts it (hono server + dispatcher plugin,
services provisioned), reads `/api/v1/discovery`, and asserts every advertised
/ dispatcher-registered route is reachable (never 404/405/501) for an anonymous
AND an admin principal, and that discovery is service-aware in both directions
(no dead advertisement). Closes the "works in the dispatcher, dead on os serve"
class (#3361/#3362/MCP 501) with one test. Does not duplicate the Server-Timing
(#3384) / notifications mark-read (#3388) / REST `/data`·`/meta`·`/ui`
(@objectstack/client) coverage.
- http-dispatcher: document why `routes.mcp` is advertised on the
`isMcpServerEnabled()` flag (the #2698 auto-load lockstep) rather than gated on
service presence — comment only, keeps the dispatcher symmetric with
@objectstack/rest.
Test + comment only; no runtime code changes (no-release changeset).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015si15Q1KWMKpVQWYsEvgcS
os-zhuang added a commit that referenced this pull request Jul 21, 2026
…3369) (#3386)
Adds the "boot-the-real-server" route-parity gate #3369 calls for, rebased onto
main after the #3361 Server-Timing fix (#3384) and the notifications mark-read
e2e (#3388) landed independently. Keeps only the net-new, non-overlapping work.
- runtime: route-parity e2e gate (`route-parity.integration.test.ts`). Boots the
real hono app the way `os serve` mounts it (hono server + dispatcher plugin,
services provisioned), reads `/api/v1/discovery`, and asserts every advertised
/ dispatcher-registered route is reachable (never 404/405/501) for an anonymous
AND an admin principal, and that discovery is service-aware in both directions
(no dead advertisement). Closes the "works in the dispatcher, dead on os serve"
class (#3361/#3362/MCP 501) with one test. Does not duplicate the Server-Timing
(#3384) / notifications mark-read (#3388) / REST `/data`·`/meta`·`/ui`
(@objectstack/client) coverage.
- http-dispatcher: document why `routes.mcp` is advertised on the
`isMcpServerEnabled()` flag (the #2698 auto-load lockstep) rather than gated on
service presence — comment only, keeps the dispatcher symmetric with
@objectstack/rest.
Test + comment only; no runtime code changes (no-release changeset).
Claude-Session: https://claude.ai/code/session_015si15Q1KWMKpVQWYsEvgcS
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filesize/mtests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v16.0: mark-notification-read 404s on os dev/serve — unread never clears (#3354 not effective on hono server)

2 participants

@os-zhuang@claude