Skip to content

test(app-shell): pin the bell panel's Unread/All render oracle — #4230's console predates #4199 (#4230) - #4284

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4230-bell-panel-regression
Aug 11, 2026
Merged

test(app-shell): pin the bell panel's Unread/All render oracle — #4230's console predates #4199 (#4230)#4284
yinlianghui merged 1 commit into
mainfrom
claude/issue-4230-bell-panel-regression

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Part of #4230. Deliberately not Fixes — no production code changed, and whether the card closes as not-reproducible (or stays open pending a QA re-run on a current console) is a triage call, not this PR's to make.

The premise did not survive the re-check

#4230 is filed as a regression: "the symptom is back on console 09987b68 … whatever #4199 fixed, it does not hold on this build." #4199 was never in that build.

fact value
vendored console pin (objectstack#7517 "Environment") 09987b680d53801c79f67d969b14e9bb732b8a22
that commit's date 2026-08-09 02:35:56 +0000
#4110's fix, PR #4199 7b07832327ea07af01208309b0e0d9c60df3645f
its merge date 2026-08-10 20:39:10 +0000
gap 42.1 hours — the fix landed after the pin
$ git merge-base --is-ancestor 7b0783232 09987b68 && echo YES || echo NO
NO — QA build PREDATES the fix

$ git show 09987b68:packages/app-shell/src/layout/AppHeader.tsx | grep -n 'isApp'
251:    if (!dataSource || !isApp) return;
338:    if (!dataSource || !isApp || !user?.id) return;      # the inbox poll
434:  }, [dataSource, isApp, user?.id]);
450:    if (!isApp || !user?.id) return;

Line 338 is the exact statement #4199 rewrote to if (!dataSource || !user?.id) return;. And #4199 is the only commit touching the bell path in the whole range 09987b68..7b0783232:

$ git log --oneline 09987b68..7b0783232 -- .../AppHeader.tsx .../InboxPopover.tsx
7b0783232 fix(app-shell): the bell polls the inbox on every console surface … (#4110) (#4199)
297534b78 fix(i18n): fail on an inline defaultValue that contradicts its en value (#3810) (#4119)
8c60819a6 fix(app-shell): surface the bell badge's … breakdown in the inbox popover (#7233) (#4073)

So: #4230 is #4110, observed on a build that predates #4110's fix. Not a regression, and #4199 did not regress — it was simply absent. Every corroborating detail in the report fits the isApp gate exactly: the bell empty under both filters, the home To-do card (useHomeInbox, never gated) listing the same rows, and zero /api/v1/* requests on open (a gated poll issues none, and post-#4199 a quiet open is expected anyway, per #4225).

The symptom does not reproduce at origin/main (436681e72): #4199's un-gated poll is intact at AppHeader.tsx:314, and #4197 / PR #4223's sharedUserFeeds.ts refactor — the change the dispatch flagged as the regression risk on this data path — deliberately left the inbox read alone (it shared only approvals and sys_activity), so it could not have re-broken it.

What this PR actually adds, and why it is still worth landing

A hole in #4199's own pin. Every case in AppHeader.inboxVariant.test.tsx holds one row and never touches the popover's Unread/All sub-filter. But the symptom both cards report is a pair of readings — "You're all caught up" under Unread and "No notifications" under All — and All is the load-bearing half, because it applies no predicate at all:

  • a panel empty under All has been handed nothing, full stop;
  • a panel empty only under Unread may simply have no unread rows, which is correct behaviour.

Nothing in the file could tell those apart. A recurrence that left the poll running but dropped rows between the poll and the popover — the other branch #4110's investigation named, and the one #4230 left explicitly open — would have found every existing case green.

Eight new cases drive the QA payload through the real AppHeader + InboxPopover pair: ten sys_inbox_message rows for one user, newest-first, five already read, including the approval.reminder #4230 names. They assert the unread cut under Unread, all ten titles under All, the round trip back to Unread, that the repeated-topic fold shows x2 unread / x3 under All rather than vanishing (the #2765 grouping is the one transform between poll and list), and that the badge reconciles with the rows on show. One case pins the distinction neither #4110 nor #4230 could make from outside: with every row read, Unread is correctly empty while All must still hold ten.

Oracle boundary, stated rather than implied: this mounts the real components over a fake adapter. It pins everything from dataSource.find to the rendered row — the whole span both cards localise the defect to — and it does not pin the adapter, the REST layer, or /api/v1/notifications itself. No live console or backend was driven in this round.

Reverse verification

Prediction, stated before running: restoring the isApp gate on the poll turns every variant="home" / "orgs" case red and leaves variant="app" green — #4110's exact signature. That is what happened.

$ git diff   # temporary, reverted
-    if (!dataSource || !user?.id) return;
+    if (!dataSource || !isApp || !user?.id) return;

Tests  17 failed | 13 passed (30)

All eight new cases red; the variant="app" control cases green throughout:

FAIL … (#4230) > lists the unread rows under Unread — the default filter
FAIL … (#4230) > lists ALL ten rows under All — the filter that applies no predicate
FAIL … (#4230) > surfaces the `approval.reminder` row #4230 names, on the surface it names
FAIL … (#4230) > separates "everything is read" from "the panel got nothing"
FAIL … (#4230) > coalesces the repeated digest instead of dropping it (#2765 ∩ #4230)
FAIL … (#4230) > reconciles the badge with the rows on show — 4 unread topics, 0 approvals
FAIL … (#4230) > holds on every console surface, not only inside an app
FAIL … (#4230) > returns to the unread cut when the user switches back

TestingLibraryElementError: Unable to find an element with the text:
Approval reminder: INV-1008.

Taken out with git checkout --, never git stash.

#4235 — one fix or two? Two.

#4230 asked whether the home-header panel (#4235) shares this root cause. It does not, and the reasoning is now sharper than when #4235 was filed:

Sequencing, per #4225: whoever extends hooks/sharedUserFeeds.ts with an inbox feed will be editing this exact path, and the eight cases added here are what will tell them if they re-break the bell.

Verification

  • npx vitest run packages/app-shell/src/layout/__tests__/AppHeader.inboxVariant.test.tsx30 passed (22 pre-existing, 8 new)
  • npx vitest run packages/app-shell (repo-root vitest per pnpm --filter @object-ui/app-shell test 跑的是 @object-ui/console 的 22 个文件,app-shell 自己的 276 个一个没跑,却报绿 #3378) — 344 files, 3294 passed, 1 skipped
  • pnpm --filter @object-ui/app-shell type-check — green (both tsc passes: tsc --noEmit and tsc -p tsconfig.typetests.json), after building the dependency closure --filter '@object-ui/app-shell^...' build
  • pnpm --filter @object-ui/app-shell lint0 errors (2286 pre-existing warnings, baseline unchanged)
  • check-control-bytes, check-changeset-presence, check-changeset-no-major, check-changeset-fixed, check-i18n-call-site-keys, check-i18n-en-drift — all green

Changeset: empty frontmatter (the gate's documented exemption). Tests and comments only, no source change, so nothing to release.

Refs #4110, #4156, #4199, #4225, #4235, objectstack#7517.


Generated by Claude Code

#4230 reported the console bell dead again and filed it as a regression of
#4110 / PR #4199. It is not a regression: the QA run vendored console
09987b6 (2026-08-09 02:35:56Z) and #4199 landed as 7b07832 (2026-08-10
20:39:10Z), 42 hours later. `git merge-base --is-ancestor 7b07832 09987b6`
exits non-zero, and that console's AppHeader.tsx line 338 still reads
`if (!dataSource || !isApp || !user?.id) return;` — the exact gate #4199
removed. No production code needed changing.

What the round did expose is a hole in #4199's own pin: every case in
AppHeader.inboxVariant.test.tsx held ONE row and never touched the popover's
Unread/All sub-filter, so nothing could tell "Unread is empty because every
row is read" apart from "the panel was handed nothing" — and the reading
under All is the load-bearing half of the reported symptom, because All
applies no predicate at all.

Adds eight cases driving the QA payload (ten sys_inbox_message rows, mixed
read-state, including the approval.reminder #4230 names) through the real
AppHeader + InboxPopover pair under BOTH filters, plus the grouping fold
(a repeated topic shows x2 unread / x3 all rather than vanishing) and the
badge reconciliation. Reverse-verified: restoring the isApp gate turns all
eight red on home/orgs and leaves variant="app" green — #4110's signature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@github-actions github-actions Bot added the tests label Aug 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.9 KB 350 KB
Entry file index-gOolFs_6.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.88KB 3.25KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 488.62KB 108.26KB
core (index.js) 3.04KB 1.15KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 150.04KB 39.79KB
fields (index.js) 228.45KB 56.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 16.38KB 5.47KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.98KB 10.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 61.73KB 17.54KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 118.79KB 30.79KB
plugin-designer (index.js) 210.91KB 42.67KB
plugin-detail (index.js) 238.88KB 59.71KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 187.97KB 49.90KB
plugin-kanban (index.js) 48.60KB 13.41KB
plugin-list (index.js) 109.18KB 26.48KB
plugin-map (index.js) 17.00KB 5.32KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.60KB 10.58KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.71KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 11, 2026 11:17
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit a43ff87 Aug 11, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4230-bell-panel-regression branch August 11, 2026 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants