Skip to content

Commit b5d137a

Browse files
test(ui): assert what ui needs from spec-owned shapes, not their exact counts (#26238) (#26272)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9WS2SSi8hVuwcpHdWi5p4oQ)_ 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`. #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](https://claude.ai/code/session_01HRaa5qRyCQv9YZJCosen4d)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 366e4aa commit b5d137a

38 files changed

Lines changed: 256 additions & 217 deletions

File tree

‎ui/apps/console/src/__tests__/component-input-union-specimens.test.ts‎

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,10 @@ describe('objectui#4970 — element:text.content / element:button.label declare
310310
// these arms honest rather than copied from the issue — #4970's own table
311311
// was measured at the 17.0.0-rc.6 pin, and the answer had to be re-taken
312312
// before the declarations moved.
313-
expect(specAcceptedArms(ElementTextPropsSchema, 'content')).toEqual(['string', 'object']);
314-
expect(specAcceptedArms(ElementButtonPropsSchema, 'label')).toEqual(['string', 'object']);
313+
// Membership (#26238): the arms the declarations below must carry. An arm
314+
// the spec adds is caught by the derived comparisons, not by a literal.
315+
expect(specAcceptedArms(ElementTextPropsSchema, 'content')).toEqual(expect.arrayContaining(['string', 'object']));
316+
expect(specAcceptedArms(ElementButtonPropsSchema, 'label')).toEqual(expect.arrayContaining(['string', 'object']));
315317
});
316318

317319
it('`element:text.content` accepts the inline translation map', () => {
@@ -417,26 +419,20 @@ describe('objectui#5717 — element:text_input label / placeholder / description
417419
// Measured on the `@objectstack/spec` 17.1.0 pin — the same discipline the
418420
// #4970 block adopted, and the reason these arms are honest rather than
419421
// copied from the card that reported them.
420-
expect(specAcceptedArms(ElementTextInputPropsSchema, 'label')).toEqual(['string', 'object']);
421-
expect(specAcceptedArms(ElementTextInputPropsSchema, 'placeholder')).toEqual([
422-
'string',
423-
'object',
424-
]);
425-
expect(specAcceptedArms(ElementTextInputPropsSchema, 'description')).toEqual([
426-
'string',
427-
'object',
428-
]);
422+
// Membership (#26238), as in the #4970 block above.
423+
for (const key of ['label', 'placeholder', 'description']) {
424+
expect(specAcceptedArms(ElementTextInputPropsSchema, key), key).toEqual(expect.arrayContaining(['string', 'object']));
425+
}
429426
});
430427

431428
it('the sibling key whose contract has NO object arm is measured the same way', () => {
432429
// The separation asserted from the CONTRACT rather than from the
433430
// declaration, so "why is defaultValue different" is answered by evidence
434431
// in the same vocabulary. Without this line the per-key control below reads
435432
// as a convention; with it, it reads as the contract.
436-
expect(specAcceptedArms(ElementTextInputPropsSchema, 'defaultValue')).toEqual([
437-
'string',
438-
'number',
439-
]);
433+
const defaultValueArms = specAcceptedArms(ElementTextInputPropsSchema, 'defaultValue');
434+
expect(defaultValueArms).toEqual(expect.arrayContaining(['string', 'number']));
435+
expect(defaultValueArms).not.toContain('object');
440436
});
441437

442438
it('`element:text_input.label` accepts the inline translation map', () => {

‎ui/packages/app-shell/src/console/marketplace/__tests__/plugin-runtime-tier-3846.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ describe('objectui#3846 — the trust-tier label map answers to the spec enum',
5757
it('covers exactly the tiers PluginRuntimeSchema declares', () => {
5858
// The producer's own list, read from the schema rather than retyped.
5959
const declared = [...PluginRuntimeSchema.options].sort();
60-
expect(declared).toEqual(['node', 'sandbox', 'worker']);
60+
for (const tier of ['node', 'sandbox', 'worker']) expect(declared).toContain(tier);
6161
expect(Object.keys(RUNTIME_FALLBACK).sort()).toEqual(declared);
6262
});
6363

‎ui/packages/app-shell/src/services/MetadataService.readDecorationStrip.test.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,10 +123,11 @@ const DECORATED_SERVED = {
123123
// ---------------------------------------------------------------------------
124124

125125
describe('the instrument', () => {
126-
it('is the spec that names the decorations, and it names exactly these two', () => {
126+
it('is the spec that names the decorations, and it names these two', () => {
127127
// Stated before any claim about the fix. If this list grows, the pins below
128-
// follow it without an edit — that is why they read it rather than spell it.
129-
expect([...METADATA_READ_DECORATIONS]).toEqual(['_diagnostics', '_draft']);
128+
// follow it without an edit — that is why they read it rather than spell it,
129+
// and why this asserts membership rather than the whole list (#26238).
130+
expect([...METADATA_READ_DECORATIONS]).toEqual(expect.arrayContaining(['_diagnostics', '_draft']));
130131
});
131132

132133
it('refuses each decoration BY NAME — the defect, on the schema', () => {

‎ui/packages/app-shell/src/services/MetadataService.retiredObjectEnabled.test.ts‎

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,26 @@ const unrecognizedKeys = (result: { success: boolean; error?: unknown }): string
101101
.filter((i) => i.code === 'unrecognized_keys')
102102
.flatMap((i) => i.keys ?? []));
103103

104+
/**
105+
* Levenshtein distance, case-insensitive. The near-spelling claims below are
106+
* stated over the accept set with this, rather than through a key count that
107+
* every unrelated spec adoption moves (#26238).
108+
*/
109+
function editDistance(a: string, b: string): number {
110+
const [x, y] = [a.toLowerCase(), b.toLowerCase()];
111+
const row = Array.from({ length: y.length + 1 }, (_, j) => j);
112+
for (let i = 1; i <= x.length; i++) {
113+
let diagonal = row[0];
114+
row[0] = i;
115+
for (let j = 1; j <= y.length; j++) {
116+
const above = row[j];
117+
row[j] = Math.min(above + 1, row[j - 1] + 1, diagonal + (x[i - 1] === y[j - 1] ? 0 : 1));
118+
diagonal = above;
119+
}
120+
}
121+
return row[y.length];
122+
}
123+
104124
/** A base document `ObjectSchema` accepts, for probing one key at a time. */
105125
const BASE = { name: 'account', label: 'Account', fields: { n: { type: 'text', label: 'N' } } };
106126

@@ -137,37 +157,22 @@ describe('objectui#6238 · the schema oracle — both tombstone keys are refused
137157
// key-name gate could never have stated it. `enable` passes on the name and
138158
// fails on the value, which is why this is objectui#4687's resolution
139159
// (delete the declaration) rather than objectui#6041's (rename it).
140-
// 43, not the 42 this pin carried against 17.2.0: `@objectstack/spec` 17.3.0
141-
// ADOPTED `editMode` (measured — gained set exactly `['editMode']`, lost set
142-
// empty). That adoption is unrelated to this file's subject and is pinned in
143-
// `packages/types/src/__tests__/object-schema-metadata-spec-derivation.test.ts`;
144-
// the count rides here only as the corpus guard for the membership claims
145-
// below, so it is re-pointed, not weakened.
146-
// 44 at `@objectstack/spec` 17.6.0, which adopted `imageField` (measured against
147-
// a 17.5.0 control: gained set exactly `['imageField']`, lost set empty;
148-
// objectui#11438). Re-pointed as the 17.3.0 adoption was, not weakened.
149-
// 45 at `@objectstack/spec` 18.0.0-next.0, which adopted `attachedOnRead`
150-
// (measured against a 17.7.0 control: gained set exactly `['attachedOnRead']`,
151-
// lost set empty; objectui#12030). Re-pointed the same way.
152-
// 46 on the objectstack `main` pin after 18.0.0-next.3, which adopted
153-
// `defaultRowScope` (objectstack#24398; measured against the next.3 pin: the
154-
// one top-level key `ObjectSchema` gains between the two commits, none lost;
155-
// objectui#12971). Re-pointed the same way.
156-
// 47 at `@objectstack/spec` 18.1.0, which adopted `membershipKeys`
157-
// (objectstack#25513; measured against the 915e9404 pin: the spec's own
158-
// `authorable-surface/data.json` gains exactly `data/Object:membershipKeys`
159-
// between the two commits, none lost; objectui#13440). Re-pointed the same way.
160-
// 48 on the objectstack `main` pin after objectstack#26005 (5e113b39), which adopted
161-
// `sharing` (measured against its parent fbb75998: gained set exactly `['sharing']`,
162-
// lost set empty; unchanged at 43b47156). Re-pointed the same way.
160+
// The membership claims are asserted by name over the accept set; an exact
161+
// key count used to stand in for them and was re-pointed six times by
162+
// unrelated spec adoptions (42 → 48), so it is gone (#26238).
163163
const accept = new Set(Object.keys(ObjectSchema.shape as Record<string, unknown>));
164-
expect(accept.size).toBe(48);
165164
expect(accept.has('enable')).toBe(true);
166165
expect(accept.has('enabled')).toBe(false);
167166
expect(accept.has('_deleted')).toBe(false);
168167
for (const key of ['disabled', 'active', 'isActive', 'deleted', 'isDeleted', 'softDeleted']) {
169168
expect(accept.has(key), `ObjectSchema unexpectedly accepts \`${key}\``).toBe(false);
170169
}
170+
// No near-spelling of either tombstone key is accepted, except `enable`,
171+
// whose VALUE is what refuses the rename (asserted next).
172+
const nearSpellings = [...accept].filter(
173+
(key) => key !== 'enable' && ['enabled', '_deleted'].some((retired) => editDistance(key, retired) <= 2),
174+
);
175+
expect(nearSpellings, 'ObjectSchema accepts a near-spelling of a tombstone key').toEqual([]);
171176

172177
const asBoolean = ObjectSchema.safeParse({ ...BASE, enable: false });
173178
expect(asBoolean.success).toBe(false);

‎ui/packages/app-shell/src/services/MetadataService.specKeyObjectPayload.test.ts‎

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,8 @@
1919
* shapes: `toObjectPayload` builds it and `saveObject` PUTs it whole to
2020
* `PUT /api/v1/meta/object/:name`.
2121
*
22-
* Measured against the installed `@objectstack/spec` (ESM build), whose
23-
* `ObjectSchema` accept set is 43 keys at 17.3.0 (42 at 17.2.0, when this file
24-
* was written; the one gained key is `editMode`, unrelated to the three below):
22+
* Measured against the installed `@objectstack/spec` (ESM build), at 17.2.0
23+
* when this file was written:
2524
*
2625
* ObjectSchema.safeParse({ ...base, group: 'Sales' }) => unrecognized_keys ["group"]
2726
* ObjectSchema.safeParse({ ...base, sortOrder: 3 }) => unrecognized_keys ["sortOrder"]
@@ -57,6 +56,26 @@ import { ObjectStackAdapter } from '@object-ui/data-objectstack';
5756
import type { ObjectDefinition } from '@object-ui/types';
5857
import { MetadataService } from './MetadataService';
5958

59+
/**
60+
* Levenshtein distance, case-insensitive. The no-near-spelling claim below is
61+
* stated over the accept set with this, rather than through a key count that
62+
* every unrelated spec adoption moves (#26238).
63+
*/
64+
function editDistance(a: string, b: string): number {
65+
const [x, y] = [a.toLowerCase(), b.toLowerCase()];
66+
const row = Array.from({ length: y.length + 1 }, (_, j) => j);
67+
for (let i = 1; i <= x.length; i++) {
68+
let diagonal = row[0];
69+
row[0] = i;
70+
for (let j = 1; j <= y.length; j++) {
71+
const above = row[j];
72+
row[j] = Math.min(above + 1, row[j - 1] + 1, diagonal + (x[i - 1] === y[j - 1] ? 0 : 1));
73+
diagonal = above;
74+
}
75+
}
76+
return row[y.length];
77+
}
78+
6079
/** The bodies of every PUT the SDK issued, exactly as they went over the wire. */
6180
function makeCapturingAdapter() {
6281
const puts: Array<Record<string, unknown>> = [];
@@ -153,36 +172,23 @@ describe('the instrument', () => {
153172
});
154173

155174
it('has no near-spelling for any of them — unlike objectui#6041, nothing here is a rename', () => {
156-
// 43 at `@objectstack/spec` 17.3.0, which adopted `editMode` (measured —
157-
// gained set exactly `['editMode']`, lost set empty). The count is this
158-
// file's corpus guard, not its subject; every claim it guards is below and
159-
// unchanged.
160-
// 44 at `@objectstack/spec` 17.6.0, which adopted `imageField` (measured against
161-
// a 17.5.0 control: gained set exactly `['imageField']`, lost set empty;
162-
// objectui#11438). Re-pointed as the 17.3.0 adoption was, not weakened.
163-
// 45 at `@objectstack/spec` 18.0.0-next.0, which adopted `attachedOnRead`
164-
// (measured against a 17.7.0 control: gained set exactly `['attachedOnRead']`,
165-
// lost set empty; objectui#12030). Re-pointed the same way.
166-
// 46 on the objectstack `main` pin after 18.0.0-next.3, which adopted
167-
// `defaultRowScope` (objectstack#24398; measured against the next.3 pin: the
168-
// one top-level key `ObjectSchema` gains between the two commits, none lost;
169-
// objectui#12971). Re-pointed the same way.
170-
// 47 at `@objectstack/spec` 18.1.0, which adopted `membershipKeys`
171-
// (objectstack#25513; measured against the 915e9404 pin: the spec's own
172-
// `authorable-surface/data.json` gains exactly `data/Object:membershipKeys`
173-
// between the two commits, none lost; objectui#13440). Re-pointed the same way.
174-
// 48 on the objectstack `main` pin after objectstack#26005 (5e113b39), which adopted
175-
// `sharing` (measured against its parent fbb75998: gained set exactly `['sharing']`,
176-
// lost set empty; unchanged at 43b47156). Re-pointed the same way.
175+
// The claims are asserted by name over the accept set; an exact key count
176+
// used to stand in for them and was re-pointed six times by unrelated spec
177+
// adoptions (42 → 48), so it is gone (#26238).
177178
const accept = new Set(Object.keys(ObjectSchema.shape as Record<string, unknown>));
178-
expect(accept.size).toBe(48);
179179
// `fieldGroups` is the only grouping key on the object, and it groups the
180180
// FIELDS INSIDE one object — it is not a category for objects themselves,
181181
// so `group` has no mapping target here.
182182
expect(accept.has('fieldGroups')).toBe(true);
183183
for (const key of ['group', 'sortOrder', 'relationships', 'order', 'category', 'sortField']) {
184184
expect(accept.has(key), `ObjectSchema unexpectedly accepts \`${key}\``).toBe(false);
185185
}
186+
// Nothing in the accept set is within two edits of a resolved key, so none
187+
// of the three was a misspelling of a key the schema does declare.
188+
const nearSpellings = [...accept].filter((key) =>
189+
['group', 'sortOrder', 'relationships'].some((resolved) => editDistance(key, resolved) <= 2),
190+
);
191+
expect(nearSpellings, 'ObjectSchema accepts a near-spelling of a resolved key').toEqual([]);
186192
});
187193
});
188194

‎ui/packages/app-shell/src/utils/__tests__/recordFormModalSize-12803.test.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,11 @@ describe('resolveFormViewLayout carries the authored modalSize (objectui#12803)'
3030
});
3131
});
3232

33-
it('the cases above are the whole spec enum', () => {
34-
expect(FormViewSchema.shape.modalSize.unwrap().options).toEqual(['sm', 'default', 'lg', 'xl', 'full']);
33+
it('every case above is a size the spec enum allows', () => {
34+
// Membership, not the whole enum: a size the spec adds later is copied by
35+
// the same pass-through and needs no edit here (#26238).
36+
const allowed: readonly string[] = FormViewSchema.shape.modalSize.unwrap().options;
37+
for (const size of ['sm', 'default', 'lg', 'xl', 'full']) expect(allowed).toContain(size);
3538
});
3639

3740
it('copies modalSize beside sections, and from the legacy formViews.default', () => {

‎ui/packages/app-shell/src/views/ObjectView.kanbanLane-8193.test.tsx‎

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -216,19 +216,15 @@ describe('the emitted lane key is the one `@objectstack/spec` declares (objectui
216216
expect(unrecognized(complete({ groupBy: 'stage' }))).toContain('groupBy');
217217
});
218218

219-
it('declares exactly the four keys this file reasons about', () => {
219+
it('declares the lane key and neither retired spelling — the premise of the arms above', () => {
220220
// A tripwire on the premise itself: if a future spec bump declares
221-
// `groupBy` (or drops `groupByField`), the arms above stop meaning what
222-
// they say and this reddens first. It fired at the 17.5.0 bump
223-
// (objectui#11073) for a key that is NOT a lane spelling: 17.5.0 declares
224-
// `titleField` (the card-title binding, falling back to the record display
225-
// name). Re-read: every arm above is about the lane key and still holds, so
226-
// the pin moves from three keys to four and nothing else does.
227-
expect(Object.keys((KanbanConfigSchema as any).shape).sort()).toEqual([
228-
'columns',
229-
'groupByField',
230-
'summarizeField',
231-
'titleField',
232-
]);
221+
// `groupBy` or `groupField` (or drops `groupByField`), the arms above stop
222+
// meaning what they say and this reddens first. Asserted by name: the
223+
// exact key list it used to pin fired at 17.5.0 for `titleField`, a key
224+
// that is not a lane spelling at all (#26238).
225+
const keys = Object.keys((KanbanConfigSchema as any).shape);
226+
expect(keys).toContain('groupByField');
227+
expect(keys).not.toContain('groupField');
228+
expect(keys).not.toContain('groupBy');
233229
});
234230
});

‎ui/packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.specDerivation.test.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,13 @@ describe('KIND_TO_RECORD_LOOKUP is derived from APPROVER_VALUE_SOURCES (objectui
6262
// the boundary to be re-drawn deliberately.
6363
for (const kind of EXPECTED_KINDS) {
6464
const source = APPROVER_VALUE_SOURCES[kind as keyof typeof APPROVER_VALUE_SOURCES] as Record<string, unknown>;
65-
expect(Object.keys(source).sort()).toEqual(['object', 'source', 'valueField']);
65+
const specKeys = Object.keys(source);
66+
for (const key of ['object', 'source', 'valueField']) expect(specKeys).toContain(key);
67+
// By name, not as the spec's whole key list (#26238): none of the keys
68+
// this package owns for presentation is published by the spec.
69+
const presentation = Object.keys(KIND_TO_RECORD_LOOKUP[kind]!).filter((k) => k !== 'object' && k !== 'valueField');
70+
expect(presentation).toContain('displayField');
71+
for (const key of presentation) expect(specKeys, `the spec now publishes \`${key}\``).not.toContain(key);
6672
expect(KIND_TO_RECORD_LOOKUP[kind]!.displayField).toBeTruthy();
6773
}
6874
});

‎ui/packages/app-shell/src/views/metadata-admin/packageSource-12790.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ describe('PACKAGE_SOURCES is the closed set the spec declares (objectui#12790)',
4545

4646
it('equals the `source` options on both stage branches of the served row', () => {
4747
const branches = InstalledPackageAtEitherStageSchema.options;
48-
expect(branches).toHaveLength(2);
48+
// Non-vacuity floor for the loop; the branch count is the spec's (#26238).
49+
expect(branches.length).toBeGreaterThan(0);
4950
for (const branch of branches) {
5051
expect(sorted(branch.shape.source.options)).toEqual(sorted(PACKAGE_SOURCES));
5152
}

‎ui/packages/app-shell/src/views/metadata-admin/previews/__tests__/ActionPreview.locations.test.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ describe('ActionPreview placement frames', () => {
108108
// re-added the member would leave the pins above passing while the designer
109109
// silently stopped previewing a location the product does declare.
110110
expect(ACTION_LOCATIONS as readonly string[]).not.toContain('global_nav');
111-
expect(ACTION_LOCATIONS.length).toBe(6);
111+
// Control: the vocabulary is a reading, not an empty list (#26238).
112+
expect(ACTION_LOCATIONS.length).toBeGreaterThan(0);
112113
});
113114
});

0 commit comments

Comments
 (0)