Skip to content

test(ui): assert what ui needs from spec-owned shapes, not their exact counts (#26238) - #26272

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-l7vqkg
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-l7vqkg

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #26238

Clause-②: no
Metadata-protocol: none

What changed

Test-only. 38 ui/** test files pinned an exact count, or the whole value list or key list, of a schema, enum or constant that @objectstack/spec owns. Each of those pins is now a membership assertion on what ui actually reads, writes or must refuse. Retirement guards name the retired key as absent and the successor as present. The two ObjectSchema corpus guards in app-shell also assert directly that no accepted key is within two edits of a retired or resolved key. That check replaces the key count, which unrelated spec adoptions had moved six times (42 → 48). Exact pins on ui-owned shapes are unchanged. No product source, no packages/spec, no locale pack.

An additive spec change (a new key, enum value or constant member) now leaves these 38 files green, unless ui really lacks something. The kept parity guards listed below are the cases where ui does lack something.

Sweep: patterns and control hits

Scanned at merge-base df8aac41f: 1273 ui/** test files that name @objectstack/spec, through static imports, await import, vi.importActual and require.

# Pattern Control hit (at df8aac41f)
P1 Object.keys(X.shape) or a derived variable, then .size / .length toBe(N) / toHaveLength(N) app-shell/src/services/MetadataService.retiredObjectEnabled.test.ts:161 (accept.size).toBe(47))
P2 .options / .def.entries of a spec enum, then toEqual([literal…]) app-shell/src/utils/__tests__/recordFormModalSize-12803.test.ts:34
P3 Object.keys(X.shape).sort() then toEqual([literal…]) app-shell/src/views/ObjectView.kanbanLane-8193.test.tsx:227
P4 any literal 5+ count (toHaveLength(N), .size/.length).toBe(N)) in a spec-reading file, triaged by hand (129 hits) plugin-designer/src/MetadataFieldsPage.storedTypePreserved.test.tsx:228 (toHaveLength(49), FieldType)
P5 a spec constant array (not a schema) compared with toEqual([literal…]) or a count (a broad pass over every spec identifier, 228 hits, triaged by hand) app-shell/src/services/MetadataService.readDecorationStrip.test.ts:129 (METADATA_READ_DECORATIONS)
P6 toMatchSnapshot / toMatchInlineSnapshot in a spec-reading file 0 hits. The pattern's control: components/src/__tests__/snapshot-critical.test.tsx, which is not spec-reading

The scanner was a scratch script. It recognised static named and namespace imports, destructured dynamic imports, and one level of derived variables (multi-line declarations too). Pins reached only through @object-ui/types re-exports of spec values are outside it. The ones met while triaging (ViewTypeSchema, TextSchema, VIEW_ITEM_KEYS) were read by hand.

Converted (38 files)

Paths are relative to ui/packages/ unless they start with ui/.

  • app-shell/.../MetadataService.retiredObjectEnabled.test.ts: accept.size 47 removed. Added: no near-spelling, within two edits, of enabled or _deleted (except enable, which the next lines refuse by value). The six-entry re-point history is condensed to one line.
  • app-shell/.../MetadataService.specKeyObjectPayload.test.ts: accept.size 47 removed. Added: no near-spelling of group, sortOrder or relationships. History condensed, and the header's "43 keys" dropped.
  • app-shell/.../recordFormModalSize-12803.test.ts: the whole modalSize enum → each tested size is an allowed value.
  • app-shell/.../ObjectView.kanbanLane-8193.test.tsx: the 4-key KanbanConfig list → groupByField present; groupField and groupBy absent.
  • app-shell/.../plugin-runtime-tier-3846.test.tsx: the literal tier list → membership. The derived RUNTIME_FALLBACK equality stays (see Kept).
  • app-shell/.../packageSource-12790.test.ts: union branch count 2 → a non-empty floor.
  • app-shell/.../ActionPreview.locations.test.tsx: ACTION_LOCATIONS.length 6 → a floor. global_nav stays absent.
  • app-shell/.../MetadataService.readDecorationStrip.test.ts, data-objectstack/src/draft-envelope.test.ts: the decoration list → contains _diagnostics and _draft.
  • app-shell/.../FlowReferenceField.specDerivation.test.ts: the spec source's 3-key list → contains object, source and valueField, and none of the presentation keys this package owns.
  • core/.../normalize-list-view.foldOutputAuthorable-5435.test.ts: count 11 and the key list → the seven toggles the fold emits are declared.
  • core/.../normalize-list-view.pageResidual-8429.test.ts: counts 9/11/9/11 and both literal lists → the nine drawn kinds present; page absent; the objectui face is the spec's plus two; the census uses toMatchObject plus the derived "every spec kind resolves to itself".
  • core/.../filter-tokens.recordContext-7297.test.ts: ['record_id'] → contains. The compile-time ratchet stays in the type check.
  • core/.../filter-view-rule-arity-8557.test.ts: the literal array and scalar class lists → membership. The exact partition stays.
  • data-objectstack/src/listViews.roundTripKeys-11013.test.ts: the 5-key list → contains each.
  • data-objectstack/src/metadata-client.overlayScope.test.ts: ['env'] → contains. The old spellings are still refused by name.
  • plugin-dashboard and plugin-report spec-symbol-dataset-totals-11073.test.ts: ['groupings'] → contains.
  • plugin-designer/src/MetadataFieldsPage.storedTypePreserved.test.tsx: FieldType 49, census 22 and checked === 22 → the census holds every measured member, and checked === CENSUS.length. DESIGNER_TYPES 27 (ui-owned) stays.
  • plugin-detail/.../recordActivityFeed.test.ts: declared 13 and the unproduced list → membership plus an exact partition. The produced set (ui-owned) stays exact.
  • plugin-list/.../UserFilters.tabPresetOperators.test.tsx: VIEW_FILTER_OPERATORS.length 20 → a floor plus icontains.
  • plugin-tree/src/ObjectTree.schemaTyped-8655.test.ts, types/.../calendar-flat-color-allday-8466.test.ts, types/.../related-list-add-picker-filter-mirror-9964.test.ts: block key lists → the keys this code reads.
  • sdui-parser/.../dashboard-widget-options-census.test.ts: the 5-key list → contains each. The derived consumed/declared parity stays (see Kept).
  • auth/.../auth-spec-parity.test.ts: the 6-provider list → contains github, google and local.
  • fields/.../LocationField.optionalKeys.test.tsx: the 4-key list → lat, lng and altitude present; latitude and longitude absent.
  • fields/.../RegistryRefField.audienceAnchors-12200.test.tsx: the anchor list → contains everyone and guest.
  • ui/apps/console/.../component-input-union-specimens.test.ts: the literal accepted-arm lists → arrayContaining. defaultValue keeps its no-object-arm claim.
  • types/.../export-options-spec-parity.test.ts, types/.../object-grid-export-options-refusal-7762.test.ts: union arms 2 and the key count or list → a floor plus named keys.
  • types/.../grid-kanban-calendar-members-round8-6152.test.ts: the operations key list → named members. The local face is derived as the row's keys plus the two tombstones.
  • types/.../list-view-options-bag-6152.test.ts: the literal KINDS list → contains this package's block kinds.
  • types/.../record-highlights-fields-icon-9280.test.ts, types/.../record-highlights-layout-9187.test.ts: the entry, layout and props key lists → named members, with icon absent.
  • types/.../select-option-form-view-boundary-12736.test.ts, types/.../select-option-spec-extension-7014.test.ts: the SelectOption key lists → named members, with icon and disabled absent.
  • types/.../object-view-unmirrored-keys-7779.test.ts: the 51-key list, count 51, the retired list and live count 43 → membership and a floor.

Kept, with the reason

  • Derived parity, both sides computed (ui set vs spec set). Red means ui really lacks a mapping, label, declaration or consumer for a new spec member, and the fix is ui source, not the test. These are: RUNTIME_FALLBACK = tiers; PACKAGE_SOURCES = sources; the pageResidual "no undrawable spec kind"; the UserFilters "every view operator is AST-valid"; the sdui-parser consumed ⊇ declared; export-options LOCAL keys = spec keys; object-view-unmirrored-keys "narrower than the protocol is empty" (the maintainer principle stated in that file); report-chart-query-spec-parity:614; navigation-spec-parity:316; bulk-action-spec-parity:46; filter-operator-protocol-set-9559:105; mirror-groups-cd-10286:67; ObjectValidationsPanel.runsOn-11923:134; ObjectMetricWidget.countNotCurrency-10356:114; imported-defaults-8317:637. tooling: the root tsconfig.json is both the shared base for 72 packages and the scripts/ program (the one DEBT entry, 26 errors); 14 packages re-set module resolution three ways; 18 vitest configs are byte-identical; three debt ledgers are empty #26092 is the half that makes these visible on the spec PR itself.
  • ui-owned shapes: DESIGNER_TYPES 27; produced feed kinds; OBJECTUI local categories detail/list; the metadata-form-i18n header children (the ui bundle's rows); the i18n locale and key counts; fixture and row counts (plugin-grid, plugin-map, deriveMasterDetail, …); offline-nav-performance literal arrays (typed against spec types, so tsc already checks membership); the test-support spec-tombstones fixture schema; the object-view-unmirrored-keys buckets over ui's own declared and unread members; object-view-slot-key-lists (ui's d.ts members).
  • Required-set pins (list-view-blocks-by-reference-6152:122-125): an additive optional key cannot move a required set. Adding a required member is a narrowing, so it is breaking, not additive.
  • Behavioural refusals and parse results (for example unrecognized_keys naming a key, a refusal path, a parsed value): these already assert one named key or value.

Proof: a scratch extra member in the spec, old assertion vs new

25 scratch members were added in one pass to the spec source: keys zzScratchKey and values zz_scratch_*, across ObjectSchema, FormView.modalSize, KanbanConfig, UserActionsConfig, VIEW_FILTER_OPERATORS, TreeConfig, CalendarConfig, ObjectListView, the ListView type enum, ACTION_LOCATIONS, PluginRuntime, METADATA_READ_DECORATIONS, APPROVER_VALUE_SOURCES, RECORD_CONTEXT_TOKENS, DatasetTotals, FieldType, SelectOption, FeedItemType, DashboardWidgetOptions, AuthProvider, AUDIENCE_ANCHOR_POSITIONS, overlayScope, RecordHighlights (key and layout value), the export-options object branch and the location value. Then a full @objectstack/spec build, the 38 files at this branch, the same 38 files at df8aac41f (written to the tree only), and a restore. After the restore, git diff HEAD was empty on the spec, the rebuilt dist had 0 markers, and the ui tests matched HEAD.

  • 24 files: old red at the converted line, new green. These are readDecorationStrip, retiredObjectEnabled (49 vs 47), specKeyObjectPayload (49 vs 47), recordFormModalSize, kanbanLane, FlowReferenceField, ActionPreview (7 vs 6), auth-spec-parity, filter-tokens, filter-view-rule-arity, foldOutputAuthorable (12 vs 11), draft-envelope, overlayScope, LocationField, audienceAnchors, both dataset-totals, MetadataFieldsPage (50 vs 49, 23 vs 22), recordActivityFeed (14 vs 13), ObjectTree, calendar-flat-color, object-grid-export-options-refusal, record-highlights-layout and select-option-spec-extension.
  • 6 files: old red at the converted line, and new red ONLY at a kept derived-parity line. These are plugin-runtime-tier (no label for the new tier), pageResidual (ListView cannot draw the new kind), UserFilters (the new operator is not in VALID_AST_OPERATORS: the scratch added it to one spec list only), sdui-parser (DatasetWidget does not consume the new key), export-options-spec-parity (the TS mirror lacks the new key) and object-view-unmirrored-keys (objectui does not declare the new live key). Every converted line in these files was green.
  • 8 files: NOT MEASURED by this mutation set. The members they read were not mutated: union arm counts, list-view slot kinds, the highlights entry arm, the picker block, grid operations, VIEW_ITEM_KEYS, the form-view option, and element arms. Their conversions use the same membership shape.

Verification

At head cf00e778 (after merging origin/main with #26231 in), with @objectstack/{spec,client,formula,lint} closures rebuilt, all under scripts/pm/os-verify-lock.sh:

  • The 38 changed files: Test Files 38 passed (38) · Tests 891 passed (891), VERDICT command-exit 0.
  • Before the merge, at 402e830c, the full suites of the 12 touched packages: auth core data-objectstack fields plugin-dashboard plugin-designer plugin-detail plugin-list plugin-report plugin-tree sdui-parser types. Result: Test Files 1790 passed | 2 skipped (1792) · Tests 28011 passed | 65 skipped, VERDICT command-exit 0, 43m51s.
  • Declared narrowing: app-shell (about 1725 files) and ui/apps/console ran only their changed files. Their full suites are left to CI Test Core / Test UI.
  • Dispatch gates on the final head: node scripts/pm/dispatch-gates.mjs --commands derived 24 families, all 24 run, each exit 0 (check:type-check-debt under the lock: VERDICT command-exit 0, 26 raw tsc errors, none above its recorded number). --ran reconciliation: 24 derived, 24 run, 0 NOT-MEASURED, 0 UNRUN.

Acceptance notes

  • Not swept here, a different gate: type-level literal-union pins. At df8aac41f there are 51 Equal type assertions against a literal union of names in 31 spec-reading ui test files, for example plugin-chatbot/src/__tests__/spec-symbol-batch6.test.ts:251 (PendingActionStatus). Some are on ui-owned types. Those on spec-derived types red Type Check UI, not Test UI, when the spec adds a member. Converting them needs a per-type ruling (exact type equality vs extends), so they are noted for the seat to card. They are not carried by this PR.
  • The arity pin in filter-view-rule-arity-8557 was a deliberate review trigger: a new spec operator lands in the value-requiring class by default. That default is now held only by the exact partition, not by a whole-vocabulary list.
  • Changeset: none. The diff is test files only, and each touched package's files[] ships dist alone, so nothing publishes. Labelled skip-changeset at open.

Generated by Claude Code

…-l7vqkg

# Conflicts:
#	ui/packages/app-shell/src/services/MetadataService.retiredObjectEnabled.test.ts
#	ui/packages/app-shell/src/services/MetadataService.specKeyObjectPayload.test.ts
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 26, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT · seat 2 · 2026-10-26T18:12Z · PR #26272 head cf00e778 (#26238, the count-pin sweep)

check reading
scope 38 files, +256/−217, every one a *.test.ts(x) under ui/; no product source, no scripts/sdui-manifest.sources/*, no locale pack, no ui/.github/**, no ci.yml
the two guards MetadataService.retiredObjectEnabled / .specKeyObjectPayload: accept.size removed; the named membership facts stay (enable accepted, enabled / _deleted and six synonyms refused) and a no-near-spelling check (edit distance ≤ 2 over the accept set) replaces what the count stood in for
proof 25 scratch spec members: 24 files old red → new green; 6 still red only at a kept derived-parity line (each a real ui gap); 8 not reached; restore left 0 spec diff
tests 38 files / 891 tests at head; 12 touched packages' full suites 28011 green; app-shell and console narrowed to changed files, declared, rest left to CI
gates 24 derived, 24 run, all exit 0
governance NOT governed, 473 lines ≤ 3000
landing git merge-tree against origin/main clean; test-only, so it joins no sdui package line

Ruling on the open question: keep the derived ui-vs-spec parity guards red on an additive spec change. Their red names a ui gap someone must close (a missing label, consumer or declaration), which is the opposite of a brittle count; #26092 makes them run on the spec PR itself, so the spec author sees them before main does.

Out-of-scope finding (51 type-level Equal literal-union pins in 31 files, red on Type Check UI on an additive spec change): filed as its own card, linked below once created.

Seat's route: ready + auto-merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants