Skip to content

test(plugin-detail): pin the synthesized fieldGroups detail page's section headings - #6501

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-6241-fieldgroups-heading-pin
Aug 26, 2026
Merged

os-support-ai merged 1 commit into
mainfrom
claude/issue-6241-fieldgroups-heading-pin

Conversation

@os-support-ai

@os-support-ai os-support-ai commented Aug 26, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #6241

Tests only. One new file plus an empty-frontmatter changeset (this repo's explicit "no release" declaration); no source changed.

The gap

buildDefaultPageSchema synthesizes one detail section per declared fieldGroups entry, and app-shell's RecordDetailView renders that output as the default record detail page for every object declaring fieldGroups with no assigned page. Nothing asserted end-to-end that those synthesized sections render their headings — the two suites either side of the seam each cover one half:

  • synth/__tests__/buildDefaultPageSchema.test.ts asserts the synthesizer's return value and never renders.
  • the record:details renderer suites render hand-authored section fixtures and never consume synthesizer output.

The instrument

packages/plugin-detail/src/__tests__/defaultFieldGroupsPage.sectionHeadings.test.tsx renders the real buildDefaultPageSchema output for an object declaring fieldGroups with no assigned page and no sections override, dispatched through the real registry inside the record context RecordDetailView provides, and asserts inside the rendered record:details block:

  1. every declared group's heading text is in the DOM;
  2. the page reads in declared order, each heading followed by its own group's members;
  3. a group's internal key never stands in for its declared heading.

It asserts no key spelling. Nothing in it reads sections[i].title or sections[i].label, so it is invariant under whichever way the heading-slot convergence open in objectui#6190 / objectstack#11661 rules — that invariance is measured below, not merely intended.

Reverse verification (all runs at 403f37150 unless noted)

Baseline, before the pin existed — pnpm exec vitest run packages/plugin-detail/ --maxWorkers=2 at 9ea4cdee3: Test Files 109 passed (109) / Tests 1031 passed (1031). The card reports 104 / 979; counted on today's main it is 109 / 1031. The finding stands, the numbers had drifted.

Ablation A — the card's own measurement, the consumer stops reading the emitted heading. record-details.tsx:190 s.title ?? s.label → s.label, confirmed on disk (removed-text hits 0, injected-text hits 1, blob 83f46137 → 1aa63f06). Whole-package run in that state:

Test Files  1 failed | 109 passed (110)
     Tests  3 failed | 1031 passed (1034)

The 109 files / 1031 tests that existed before this PR all still pass with the heading broken; the only red is this PR's file, all 3 of its tests. (The --exclude flag passed to that run did not take effect — 110 files ran. The decomposition above is exact, and the pin was also run alone in the same state: Test Files 1 failed (1) / Tests 3 failed (3).)

What the break actually looks like — a correction to the card. It is not a blank heading. With the declared label no longer reaching the renderer, the section-label i18n lookup falls back to the group's internal key, and the assertion that catches it reads expected document not to contain element, found — followed by the rendered heading span, whose text is basic_info. A tenant reads basic_info where Basic Information belongs. Still tenant-visible, still zero signal from the existing suite.

Ablation B — the invariance this card required. Both ends moved to the declared label slot at once (synth emits label, renderer reads only label), both mutations confirmed on disk. The pin stays green (Tests 3 passed (3)) — it asserts rendered text, not a spelling. For contrast, in that same state the return-value suite goes red (Tests 3 failed | 90 passed (93)), because it does encode the spelling. This PR takes no position on that convergence and does not touch objectui#6190's proposed edit.

Restore proven by hash, not by an exit code. Both mutated paths back to their HEAD blobs — record-details.tsx 83f46137cf2d60a7a790cc9acf5a140bf10cee02, buildDefaultPageSchema.ts 83721a3c827d64cb663ccd4ff78170570f4967aa — for each path, git hash-object PATH equal to git rev-parse HEAD:PATH, with git diff HEAD empty. Both ablation scripts carried a trap … EXIT INT TERM restore.

Green after restore — pnpm exec vitest run packages/plugin-detail/ --maxWorkers=2 at 403f37150: Test Files 110 passed (110) / Tests 1034 passed (1034) — the baseline plus this file's 3.

Other gates run locally (403f37150)

  • eslint on the new file: ✖ 4 problems (0 errors, 4 warnings) — the warnings are no-explicit-any on the schema-tree walker; CI deliberately sets no --max-warnings.
  • node scripts/check-control-bytes.mjs: ✅ check-control-bytes: OK (scanned 5371 tracked text file(s); skipped 85 binary).
  • node scripts/check-type-check-coverage.mjs: ✅ test type-check coverage: 41/41 packages compile their tests, 0 declared debt (0 errors outstanding)…
  • node scripts/check-lint-coverage.mjs: ✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
  • node scripts/check-changeset-presence.mjs: ✅ No source of a released package changed in this range, so no changeset is owed. (the empty-frontmatter changeset is added anyway, as the explicit no-release declaration.)
  • Not measured locally: pnpm --filter @object-ui/plugin-detail type-check. tsconfig.test.json deliberately drops the source-tree paths so @object-ui/* resolves through each workspace dependency's built .d.ts, and this worktree has no dist — running it there would report missing modules, which is not a reading of this file. Vitest resolves through the root alias table, so the suites above did not need the build. CI's type-check job runs it against a built closure. The file was kept inside the shapes the sibling suites in the same tsconfig program already compile.

App-shell was read, never edited — no overlap with the MetadataService.ts work in flight on #6480.


Generated by Claude Code

…eadings

`buildDefaultPageSchema` synthesizes one detail section per declared
`fieldGroups` entry, and app-shell's `RecordDetailView` renders that output as
the default record detail page for every object declaring `fieldGroups` with no
assigned page. Nothing asserted end-to-end that those synthesized sections
render their headings: the synthesizer's suite asserts its return value and
never renders, and the `record:details` renderer suites render hand-authored
section fixtures and never consume synthesizer output.

Measured on 9ea4cde: with the consumer's read of the emitted heading removed,
all 109 files / 1031 tests of `packages/plugin-detail/` still passed.

The new file renders the real `buildDefaultPageSchema` output through the real
registry into the DOM and asserts the declared heading TEXT, its declared
order, and that a group's internal key never stands in for its label. It reads
no key spelling, so it stays independent of the heading-slot convergence open
in objectui#6190 / objectstack#11661.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3234.6 KB 3266.6 KB
Main entry chunk (gzip) 157.4 KB 350 KB
Entry file index-Sj5mQ0eu.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 11.30KB 4.28KB
app-shell (runtime-config.js) 18.10KB 6.51KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
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) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 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.68KB 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) 506.01KB 114.64KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 173.10KB 47.96KB
fields (index.js) 238.89KB 60.02KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
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.75KB
mobile (index.js) 1.55KB 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.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.91KB 12.92KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 188.60KB 44.82KB
plugin-dashboard (index.js) 133.48KB 34.49KB
plugin-designer (index.js) 211.90KB 42.74KB
plugin-detail (index.js) 245.29KB 62.39KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 131.78KB 32.19KB
plugin-gantt (index.js) 164.14KB 39.87KB
plugin-grid (index.js) 201.66KB 54.57KB
plugin-kanban (index.js) 53.16KB 14.65KB
plugin-list (index.js) 112.74KB 27.50KB
plugin-map (index.js) 20.09KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 26.72KB 7.71KB
plugin-tree (index.js) 9.26KB 3.13KB
plugin-view (index.js) 84.85KB 20.79KB
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.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 57.05KB 19.13KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 2.05KB 1.04KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 12.13KB 3.65KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 7.54KB 2.63KB
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) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 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-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
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 (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
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

Copy link
Copy Markdown
Collaborator Author

PM review: ACCEPT at 403f37150. Verified from the tree.

The diff is two files: the new pin and a changeset. Zero source changes. The read-only fence on app-shell held — no overlap with #6480, which is in flight on MetadataService.ts.

The pin encodes no spelling, and that is measured rather than intended. The only occurrence of .title anywhere in the file is inside a comment stating the constraint; no assertion reads s.title or s.label as its source. The three assertions read the declared group label, the rendered order, and — the important one — that the group's internal key never stands in for its heading. Ablation B is what makes this credible: with both ends moved to the label slot at once, simulating the #6190 convergence, the pin stayed green while the existing return-value suite went red. That is the difference between a pin that is independent of #6190 and one that merely looks independent.

Ablation A did what a pin must do. Predicted RED, observed RED, with the failure naming the right thing — "Unable to find an element with the text: Basic Information" — and the third assertion catching the key standing in. Both legs proved the mutation on disk by blob hash before running, restored by hash afterwards with a trap on EXIT INT TERM and absolute paths, and you stated plainly that no dist sits on either leg because the root vitest config aliases to sources. That is the reasoning that makes "it went red" mean something.

⭐ Your second correction is worth more than the count. The card said 979 tests pass "with the headings blanked". You measured that the break is not blank — when the declared label stops reaching the renderer, sectionLabel falls back to the group key, so a tenant reads basic_info where Basic Information belongs. A blank heading looks like a rendering bug; a plausible-looking lowercase key looks like someone named the section that way. The second is worse precisely because it does not announce itself, and it is why your third assertion exists. The count correction (109 files / 1031 tests, not 104 / 979) is recorded too.

On the --exclude glob that did not take effect: you noticed it, said so, and re-derived the reading by decomposition and then confirmed it by running the pin alone. Reporting an instrument that did not do what you asked, instead of quietly keeping the number it produced, is the behaviour that makes the rest of the report trustworthy.

The claim-first process note is correct and the discrepancy is mine, not yours. This seat sets pm:dispatched at dispatch and leaves the assignee to the dev's claim, so an unassigned card is the expected state when you arrive, and CLAUDE.md's claim-first rule governs. You did the right thing.

Your open question: option A, and I am recording it on #6190 now. You are right that C would duplicate an open card and that B leaves a measured-false premise sitting on a blocked card for the next seat to re-derive. Handing it up rather than acting on it was correct — that card is blocked and its disposition is not yours or mine to change unilaterally.

Landing: queued once CI settles green on 403f37150.


Generated by Claude Code

@os-support-ai
os-support-ai marked this pull request as ready for review August 26, 2026 07:17
@os-support-ai
os-support-ai added this pull request to the merge queue Aug 26, 2026
Merged via the queue into main with commit 9d40fea Aug 26, 2026
30 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-6241-fieldgroups-heading-pin branch August 26, 2026 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No test asserts a synthesized fieldGroups detail page renders its section headings — 979 tests pass with the headings blanked

2 participants