Skip to content

test(app-shell): pin the @anon metadata seed scope enumeration - #6103

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5828-anon-seed-scope-pin
Aug 24, 2026
Merged

test(app-shell): pin the @anon metadata seed scope enumeration#6103
yinlianghui merged 1 commit into
mainfrom
claude/issue-5828-anon-seed-scope-pin

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Part of #5828

Test-only. Recovers the objectui#5746 measurement harness from the unmerged
branch claude/issue-5746-anon-seed-scope-measurement and lands it as a
regression pin. No provider behaviour moves; no published surface widens.

#5828 remains open — this PR is not its answer. The card's deciding
question is server-side and still unowned: on a stub-auth or marketplace-preview deployment, is /meta/*
READ permission-filtered per user? objectstack#11373 measured that anonymous
/meta/*WRITES are refused (401, nothing persisted) on a platform-default
boot — a different door on a different boot, so it does not settle this.

Why this pin exists

objectui#5746's load-bearing correction: objectui#5744's
purgePreviousUserClientState does not cover the seed read. React runs child
effects before parent effects, so MetadataProvider's seed read precedes
AuthProvider's purge. Verified in the tree on this branch —
MetadataProvider.tsx reads the seed and only then calls
dropForeignPrincipalEntries():

const cached = previewDrafts ? null : loadFromSession('app');
dropForeignPrincipalEntries();

That leaves objectui#5198's principal-scoped cache key as the sole protection
on that boot, and it was unpinned in the tree until now.

What the harness is, and what was preserved

It mounts the real console boot path — real AuthProvider, real
ConnectedShell#4042 session gate, real MetadataProvider — with only the auth
server (an AuthClient double) and the metadata adapter doubled, and nothing
writing objectui:metadata:* by hand
, so it cannot agree with a key format the
provider does not produce. Nothing further was stubbed to stabilise it.

Re-measured on current main before anything was changed

Every reading objectui#5746 recorded still reproduces (7c96c94):

scenarioreading
S1 window (a), stale token purgedwrites=1 anon-writes=0 — SHUT
S2 window (b), cookie-only, no tokenobjectui:metadata:app:org_a:@anon
S3 degenerate cross-principalB rendered setup,crm,hr-secret — HIT on A's key
S4 auth-disabled guest bootobjectui:metadata:app:@none:@anon
S5 previewMode bootobjectui:metadata:app:@none:@anon

S1–S5 now assert those readings instead of only reporting them into a file.
That deliberately includes the @anon seed the guest and preview boots write:
that write is the observation #5828 carries and is not "fixed" here, because
whether it is a defect depends on the unanswered server-side question above. A pin
asserting today's behaviour is what lets a future ruling be executed safely.

S6 — the new assertion, and the ablation that proves it can fail

S3 already showed what a principal-blind key does, but both of its principals
degenerate to @anon, so it cannot detect the loss of #5198. S6 is the same shape
with two distinct bearers in one tab and no sign-out in between. It asserts B's
seed read goes to B's own principal scope and MISSES A's entry, with a
counter-probe first that A really did leave a hr-secret-bearing seed behind.

Predicted direction, recorded before running — ablate principalScope() to
ignore the token while leaving the @anon branch intact, so only discrimination
among real principals is removed; predict S6 red alone, failing first at
expect(seedRead.principal).not.toBe(aPrincipal). The counter-hypothesis carried
over from objectstack#11373's "403, not 200" was that a second independent layer
(dropForeignPrincipalEntries, or #5744's purge) would answer instead and leave
S6 green — which would have meant the pin cannot fail.

Observed — matched the prediction; no second layer intervened:

 ✓ S0 ✓ S1 ✓ S2 ✓ S3 ✓ S4 ✓ S5
× S6 AssertionError: expected '08dtegw0taozhy' not to be '08dtegw0taozhy'
S6 VERDICT: B rendered apps="setup,crm,hr-secret"
seed-reads=["objectui:metadata:app:org_a:08dtegw0taozhy -> HIT len=54"]

Ablation ran under trap … EXIT INT TERM with a cwd-independent restore; the
mutation was proved on disk by grepping the injected text and the removed
text, and git diff HEAD is empty afterwards.

Both instrument defects re-measured, not inherited

Either would otherwise produce a false "no writes anywhere" green.

  • (a) jsdom hands out sessionStorage as a Proxy, so this === sessionStorage
    is never true inside a Storage.prototype method. Restoring that guard in the
    instrument drove S0's counter-probe red with expected 0 to be greater than 0
    — zero writes recorded. Still handled (store identified by key prefix, with
    localStorage asserted to hold no objectui:metadata:* key) and still commented.
  • (b) vi.restoreAllMocks() does not undo a reassigned property. Handled by
    the module-level appFetchNeverLands flag reset in beforeEach and by the
    explicit restoreInstrument?.() in afterEach. Deleting the beforeEach reset
    drove S4, S5 and S6 red (expected [] to deeply equal [ 'objectui:metadata:app:@none:@anon' ]),
    confirming the reset is load-bearing. Still commented.

One change the recovery required

The harness did not apply cleanly at the type level. Its boot() helper
restated the preview prop as { simulatedRole: string }, which is wider than
today's PreviewModeOptions['simulatedRole'] ('user' | 'admin' | 'viewer'), so
type-check failed with TS2322. Fixed contract-first — the helper now uses the
published PreviewModeOptions rather than a local widened restatement.

Gates, at c1dc883f8

gateexit
pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json)0
pnpm exec vitest run packages/app-shell/src/console/__tests__/anonSeedScope-5746.enumeration.test.tsx0 — Test Files 1 passed (1), Tests 7 passed (7)
pnpm exec eslint over the merge-base delta0
pnpm check:esm-specifiers (a step inside the Type Check job)0
pnpm check:self-import · pnpm check:phantom-deps0 · 0
node scripts/check-changeset-presence.mjs0 — empty frontmatter accepted as the explicit "releases nothing" declaration
node scripts/check-changeset-fixed.mjs · check-changeset-no-major.mjs0 · 0
node scripts/check-control-bytes.mjs0
node scripts/check-type-check-coverage.mjs · check-lint-coverage.mjs0 · 0

Whole-package vitest for app-shell was not run — it is ~784 s and holds the
container's shared verify lock. The full farm is CI's run.


Generated by Claude Code

Recovers objectui#5746's measurement harness and lands it as a regression pin.
Test-only: no provider behaviour moves.
The harness mounts the REAL console boot path — real `AuthProvider`, real
`ConnectedShell` #4042 session gate, real `MetadataProvider` — with only the
auth server and the metadata adapter doubled, and nothing writing
`objectui:metadata:*` by hand, so it cannot agree with a key format the
provider does not actually produce.
Re-measured on current `main` before anything was changed; every reading #5746
recorded still reproduces:
S1 window (a) writes=1 anon-writes=0 -> SHUT
S2 window (b) objectui:metadata:app:org_a:@anon
S3 degenerate B rendered "setup,crm,hr-secret" -> HIT on A's key
S4 guest objectui:metadata:app:@none:@anon
S5 preview objectui:metadata:app:@none:@anon
S1-S5 now ASSERT those readings instead of only reporting them, including the
`@anon` seed the guest and preview boots write. That write is the observation
#5828 carries and is deliberately NOT "fixed" here: whether it matters turns on
whether `/meta/*` READS are permission-filtered on a stub-auth or
marketplace-preview deployment, which nobody has measured. objectstack#11373
measured that anonymous /meta WRITES are refused (401) on a platform-default
boot — a different door on a different boot, so it does not settle it.
S6 is new and is the pin that can fail. #5746's load-bearing correction is that
#5744's `purgePreviousUserClientState` does NOT cover the seed read — React runs
child effects before parent effects, so `MetadataProvider`'s seed read precedes
`AuthProvider`'s purge — which leaves #5198's principal-scoped key as the sole
protection on that boot. S6 boots two DISTINCT bearers in one tab and asserts B
misses A's entry. Ablating `principalScope()` to ignore the token turns S6 red
alone (`expected '08dtegw0taozhy' not to be '08dtegw0taozhy'`, B renders
`setup,crm,hr-secret`) while S0-S5 stay green, which is the predicted direction.
Both instrument defects #5746 caught are preserved and re-measured. Restoring
the `this === sessionStorage` guard drives S0's counter-probe red with zero
recorded writes, confirming jsdom still hands out `sessionStorage` as a Proxy.
Part of #5828
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3221.0 KB3990.2 KB
Main entry chunk (gzip)153.7 KB350 KB
Entry fileindex-BwRdn9wR.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.15KB114.53KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)166.86KB46.08KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.44KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.08KB61.86KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.89KB14.59KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.57KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.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 24, 2026 17:19
@yinlianghui
yinlianghui added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit 4edcbe9Aug 24, 2026
23 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5828-anon-seed-scope-pin branch August 24, 2026 17:31
Sign up for freeto 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

@yinlianghui@claude