diff --git a/ui/apps/console/src/__tests__/component-input-union-specimens.test.ts b/ui/apps/console/src/__tests__/component-input-union-specimens.test.ts index e6365b25beb..1dc25aaf66f 100644 --- a/ui/apps/console/src/__tests__/component-input-union-specimens.test.ts +++ b/ui/apps/console/src/__tests__/component-input-union-specimens.test.ts @@ -310,8 +310,10 @@ describe('objectui#4970 — element:text.content / element:button.label declare // these arms honest rather than copied from the issue — #4970's own table // was measured at the 17.0.0-rc.6 pin, and the answer had to be re-taken // before the declarations moved. - expect(specAcceptedArms(ElementTextPropsSchema, 'content')).toEqual(['string', 'object']); - expect(specAcceptedArms(ElementButtonPropsSchema, 'label')).toEqual(['string', 'object']); + // Membership (#26238): the arms the declarations below must carry. An arm + // the spec adds is caught by the derived comparisons, not by a literal. + expect(specAcceptedArms(ElementTextPropsSchema, 'content')).toEqual(expect.arrayContaining(['string', 'object'])); + expect(specAcceptedArms(ElementButtonPropsSchema, 'label')).toEqual(expect.arrayContaining(['string', 'object'])); }); it('`element:text.content` accepts the inline translation map', () => { @@ -417,15 +419,10 @@ describe('objectui#5717 — element:text_input label / placeholder / description // Measured on the `@objectstack/spec` 17.1.0 pin — the same discipline the // #4970 block adopted, and the reason these arms are honest rather than // copied from the card that reported them. - expect(specAcceptedArms(ElementTextInputPropsSchema, 'label')).toEqual(['string', 'object']); - expect(specAcceptedArms(ElementTextInputPropsSchema, 'placeholder')).toEqual([ - 'string', - 'object', - ]); - expect(specAcceptedArms(ElementTextInputPropsSchema, 'description')).toEqual([ - 'string', - 'object', - ]); + // Membership (#26238), as in the #4970 block above. + for (const key of ['label', 'placeholder', 'description']) { + expect(specAcceptedArms(ElementTextInputPropsSchema, key), key).toEqual(expect.arrayContaining(['string', 'object'])); + } }); it('the sibling key whose contract has NO object arm is measured the same way', () => { @@ -433,10 +430,9 @@ describe('objectui#5717 — element:text_input label / placeholder / description // declaration, so "why is defaultValue different" is answered by evidence // in the same vocabulary. Without this line the per-key control below reads // as a convention; with it, it reads as the contract. - expect(specAcceptedArms(ElementTextInputPropsSchema, 'defaultValue')).toEqual([ - 'string', - 'number', - ]); + const defaultValueArms = specAcceptedArms(ElementTextInputPropsSchema, 'defaultValue'); + expect(defaultValueArms).toEqual(expect.arrayContaining(['string', 'number'])); + expect(defaultValueArms).not.toContain('object'); }); it('`element:text_input.label` accepts the inline translation map', () => { diff --git a/ui/packages/app-shell/src/console/marketplace/__tests__/plugin-runtime-tier-3846.test.tsx b/ui/packages/app-shell/src/console/marketplace/__tests__/plugin-runtime-tier-3846.test.tsx index 901952ca5b3..045239b8b7d 100644 --- a/ui/packages/app-shell/src/console/marketplace/__tests__/plugin-runtime-tier-3846.test.tsx +++ b/ui/packages/app-shell/src/console/marketplace/__tests__/plugin-runtime-tier-3846.test.tsx @@ -57,7 +57,7 @@ describe('objectui#3846 — the trust-tier label map answers to the spec enum', it('covers exactly the tiers PluginRuntimeSchema declares', () => { // The producer's own list, read from the schema rather than retyped. const declared = [...PluginRuntimeSchema.options].sort(); - expect(declared).toEqual(['node', 'sandbox', 'worker']); + for (const tier of ['node', 'sandbox', 'worker']) expect(declared).toContain(tier); expect(Object.keys(RUNTIME_FALLBACK).sort()).toEqual(declared); }); diff --git a/ui/packages/app-shell/src/services/MetadataService.readDecorationStrip.test.ts b/ui/packages/app-shell/src/services/MetadataService.readDecorationStrip.test.ts index 57eadc2cc85..625046cdfb3 100644 --- a/ui/packages/app-shell/src/services/MetadataService.readDecorationStrip.test.ts +++ b/ui/packages/app-shell/src/services/MetadataService.readDecorationStrip.test.ts @@ -123,10 +123,11 @@ const DECORATED_SERVED = { // --------------------------------------------------------------------------- describe('the instrument', () => { - it('is the spec that names the decorations, and it names exactly these two', () => { + it('is the spec that names the decorations, and it names these two', () => { // Stated before any claim about the fix. If this list grows, the pins below - // follow it without an edit — that is why they read it rather than spell it. - expect([...METADATA_READ_DECORATIONS]).toEqual(['_diagnostics', '_draft']); + // follow it without an edit — that is why they read it rather than spell it, + // and why this asserts membership rather than the whole list (#26238). + expect([...METADATA_READ_DECORATIONS]).toEqual(expect.arrayContaining(['_diagnostics', '_draft'])); }); it('refuses each decoration BY NAME — the defect, on the schema', () => { diff --git a/ui/packages/app-shell/src/services/MetadataService.retiredObjectEnabled.test.ts b/ui/packages/app-shell/src/services/MetadataService.retiredObjectEnabled.test.ts index b371aed8192..a9078efd3b4 100644 --- a/ui/packages/app-shell/src/services/MetadataService.retiredObjectEnabled.test.ts +++ b/ui/packages/app-shell/src/services/MetadataService.retiredObjectEnabled.test.ts @@ -101,6 +101,26 @@ const unrecognizedKeys = (result: { success: boolean; error?: unknown }): string .filter((i) => i.code === 'unrecognized_keys') .flatMap((i) => i.keys ?? [])); +/** + * Levenshtein distance, case-insensitive. The near-spelling claims below are + * stated over the accept set with this, rather than through a key count that + * every unrelated spec adoption moves (#26238). + */ +function editDistance(a: string, b: string): number { + const [x, y] = [a.toLowerCase(), b.toLowerCase()]; + const row = Array.from({ length: y.length + 1 }, (_, j) => j); + for (let i = 1; i <= x.length; i++) { + let diagonal = row[0]; + row[0] = i; + for (let j = 1; j <= y.length; j++) { + const above = row[j]; + row[j] = Math.min(above + 1, row[j - 1] + 1, diagonal + (x[i - 1] === y[j - 1] ? 0 : 1)); + diagonal = above; + } + } + return row[y.length]; +} + /** A base document `ObjectSchema` accepts, for probing one key at a time. */ const BASE = { name: 'account', label: 'Account', fields: { n: { type: 'text', label: 'N' } } }; @@ -137,37 +157,22 @@ describe('objectui#6238 · the schema oracle — both tombstone keys are refused // key-name gate could never have stated it. `enable` passes on the name and // fails on the value, which is why this is objectui#4687's resolution // (delete the declaration) rather than objectui#6041's (rename it). - // 43, not the 42 this pin carried against 17.2.0: `@objectstack/spec` 17.3.0 - // ADOPTED `editMode` (measured — gained set exactly `['editMode']`, lost set - // empty). That adoption is unrelated to this file's subject and is pinned in - // `packages/types/src/__tests__/object-schema-metadata-spec-derivation.test.ts`; - // the count rides here only as the corpus guard for the membership claims - // below, so it is re-pointed, not weakened. - // 44 at `@objectstack/spec` 17.6.0, which adopted `imageField` (measured against - // a 17.5.0 control: gained set exactly `['imageField']`, lost set empty; - // objectui#11438). Re-pointed as the 17.3.0 adoption was, not weakened. - // 45 at `@objectstack/spec` 18.0.0-next.0, which adopted `attachedOnRead` - // (measured against a 17.7.0 control: gained set exactly `['attachedOnRead']`, - // lost set empty; objectui#12030). Re-pointed the same way. - // 46 on the objectstack `main` pin after 18.0.0-next.3, which adopted - // `defaultRowScope` (objectstack#24398; measured against the next.3 pin: the - // one top-level key `ObjectSchema` gains between the two commits, none lost; - // objectui#12971). Re-pointed the same way. - // 47 at `@objectstack/spec` 18.1.0, which adopted `membershipKeys` - // (objectstack#25513; measured against the 915e9404 pin: the spec's own - // `authorable-surface/data.json` gains exactly `data/Object:membershipKeys` - // between the two commits, none lost; objectui#13440). Re-pointed the same way. - // 48 on the objectstack `main` pin after objectstack#26005 (5e113b39), which adopted - // `sharing` (measured against its parent fbb75998: gained set exactly `['sharing']`, - // lost set empty; unchanged at 43b47156). Re-pointed the same way. + // The membership claims are asserted by name over the accept set; an exact + // key count used to stand in for them and was re-pointed six times by + // unrelated spec adoptions (42 → 48), so it is gone (#26238). const accept = new Set(Object.keys(ObjectSchema.shape as Record)); - expect(accept.size).toBe(48); expect(accept.has('enable')).toBe(true); expect(accept.has('enabled')).toBe(false); expect(accept.has('_deleted')).toBe(false); for (const key of ['disabled', 'active', 'isActive', 'deleted', 'isDeleted', 'softDeleted']) { expect(accept.has(key), `ObjectSchema unexpectedly accepts \`${key}\``).toBe(false); } + // No near-spelling of either tombstone key is accepted, except `enable`, + // whose VALUE is what refuses the rename (asserted next). + const nearSpellings = [...accept].filter( + (key) => key !== 'enable' && ['enabled', '_deleted'].some((retired) => editDistance(key, retired) <= 2), + ); + expect(nearSpellings, 'ObjectSchema accepts a near-spelling of a tombstone key').toEqual([]); const asBoolean = ObjectSchema.safeParse({ ...BASE, enable: false }); expect(asBoolean.success).toBe(false); diff --git a/ui/packages/app-shell/src/services/MetadataService.specKeyObjectPayload.test.ts b/ui/packages/app-shell/src/services/MetadataService.specKeyObjectPayload.test.ts index 1e7b199f92c..f001074d7c9 100644 --- a/ui/packages/app-shell/src/services/MetadataService.specKeyObjectPayload.test.ts +++ b/ui/packages/app-shell/src/services/MetadataService.specKeyObjectPayload.test.ts @@ -19,9 +19,8 @@ * shapes: `toObjectPayload` builds it and `saveObject` PUTs it whole to * `PUT /api/v1/meta/object/:name`. * - * Measured against the installed `@objectstack/spec` (ESM build), whose - * `ObjectSchema` accept set is 43 keys at 17.3.0 (42 at 17.2.0, when this file - * was written; the one gained key is `editMode`, unrelated to the three below): + * Measured against the installed `@objectstack/spec` (ESM build), at 17.2.0 + * when this file was written: * * ObjectSchema.safeParse({ ...base, group: 'Sales' }) => unrecognized_keys ["group"] * ObjectSchema.safeParse({ ...base, sortOrder: 3 }) => unrecognized_keys ["sortOrder"] @@ -57,6 +56,26 @@ import { ObjectStackAdapter } from '@object-ui/data-objectstack'; import type { ObjectDefinition } from '@object-ui/types'; import { MetadataService } from './MetadataService'; +/** + * Levenshtein distance, case-insensitive. The no-near-spelling claim below is + * stated over the accept set with this, rather than through a key count that + * every unrelated spec adoption moves (#26238). + */ +function editDistance(a: string, b: string): number { + const [x, y] = [a.toLowerCase(), b.toLowerCase()]; + const row = Array.from({ length: y.length + 1 }, (_, j) => j); + for (let i = 1; i <= x.length; i++) { + let diagonal = row[0]; + row[0] = i; + for (let j = 1; j <= y.length; j++) { + const above = row[j]; + row[j] = Math.min(above + 1, row[j - 1] + 1, diagonal + (x[i - 1] === y[j - 1] ? 0 : 1)); + diagonal = above; + } + } + return row[y.length]; +} + /** The bodies of every PUT the SDK issued, exactly as they went over the wire. */ function makeCapturingAdapter() { const puts: Array> = []; @@ -153,29 +172,10 @@ describe('the instrument', () => { }); it('has no near-spelling for any of them — unlike objectui#6041, nothing here is a rename', () => { - // 43 at `@objectstack/spec` 17.3.0, which adopted `editMode` (measured — - // gained set exactly `['editMode']`, lost set empty). The count is this - // file's corpus guard, not its subject; every claim it guards is below and - // unchanged. - // 44 at `@objectstack/spec` 17.6.0, which adopted `imageField` (measured against - // a 17.5.0 control: gained set exactly `['imageField']`, lost set empty; - // objectui#11438). Re-pointed as the 17.3.0 adoption was, not weakened. - // 45 at `@objectstack/spec` 18.0.0-next.0, which adopted `attachedOnRead` - // (measured against a 17.7.0 control: gained set exactly `['attachedOnRead']`, - // lost set empty; objectui#12030). Re-pointed the same way. - // 46 on the objectstack `main` pin after 18.0.0-next.3, which adopted - // `defaultRowScope` (objectstack#24398; measured against the next.3 pin: the - // one top-level key `ObjectSchema` gains between the two commits, none lost; - // objectui#12971). Re-pointed the same way. - // 47 at `@objectstack/spec` 18.1.0, which adopted `membershipKeys` - // (objectstack#25513; measured against the 915e9404 pin: the spec's own - // `authorable-surface/data.json` gains exactly `data/Object:membershipKeys` - // between the two commits, none lost; objectui#13440). Re-pointed the same way. - // 48 on the objectstack `main` pin after objectstack#26005 (5e113b39), which adopted - // `sharing` (measured against its parent fbb75998: gained set exactly `['sharing']`, - // lost set empty; unchanged at 43b47156). Re-pointed the same way. + // The claims are asserted by name over the accept set; an exact key count + // used to stand in for them and was re-pointed six times by unrelated spec + // adoptions (42 → 48), so it is gone (#26238). const accept = new Set(Object.keys(ObjectSchema.shape as Record)); - expect(accept.size).toBe(48); // `fieldGroups` is the only grouping key on the object, and it groups the // FIELDS INSIDE one object — it is not a category for objects themselves, // so `group` has no mapping target here. @@ -183,6 +183,12 @@ describe('the instrument', () => { for (const key of ['group', 'sortOrder', 'relationships', 'order', 'category', 'sortField']) { expect(accept.has(key), `ObjectSchema unexpectedly accepts \`${key}\``).toBe(false); } + // Nothing in the accept set is within two edits of a resolved key, so none + // of the three was a misspelling of a key the schema does declare. + const nearSpellings = [...accept].filter((key) => + ['group', 'sortOrder', 'relationships'].some((resolved) => editDistance(key, resolved) <= 2), + ); + expect(nearSpellings, 'ObjectSchema accepts a near-spelling of a resolved key').toEqual([]); }); }); diff --git a/ui/packages/app-shell/src/utils/__tests__/recordFormModalSize-12803.test.ts b/ui/packages/app-shell/src/utils/__tests__/recordFormModalSize-12803.test.ts index 11eb64c6ccf..5621844a532 100644 --- a/ui/packages/app-shell/src/utils/__tests__/recordFormModalSize-12803.test.ts +++ b/ui/packages/app-shell/src/utils/__tests__/recordFormModalSize-12803.test.ts @@ -30,8 +30,11 @@ describe('resolveFormViewLayout carries the authored modalSize (objectui#12803)' }); }); - it('the cases above are the whole spec enum', () => { - expect(FormViewSchema.shape.modalSize.unwrap().options).toEqual(['sm', 'default', 'lg', 'xl', 'full']); + it('every case above is a size the spec enum allows', () => { + // Membership, not the whole enum: a size the spec adds later is copied by + // the same pass-through and needs no edit here (#26238). + const allowed: readonly string[] = FormViewSchema.shape.modalSize.unwrap().options; + for (const size of ['sm', 'default', 'lg', 'xl', 'full']) expect(allowed).toContain(size); }); it('copies modalSize beside sections, and from the legacy formViews.default', () => { diff --git a/ui/packages/app-shell/src/views/ObjectView.kanbanLane-8193.test.tsx b/ui/packages/app-shell/src/views/ObjectView.kanbanLane-8193.test.tsx index 660f88eb666..b10963b26bf 100644 --- a/ui/packages/app-shell/src/views/ObjectView.kanbanLane-8193.test.tsx +++ b/ui/packages/app-shell/src/views/ObjectView.kanbanLane-8193.test.tsx @@ -216,19 +216,15 @@ describe('the emitted lane key is the one `@objectstack/spec` declares (objectui expect(unrecognized(complete({ groupBy: 'stage' }))).toContain('groupBy'); }); - it('declares exactly the four keys this file reasons about', () => { + it('declares the lane key and neither retired spelling — the premise of the arms above', () => { // A tripwire on the premise itself: if a future spec bump declares - // `groupBy` (or drops `groupByField`), the arms above stop meaning what - // they say and this reddens first. It fired at the 17.5.0 bump - // (objectui#11073) for a key that is NOT a lane spelling: 17.5.0 declares - // `titleField` (the card-title binding, falling back to the record display - // name). Re-read: every arm above is about the lane key and still holds, so - // the pin moves from three keys to four and nothing else does. - expect(Object.keys((KanbanConfigSchema as any).shape).sort()).toEqual([ - 'columns', - 'groupByField', - 'summarizeField', - 'titleField', - ]); + // `groupBy` or `groupField` (or drops `groupByField`), the arms above stop + // meaning what they say and this reddens first. Asserted by name: the + // exact key list it used to pin fired at 17.5.0 for `titleField`, a key + // that is not a lane spelling at all (#26238). + const keys = Object.keys((KanbanConfigSchema as any).shape); + expect(keys).toContain('groupByField'); + expect(keys).not.toContain('groupField'); + expect(keys).not.toContain('groupBy'); }); }); diff --git a/ui/packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.specDerivation.test.ts b/ui/packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.specDerivation.test.ts index eed4ebadea3..30672f8127c 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.specDerivation.test.ts +++ b/ui/packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.specDerivation.test.ts @@ -62,7 +62,13 @@ describe('KIND_TO_RECORD_LOOKUP is derived from APPROVER_VALUE_SOURCES (objectui // the boundary to be re-drawn deliberately. for (const kind of EXPECTED_KINDS) { const source = APPROVER_VALUE_SOURCES[kind as keyof typeof APPROVER_VALUE_SOURCES] as Record; - expect(Object.keys(source).sort()).toEqual(['object', 'source', 'valueField']); + const specKeys = Object.keys(source); + for (const key of ['object', 'source', 'valueField']) expect(specKeys).toContain(key); + // By name, not as the spec's whole key list (#26238): none of the keys + // this package owns for presentation is published by the spec. + const presentation = Object.keys(KIND_TO_RECORD_LOOKUP[kind]!).filter((k) => k !== 'object' && k !== 'valueField'); + expect(presentation).toContain('displayField'); + for (const key of presentation) expect(specKeys, `the spec now publishes \`${key}\``).not.toContain(key); expect(KIND_TO_RECORD_LOOKUP[kind]!.displayField).toBeTruthy(); } }); diff --git a/ui/packages/app-shell/src/views/metadata-admin/packageSource-12790.test.ts b/ui/packages/app-shell/src/views/metadata-admin/packageSource-12790.test.ts index 8cfa1b379de..60e4212e15a 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/packageSource-12790.test.ts +++ b/ui/packages/app-shell/src/views/metadata-admin/packageSource-12790.test.ts @@ -45,7 +45,8 @@ describe('PACKAGE_SOURCES is the closed set the spec declares (objectui#12790)', it('equals the `source` options on both stage branches of the served row', () => { const branches = InstalledPackageAtEitherStageSchema.options; - expect(branches).toHaveLength(2); + // Non-vacuity floor for the loop; the branch count is the spec's (#26238). + expect(branches.length).toBeGreaterThan(0); for (const branch of branches) { expect(sorted(branch.shape.source.options)).toEqual(sorted(PACKAGE_SOURCES)); } diff --git a/ui/packages/app-shell/src/views/metadata-admin/previews/__tests__/ActionPreview.locations.test.tsx b/ui/packages/app-shell/src/views/metadata-admin/previews/__tests__/ActionPreview.locations.test.tsx index 09a656cf9f8..74858342d6d 100644 --- a/ui/packages/app-shell/src/views/metadata-admin/previews/__tests__/ActionPreview.locations.test.tsx +++ b/ui/packages/app-shell/src/views/metadata-admin/previews/__tests__/ActionPreview.locations.test.tsx @@ -108,6 +108,7 @@ describe('ActionPreview placement frames', () => { // re-added the member would leave the pins above passing while the designer // silently stopped previewing a location the product does declare. expect(ACTION_LOCATIONS as readonly string[]).not.toContain('global_nav'); - expect(ACTION_LOCATIONS.length).toBe(6); + // Control: the vocabulary is a reading, not an empty list (#26238). + expect(ACTION_LOCATIONS.length).toBeGreaterThan(0); }); }); diff --git a/ui/packages/auth/src/__tests__/auth-spec-parity.test.ts b/ui/packages/auth/src/__tests__/auth-spec-parity.test.ts index 45d0b896e83..84910dee555 100644 --- a/ui/packages/auth/src/__tests__/auth-spec-parity.test.ts +++ b/ui/packages/auth/src/__tests__/auth-spec-parity.test.ts @@ -265,9 +265,10 @@ describe('the AuthProvider exemption rests on the spec symbol being an id enum', 'ALLOW entry for @object-ui/auth:AuthProvider assumes it is an enum of provider ' + 'ids — re-read what it means now before keeping the exemption.', ).toBeTruthy(); - expect(Object.keys((specAuthProvider as { def: { entries: Record } }).def.entries).sort()).toEqual( - ['github', 'google', 'ldap', 'local', 'microsoft', 'saml'], - ); + // An enum of provider ids, asserted by member rather than as the spec's + // whole provider list (#26238): a provider the spec adds keeps it an id enum. + const providerIds = Object.keys((specAuthProvider as { def: { entries: Record } }).def.entries); + for (const id of ['github', 'google', 'local']) expect(providerIds).toContain(id); }); it('our AuthProvider is a React component, not a schema', async () => { diff --git a/ui/packages/core/src/utils/__tests__/filter-tokens.recordContext-7297.test.ts b/ui/packages/core/src/utils/__tests__/filter-tokens.recordContext-7297.test.ts index 4e4d79aa0f6..5397ce76ca3 100644 --- a/ui/packages/core/src/utils/__tests__/filter-tokens.recordContext-7297.test.ts +++ b/ui/packages/core/src/utils/__tests__/filter-tokens.recordContext-7297.test.ts @@ -34,10 +34,11 @@ function resolve(filter: unknown, scope: Record) { } describe('{record_id} resolves against the record in scope (objectui#7297)', () => { - it('is the one record-context token the installed spec declares', () => { + it('is a record-context token the installed spec declares', () => { // The resolver's `satisfies Record` literal reds the - // compile on a second one; this states the same ratchet at runtime. - expect([...RECORD_CONTEXT_TOKENS]).toEqual(['record_id']); + // compile on a second one, so that ratchet lives in the type check; at + // runtime only the token this file resolves is asserted (#26238). + expect([...RECORD_CONTEXT_TOKENS]).toContain('record_id'); }); it('resolves in every filter shape, in both spellings', () => { diff --git a/ui/packages/core/src/utils/__tests__/filter-view-rule-arity-8557.test.ts b/ui/packages/core/src/utils/__tests__/filter-view-rule-arity-8557.test.ts index 3b3d6345bdc..c8cdcb497ca 100644 --- a/ui/packages/core/src/utils/__tests__/filter-view-rule-arity-8557.test.ts +++ b/ui/packages/core/src/utils/__tests__/filter-view-rule-arity-8557.test.ts @@ -279,8 +279,9 @@ describe('objectui#8557 — the arity classification cannot drift from the spec' // The list and pair classes are the spec's own exports. The valueless class // is the one written out in the source, because the spec exports no set for // it — so this pin is what keeps it honest. A new spec operator lands in the - // scalar remainder by default, which is the REFUSING class, so this pin is - // also the review trigger for that choice. + // scalar remainder by default, which is the REFUSING class. That default is + // no longer re-confirmed by a whole-vocabulary list: the classes are held + // by name and the partition stays exact (#26238). const valueless = ['is_empty', 'is_not_empty', 'is_null', 'is_not_null']; const arrayValued: string[] = [ ...VIEW_FILTER_LIST_VALUE_OPERATORS, @@ -289,16 +290,17 @@ describe('objectui#8557 — the arity classification cannot drift from the spec' const all = [...VIEW_FILTER_OPERATORS] as string[]; for (const op of [...valueless, ...arrayValued]) expect(all).toContain(op); - expect(arrayValued).toEqual(['in', 'not_in', 'between']); + for (const op of ['in', 'not_in', 'between']) expect(arrayValued).toContain(op); const scalar = all.filter((op) => !valueless.includes(op) && !arrayValued.includes(op)); - // The refusing class, spelled out. If the spec adds an operator this list - // changes and the reader has to say which class it belongs to. - expect(scalar).toEqual([ + // The refusing class, by name. + for (const op of [ 'equals', 'not_equals', 'contains', 'not_contains', 'icontains', 'starts_with', 'ends_with', 'greater_than', 'less_than', 'greater_than_or_equal', 'less_than_or_equal', 'before', 'after', - ]); + ]) { + expect(scalar).toContain(op); + } // Exact partition: no operator in two classes, none in none. expect(scalar.length + valueless.length + arrayValued.length).toBe(all.length); }); diff --git a/ui/packages/core/src/utils/__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts b/ui/packages/core/src/utils/__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts index 98cb370b9a0..1ca86944a11 100644 --- a/ui/packages/core/src/utils/__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts +++ b/ui/packages/core/src/utils/__tests__/normalize-list-view.foldOutputAuthorable-5435.test.ts @@ -110,11 +110,11 @@ describe('the fold`s output is authorable (objectui#5435)', () => { it('reads a non-empty spec vocabulary that contains all three adopted keys', () => { const specKeys = Object.keys(UserActionsConfigSchema.shape).sort(); - expect(specKeys.length).toBe(11); - expect(specKeys).toEqual([ - 'addRecordForm', 'buttons', 'editInline', 'filter', 'group', 'hideFields', - 'refresh', 'rowColor', 'rowHeight', 'search', 'sort', - ]); + // Membership, not the spec's whole key list (#26238): every toggle the + // fold emits must be a key the spec declares, and that is all it needs. + for (const key of ['filter', 'group', 'hideFields', 'rowColor', 'rowHeight', 'search', 'sort']) { + expect(specKeys).toContain(key); + } // The card's whole subject: these were absent at 17.0.0. for (const key of ADOPTED_KEYS) expect(specKeys).toContain(key); diff --git a/ui/packages/core/src/utils/__tests__/normalize-list-view.pageResidual-8429.test.ts b/ui/packages/core/src/utils/__tests__/normalize-list-view.pageResidual-8429.test.ts index 1efb2549908..2a91cc2a3c2 100644 --- a/ui/packages/core/src/utils/__tests__/normalize-list-view.pageResidual-8429.test.ts +++ b/ui/packages/core/src/utils/__tests__/normalize-list-view.pageResidual-8429.test.ts @@ -140,20 +140,17 @@ describe('the `page` residual (objectui#8429), closed by the protocol at @object }); describe('the populations, sized before they are used', () => { - it('reads a non-empty, exactly-sized vocabulary from each face', () => { - // The spec's list-view types, as published in @objectstack/spec 17.5.0: - // the nine visualizations, `page` retired. - expect(SPEC_LIST_VIEW_TYPES.length).toBe(9); - expect([...SPEC_LIST_VIEW_TYPES].sort()).toEqual([ - 'calendar', 'chart', 'gallery', 'gantt', 'grid', 'kanban', 'map', 'timeline', 'tree', - ]); - - // objectui's derived face: the spec's nine, plus the two local categories. - expect(OBJECTUI_VIEW_TYPES.length).toBe(11); - expect([...OBJECTUI_VIEW_TYPES].sort()).toEqual([ - 'calendar', 'chart', 'detail', 'gallery', 'gantt', 'grid', - 'kanban', 'list', 'map', 'timeline', 'tree', - ]); + it('reads a non-empty vocabulary from each face, `page` on neither', () => { + // The spec's list-view types, as published since @objectstack/spec 17.5.0: + // the nine visualizations ListView draws, `page` retired. Asserted by + // name; the spec's whole list is not this file's to pin (#26238). + const drawn = ['calendar', 'chart', 'gallery', 'gantt', 'grid', 'kanban', 'map', 'timeline', 'tree']; + for (const kind of drawn) expect(SPEC_LIST_VIEW_TYPES).toContain(kind); + expect(SPEC_LIST_VIEW_TYPES).not.toContain('page'); + + // objectui's derived face: the spec's kinds, plus the two local categories. + for (const kind of ['detail', 'list']) expect(OBJECTUI_VIEW_TYPES).toContain(kind); + expect(OBJECTUI_VIEW_TYPES.length).toBe(SPEC_LIST_VIEW_TYPES.length + 2); // The derivation itself: objectui declares a superset, never a subset. for (const kind of SPEC_LIST_VIEW_TYPES) { @@ -161,11 +158,11 @@ describe('the `page` residual (objectui#8429), closed by the protocol at @object } }); - it('partitions objectui`s vocabulary into 9 drawable kinds and the 2 local categories', () => { + it('partitions objectui`s vocabulary into the drawable kinds and the 2 local categories', () => { const drawable = OBJECTUI_VIEW_TYPES.filter((k) => isListViewVisualization(k)); const undrawable = OBJECTUI_VIEW_TYPES.filter((k) => !isListViewVisualization(k)); - expect(drawable.length).toBe(9); + expect(drawable.length).toBeGreaterThan(0); expect(undrawable.length).toBe(2); expect(drawable.length + undrawable.length).toBe(OBJECTUI_VIEW_TYPES.length); expect([...undrawable].sort()).toEqual(['detail', 'list']); @@ -176,8 +173,9 @@ describe('the `page` residual (objectui#8429), closed by the protocol at @object it('leaves no spec-authorable list-view type ListView cannot draw', () => { const residual = SPEC_LIST_VIEW_TYPES.filter((k) => !isListViewVisualization(k)); expect(residual).toEqual([]); - // Control: the subtraction is a real one — every one of the spec's nine IS drawable. - expect(SPEC_LIST_VIEW_TYPES.filter((k) => isListViewVisualization(k)).length).toBe(9); + // Control: the subtraction is a real one — every one of the spec's kinds IS drawable. + expect(SPEC_LIST_VIEW_TYPES.filter((k) => isListViewVisualization(k)).length).toBe(SPEC_LIST_VIEW_TYPES.length); + expect(SPEC_LIST_VIEW_TYPES.length).toBeGreaterThan(0); }); it('refuses `page` on BOTH published `@object-ui/types` faces, as the spec does', () => { @@ -214,15 +212,17 @@ describe('the `page` residual (objectui#8429), closed by the protocol at @object }); describe('the whole vocabulary, as a census', () => { - it('maps every one of the 11 declared kinds to what the renderer actually gets', () => { + it('maps every declared kind to what the renderer actually gets', () => { const census = new Map( OBJECTUI_VIEW_TYPES.map((kind) => [ kind, readViewType(normalizeListViewSchema({ type: 'list-view', objectName: 'account', specType: kind })), ]), ); - expect(census.size).toBe(11); - expect(Object.fromEntries(census)).toEqual({ + expect(census.size).toBe(OBJECTUI_VIEW_TYPES.length); + // The kinds named here by their mapping; a kind the spec adds later is + // held by the closing line instead (it must resolve to itself, #26238). + expect(Object.fromEntries(census)).toMatchObject({ grid: 'grid', kanban: 'kanban', gallery: 'gallery', diff --git a/ui/packages/data-objectstack/src/draft-envelope.test.ts b/ui/packages/data-objectstack/src/draft-envelope.test.ts index dad24941fa0..9c47cd00bb4 100644 --- a/ui/packages/data-objectstack/src/draft-envelope.test.ts +++ b/ui/packages/data-objectstack/src/draft-envelope.test.ts @@ -53,9 +53,10 @@ const envelope = (item: unknown) => ({ type: 'object', name: 'crmext_visit', ite describe('extractDraftBody — the hoisted draft-envelope reader (objectui#8181)', () => { it('is the spec list that gets removed, not a local one', () => { - // The control for every "was it stripped?" assertion below: if the spec - // ever adds a third decoration, this file must be read again. - expect([...METADATA_READ_DECORATIONS]).toEqual(['_diagnostics', '_draft']); + // The control for every "was it stripped?" assertion below, which name + // these two. Membership (#26238): a third decoration the spec adds is + // stripped by the same spec list without an edit here. + expect([...METADATA_READ_DECORATIONS]).toEqual(expect.arrayContaining(['_diagnostics', '_draft'])); }); it('removes the read decorations and leaves the authored body untouched', () => { diff --git a/ui/packages/data-objectstack/src/listViews.roundTripKeys-11013.test.ts b/ui/packages/data-objectstack/src/listViews.roundTripKeys-11013.test.ts index 584f261ff8c..eaf6a958c0c 100644 --- a/ui/packages/data-objectstack/src/listViews.roundTripKeys-11013.test.ts +++ b/ui/packages/data-objectstack/src/listViews.roundTripKeys-11013.test.ts @@ -83,8 +83,12 @@ async function reloadRow(ds: any): Promise> { } describe('objectui#11013 — the carried set is the spec\'s', () => { - it('the ViewItem round-trip keys are the five the card names, read off the spec', () => { - expect([...VIEW_ITEM_KEYS].sort()).toEqual(['columnState', 'isDefault', 'isPinned', 'sortOrder', 'visibility']); + it('the ViewItem round-trip keys include the five the card names, read off the spec', () => { + // Membership (#26238): the set is the spec's, so a key it adds is carried + // too; what this card needs is that these five are among them. + for (const key of ['columnState', 'isDefault', 'isPinned', 'sortOrder', 'visibility']) { + expect(VIEW_ITEM_KEYS).toContain(key); + } // Control on the same record: a key declared on the list overlay only is not one of them. expect(VIEW_ITEM_KEYS).not.toContain('_isOverride'); expect(Object.keys(VIEW_CONSOLE_ROUND_TRIP_KEYS)).toContain('_isOverride'); diff --git a/ui/packages/data-objectstack/src/metadata-client.overlayScope.test.ts b/ui/packages/data-objectstack/src/metadata-client.overlayScope.test.ts index ab7d41b3a7d..67db6e217f0 100644 --- a/ui/packages/data-objectstack/src/metadata-client.overlayScope.test.ts +++ b/ui/packages/data-objectstack/src/metadata-client.overlayScope.test.ts @@ -89,15 +89,16 @@ const _notASpecScope: MetadataLayered['overlayScope'] = 'organization'; // `LAYER_SCOPE_ZH`'s `org` entry left with it (objectui#12030). describe('MetadataLayered.overlayScope tracks the spec enum (objectui#4982)', () => { - it('the producer vocabulary is exactly env, plus null, on the installed spec', () => { + it('the producer vocabulary holds env, plus null, on the installed spec', () => { // Read through `@object-ui/test-support` rather than `.unwrap().options` // (objectui#5872 class (4)). The bare spelling was a SEVENTH way to ask this // question and the only one with neither a cast nor a guard: it answered // correctly only while `overlayScope` stayed wrapped exactly once and Zod // kept `.options` where it is. `enumOptions` walks the wrappers and answers // `[]` when it cannot read — and `[]` fails this assertion, which is the - // non-vacuity duty that reader's docblock leaves with every caller. - expect(enumOptions(specOverlayScope)).toEqual(['env']); + // non-vacuity duty that reader's docblock leaves with every caller. Asserted + // as membership (#26238); the retired spellings are refused by name below. + expect(enumOptions(specOverlayScope)).toContain('env'); expect(specOverlayScope.safeParse(null).success).toBe(true); }); diff --git a/ui/packages/fields/src/__tests__/LocationField.optionalKeys.test.tsx b/ui/packages/fields/src/__tests__/LocationField.optionalKeys.test.tsx index b0b0fd436b0..60a1b516da5 100644 --- a/ui/packages/fields/src/__tests__/LocationField.optionalKeys.test.tsx +++ b/ui/packages/fields/src/__tests__/LocationField.optionalKeys.test.tsx @@ -206,7 +206,10 @@ describe('NEGATIVE CONTROL — the carry is a key-by-key pick, never a spread (o provider: 'gps', altitude: 5, }); - expect(SPEC_KEYS).toEqual(['lat', 'lng', 'altitude', 'accuracy']); + // The derivation's non-vacuity half, by name (#26238): the keys this field + // writes are declared, and the retired pair is not. + for (const key of ['lat', 'lng', 'altitude']) expect(SPEC_KEYS).toContain(key); + for (const key of ['latitude', 'longitude']) expect(SPEC_KEYS).not.toContain(key); expect(Object.keys(emitted).filter((k) => !SPEC_KEYS.includes(k))).toEqual([]); }); diff --git a/ui/packages/fields/src/widgets/__tests__/RegistryRefField.audienceAnchors-12200.test.tsx b/ui/packages/fields/src/widgets/__tests__/RegistryRefField.audienceAnchors-12200.test.tsx index a2d1f090fde..21626cfbbb6 100644 --- a/ui/packages/fields/src/widgets/__tests__/RegistryRefField.audienceAnchors-12200.test.tsx +++ b/ui/packages/fields/src/widgets/__tests__/RegistryRefField.audienceAnchors-12200.test.tsx @@ -72,8 +72,9 @@ afterEach(() => cleanup()); describe('position-ref leaves the audience anchors out of what it offers (objectui#12200)', () => { it('offers the assignable positions and no anchor', async () => { - // The names the card asks for are the ones the spec publishes. - expect([...AUDIENCE_ANCHOR_POSITIONS].sort()).toEqual(['everyone', 'guest']); + // The names the card asks for are ones the spec publishes (membership, + // #26238: the list itself is the spec's). + for (const anchor of ['everyone', 'guest']) expect([...AUDIENCE_ANCHOR_POSITIONS]).toContain(anchor); mount(PositionRefField, { dataSource: lister(POSITIONS) }); diff --git a/ui/packages/plugin-dashboard/src/__tests__/spec-symbol-dataset-totals-11073.test.ts b/ui/packages/plugin-dashboard/src/__tests__/spec-symbol-dataset-totals-11073.test.ts index 841f56bcd3e..396b9b3a0db 100644 --- a/ui/packages/plugin-dashboard/src/__tests__/spec-symbol-dataset-totals-11073.test.ts +++ b/ui/packages/plugin-dashboard/src/__tests__/spec-symbol-dataset-totals-11073.test.ts @@ -29,7 +29,8 @@ import { DatasetTotalsSchema } from '@objectstack/spec/api'; describe('objectui#11073 — the spec`s DatasetTotals is the request, not a result grouping', () => { it('declares `groupings` and none of the result grouping`s members', () => { const keys = Object.keys((DatasetTotalsSchema as unknown as { shape: Record }).shape); - expect(keys).toEqual(['groupings']); + // By name (#26238): the request side is identified by `groupings`. + expect(keys).toContain('groupings'); for (const resultMember of ['dimensions', 'rows']) expect(keys).not.toContain(resultMember); }); }); diff --git a/ui/packages/plugin-designer/src/MetadataFieldsPage.storedTypePreserved.test.tsx b/ui/packages/plugin-designer/src/MetadataFieldsPage.storedTypePreserved.test.tsx index 13cc6b7db7a..41d73b24f9f 100644 --- a/ui/packages/plugin-designer/src/MetadataFieldsPage.storedTypePreserved.test.tsx +++ b/ui/packages/plugin-designer/src/MetadataFieldsPage.storedTypePreserved.test.tsx @@ -95,8 +95,9 @@ const CENSUS: readonly string[] = SPEC_FIELD_TYPES.filter((t) => !DESIGNER_TYPES /** * Measured on `@objectstack/spec` 17.3.0 with `DESIGNER_FIELD_TYPES` at 27 * members. Restated here as a LITERAL so the derivation above cannot quietly - * answer something else: if either vocabulary moves, this fails and a human - * reads the new difference rather than inheriting a stale one. + * answer something else: every member must still be in the census. A type the + * spec adds later joins the census and is round-tripped by the same pins, so + * the census is held as a superset of this list, not equal to it (#26238). */ const CENSUS_AT_FILING = [ 'audio', 'avatar', 'checkboxes', 'composite', 'json', 'master_detail', @@ -224,19 +225,17 @@ afterEach(() => { // --------------------------------------------------------------------------- describe('objectui#8060 · C0 the census', () => { - it('is non-empty, is exactly the 22 measured members, and is the SPEC minus the DESIGNER set', () => { - expect(SPEC_FIELD_TYPES).toHaveLength(49); + it('is non-empty, holds every measured member, and is the SPEC minus the DESIGNER set', () => { expect(DESIGNER_TYPES).toHaveLength(27); // ⚠️ The vacuity guard. Every iterating pin below is green over an empty - // set; these three lines are what makes an emptied census fail loudly. + // set; these lines are what makes an emptied census fail loudly. expect(CENSUS.length).toBeGreaterThan(0); - expect(CENSUS).toHaveLength(22); - expect([...CENSUS].sort()).toEqual([...CENSUS_AT_FILING]); + for (const type of CENSUS_AT_FILING) expect(CENSUS).toContain(type); // Why direction 1 (preserve + read-only) is viable at all: the designer's // vocabulary is a strict SUBSET of the spec's, so nothing it can author is - // unknown to the spec, and 27 + 22 = 49 accounts for every declared type. + // unknown to the spec, and designer + census accounts for every declared type. expect(DESIGNER_TYPES.filter((t) => !SPEC_FIELD_TYPES.includes(t))).toEqual([]); expect(DESIGNER_TYPES.length + CENSUS.length).toBe(SPEC_FIELD_TYPES.length); }); @@ -285,9 +284,10 @@ describe('objectui#8060 · P1 every census member survives an edit to a differen cleanup(); } - // The vacuity guard, restated where the iteration happens. - expect(checked).toBe(22); + // The vacuity guard, restated where the iteration happens: every census + // member was checked, and the census holds at least the measured members. expect(checked).toBe(CENSUS.length); + expect(checked).toBeGreaterThanOrEqual(CENSUS_AT_FILING.length); }); }); diff --git a/ui/packages/plugin-detail/src/renderers/__tests__/recordActivityFeed.test.ts b/ui/packages/plugin-detail/src/renderers/__tests__/recordActivityFeed.test.ts index 4a3bdad45cb..aa6a4f6e407 100644 --- a/ui/packages/plugin-detail/src/renderers/__tests__/recordActivityFeed.test.ts +++ b/ui/packages/plugin-detail/src/renderers/__tests__/recordActivityFeed.test.ts @@ -693,12 +693,17 @@ describe('a `types` entry naming a kind nothing produces says so (objectui#5877) * turn this red: the census is the diagnostic's input, and it moving without * anyone noticing is exactly how the warn would start lying. */ - it('the census — 13 declared kinds, 5 produced, 8 not', () => { - expect(declared).toHaveLength(13); + it('the census — 5 produced kinds, the declared rest not', () => { + // The produced set is this package's and stays exact. The declared set is + // the spec's, so it is held by name (#26238): a kind the spec adds lands on + // the unproduced side, which is what the diagnostic warns about. expect(produced.slice().sort()).toEqual(['comment', 'event', 'field_change', 'system', 'task']); - expect(unproduced.slice().sort()).toEqual([ + for (const kind of [ 'approval', 'call', 'email', 'file', 'note', 'record_create', 'record_delete', 'sharing', - ]); + ]) { + expect(unproduced).toContain(kind); + } + expect(produced.length + unproduced.length).toBe(declared.length); }); it('`system` is PRODUCED — the unmapped fallback is a producer, not a gap', () => { diff --git a/ui/packages/plugin-list/src/__tests__/UserFilters.tabPresetOperators.test.tsx b/ui/packages/plugin-list/src/__tests__/UserFilters.tabPresetOperators.test.tsx index 85ee6d7e08a..24a96779520 100644 --- a/ui/packages/plugin-list/src/__tests__/UserFilters.tabPresetOperators.test.tsx +++ b/ui/packages/plugin-list/src/__tests__/UserFilters.tabPresetOperators.test.tsx @@ -122,9 +122,10 @@ describe('UserFilters tab presets — the reported defect (#3470)', () => { describe('UserFilters tab presets — lowering is structural, not translated', () => { it('reads both vocabularies from the spec', () => { // Guards every it.each below against silently passing on an empty list. - // 19 through @objectstack/spec 17.0.0; 20 as of 17.1.0, which adds the - // canonical `icontains` (objectui#5328). - expect(VIEW_FILTER_OPERATORS.length).toBe(20); + // A floor and a named member, not the spec's count (#26238); `icontains` + // is the canonical spelling 17.1.0 added (objectui#5328). + expect(VIEW_FILTER_OPERATORS.length).toBeGreaterThan(0); + expect(VIEW_FILTER_OPERATORS as readonly string[]).toContain('icontains'); expect(VALID_AST_OPERATORS.size).toBeGreaterThan(0); }); diff --git a/ui/packages/plugin-report/src/__tests__/spec-symbol-dataset-totals-11073.test.ts b/ui/packages/plugin-report/src/__tests__/spec-symbol-dataset-totals-11073.test.ts index 8e478b0c55b..5242588d1c6 100644 --- a/ui/packages/plugin-report/src/__tests__/spec-symbol-dataset-totals-11073.test.ts +++ b/ui/packages/plugin-report/src/__tests__/spec-symbol-dataset-totals-11073.test.ts @@ -29,7 +29,8 @@ import { DatasetTotalsSchema } from '@objectstack/spec/api'; describe('objectui#11073 — the spec`s DatasetTotals is the request, not a result grouping', () => { it('declares `groupings` and none of the result grouping`s members', () => { const keys = Object.keys((DatasetTotalsSchema as unknown as { shape: Record }).shape); - expect(keys).toEqual(['groupings']); + // By name (#26238): the request side is identified by `groupings`. + expect(keys).toContain('groupings'); for (const resultMember of ['dimensions', 'rows']) expect(keys).not.toContain(resultMember); }); }); diff --git a/ui/packages/plugin-tree/src/ObjectTree.schemaTyped-8655.test.ts b/ui/packages/plugin-tree/src/ObjectTree.schemaTyped-8655.test.ts index c6fe50ec2f9..c8797ddae15 100644 --- a/ui/packages/plugin-tree/src/ObjectTree.schemaTyped-8655.test.ts +++ b/ui/packages/plugin-tree/src/ObjectTree.schemaTyped-8655.test.ts @@ -457,11 +457,15 @@ describe('objectui#8655 — `filter` is the one DECLARE verdict, executed by obj }); describe('objectui#8655 — `tree` was a VIEW-level block, and since 17.5.0 the node\'s own (declared by objectui#11168)', () => { - it('the spec declares the block, and its members are the four this resolver reads', () => { + it('the spec declares the block, and its members include the four this resolver reads', () => { const blockKeys = Object.keys( (TreeConfigSchema as unknown as { shape: Record }).shape, ).sort(); - expect(blockKeys).toEqual(['defaultExpandedDepth', 'fields', 'labelField', 'parentField']); + // Membership (#26238): a key the spec adds to the block is not one this + // resolver reads, so it does not move this claim. + for (const key of ['defaultExpandedDepth', 'fields', 'labelField', 'parentField']) { + expect(blockKeys).toContain(key); + } }); it('…and ONE element face declares a `tree` key since 17.5.0 — this node\'s — beside the `list-view` row (control: one key that IS on four)', () => { diff --git a/ui/packages/sdui-parser/src/__tests__/dashboard-widget-options-census.test.ts b/ui/packages/sdui-parser/src/__tests__/dashboard-widget-options-census.test.ts index 79ae3d65406..c7b40757934 100644 --- a/ui/packages/sdui-parser/src/__tests__/dashboard-widget-options-census.test.ts +++ b/ui/packages/sdui-parser/src/__tests__/dashboard-widget-options-census.test.ts @@ -93,10 +93,13 @@ describe('leg 1 — the spec side of the pin', () => { expect((parsed as { data: Record }).data.__census_probe).toBe(1); }); - it('declared keys are exactly the pinned spec set', () => { + it('declared keys include the pinned spec set', () => { // Derivation guard for everything below: a schema that stopped exporting // `shape` (or renamed keys) must fail HERE, not let ⊆-checks pass on []. - expect(declaredKeys).toEqual(['dateGranularity', 'limit', 'sortBy', 'sortOrder', 'stageOrder']); + // Membership (#26238): a key the spec adds is judged by the next case. + for (const key of ['dateGranularity', 'limit', 'sortBy', 'sortOrder', 'stageOrder']) { + expect(declaredKeys).toContain(key); + } }); it('every declared key is accepted, and no undeclared key is — not even the retired `description`', () => { diff --git a/ui/packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts b/ui/packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts index 9bbda9d7b14..bec6c22658e 100644 --- a/ui/packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts +++ b/ui/packages/types/src/__tests__/calendar-flat-color-allday-8466.test.ts @@ -535,13 +535,10 @@ describe('objectui#8466 — the spec refuses ALL FIVE flat keys, which is why de // by name, an asymmetry that was real nested and absent flat. 17.5.0 // declares `allDayField` in the nested block too (objectui#11073), so the // nested asymmetry is gone; the flat refusal of all five did not move. - expect(Object.keys(CalendarConfigSchema.shape)).toEqual([ - 'startDateField', - 'endDateField', - 'titleField', - 'colorField', - 'allDayField', - ]); + // The nested keys this decision turns on, by name (#26238). + expect(Object.keys(CalendarConfigSchema.shape)).toEqual( + expect.arrayContaining(['startDateField', 'colorField', 'allDayField']), + ); expect(CalendarConfigSchema.safeParse({ startDateField: 's', allDayField: 'x' }).success).toBe(true); expect(CalendarConfigSchema.safeParse({ startDateField: 's', colorField: 'c' }).success).toBe(true); // Firing control: a key the nested block never declared is still refused by name. diff --git a/ui/packages/types/src/__tests__/export-options-spec-parity.test.ts b/ui/packages/types/src/__tests__/export-options-spec-parity.test.ts index 804c1cadc62..a58ca6897f5 100644 --- a/ui/packages/types/src/__tests__/export-options-spec-parity.test.ts +++ b/ui/packages/types/src/__tests__/export-options-spec-parity.test.ts @@ -131,10 +131,12 @@ describe('exportOptions ↔ installed @objectstack/spec (objectui#4535)', () => // Non-vacuity floor. Every assertion below reads through `specObjectBranch`, // and a spec refactor that moved the shape would otherwise turn each of them // into a comparison against `undefined` — which some would pass. + // Floors and named members, not the spec's arm and key counts (#26238): + // the parity cases below compare the full key sets, derived on both sides. expect(specExportOptions).toBeDefined(); - expect(specUnion.options).toHaveLength(2); + expect(specUnion.options?.length).toBeGreaterThan(0); expect(specObjectBranch).toBeDefined(); - expect(Object.keys(specObjectBranch?.shape ?? {}).length).toBe(5); + expect(Object.keys(specObjectBranch?.shape ?? {})).toContain('formats'); expect(specFormatEnumValues().length).toBeGreaterThan(0); }); diff --git a/ui/packages/types/src/__tests__/grid-kanban-calendar-members-round8-6152.test.ts b/ui/packages/types/src/__tests__/grid-kanban-calendar-members-round8-6152.test.ts index baa8318f867..c8d1dec460a 100644 --- a/ui/packages/types/src/__tests__/grid-kanban-calendar-members-round8-6152.test.ts +++ b/ui/packages/types/src/__tests__/grid-kanban-calendar-members-round8-6152.test.ts @@ -122,8 +122,13 @@ describe('objectui#6152 round 8 — the mirror members are the rows\' own, by re const local = (ObjectGridSchema.shape.operations as unknown as { unwrap: () => Shaped }).unwrap().shape; const row = (SpecObjectGridPropsSchema.shape.operations as unknown as { unwrap: () => Shaped }).unwrap().shape; for (const key of ['create', 'update', 'delete', 'export']) expect(local[key]).toBe(row[key]); - expect(Object.keys(row).sort()).toEqual(['create', 'delete', 'export', 'update']); - expect(Object.keys(local).sort()).toEqual(['create', 'delete', 'export', 'import', 'read', 'update']); + // The row's toggles by name, and the local face is exactly the row's plus + // the two local tombstones — derived, so a toggle the spec adds rides both + // sides (#26238). + expect(Object.keys(row)).toEqual(expect.arrayContaining(['create', 'delete', 'export', 'update'])); + expect(Object.keys(row)).not.toContain('read'); + expect(Object.keys(row)).not.toContain('import'); + expect(Object.keys(local).sort()).toEqual([...Object.keys(row), 'import', 'read'].sort()); }); }); diff --git a/ui/packages/types/src/__tests__/list-view-options-bag-6152.test.ts b/ui/packages/types/src/__tests__/list-view-options-bag-6152.test.ts index e5c6531ebd1..3cce7d05e84 100644 --- a/ui/packages/types/src/__tests__/list-view-options-bag-6152.test.ts +++ b/ui/packages/types/src/__tests__/list-view-options-bag-6152.test.ts @@ -113,7 +113,9 @@ describe('objectui#6152 round 12 — the installed list overlay\'s `options` bag expect(SPEC_BAG_OPTIONAL._zod.def.type).toBe('optional'); expect(SPEC_BAG._zod.def.type).toBe('object'); expect(catchallOf(SPEC_BAG)).toBe('never'); - expect(KINDS).toEqual(['calendar', 'chart', 'gallery', 'gantt', 'kanban', 'map', 'timeline', 'tree']); + // The kinds this package's blocks cover, by name (#26238); the bag's key + // set is then compared with the spec's own rule, derived on both sides. + for (const kind of Object.keys(ARMS)) expect(KINDS).toContain(kind); expect(Object.keys(SPEC_BAG.shape).sort()).toEqual(KINDS); }); diff --git a/ui/packages/types/src/__tests__/object-grid-export-options-refusal-7762.test.ts b/ui/packages/types/src/__tests__/object-grid-export-options-refusal-7762.test.ts index fc3fcceeadd..363b47bddcf 100644 --- a/ui/packages/types/src/__tests__/object-grid-export-options-refusal-7762.test.ts +++ b/ui/packages/types/src/__tests__/object-grid-export-options-refusal-7762.test.ts @@ -96,11 +96,12 @@ describe('ObjectGridSchema.exportOptions — the spec object arm, by reference ( // Every identity assertion below reads through `specObjectArm`. A spec refactor that // moved the shape would otherwise turn each of them into a comparison against // `undefined`, and `toBe(undefined) === toBe(undefined)` would pass. + // Floors and named members, not the spec's arm and key counts (#26238): + // the identity case below compares the full key sets, derived on both sides. expect(specExportOptions).toBeDefined(); - expect(specUnion.options).toHaveLength(2); + expect(specUnion.options?.length).toBeGreaterThan(0); expect(specObjectArm).toBeDefined(); - expect(Object.keys(specObjectArm?.shape ?? {}).sort()) - .toEqual(['fileNamePrefix', 'formats', 'includeHeaders', 'maxRecords', 'streaming']); + expect(Object.keys(specObjectArm?.shape ?? {})).toEqual(expect.arrayContaining(['formats', 'maxRecords'])); }); it('is a member of the mirror shape (membership cannot be read off acceptance under passthrough)', () => { diff --git a/ui/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts b/ui/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts index 4edea928edd..8c00c5eab86 100644 --- a/ui/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts +++ b/ui/packages/types/src/__tests__/object-view-unmirrored-keys-7779.test.ts @@ -1784,18 +1784,21 @@ function foldFlagMap(): Record { } describe('objectui#7924 — the unread members, in the FOUR buckets the ruling was written in plus the server-enforced fifth (objectui#12341) and the retired-ahead-of-the-pin sixth (objectui#13118), derived against the installed protocol (bucket ① empty since objectui#10758)', () => { - it('the protocol declares 51 keys for a named list view, and the instrument that reads them fires', () => { + it('the protocol declares every pinned key for a named list view, and the instrument that reads them fires', () => { const keys = protocolListViewKeys(); - expect(keys).toEqual([...PROTOCOL_LIST_VIEW_KEYS]); - expect(keys).toHaveLength(51); + // Membership, not equality or a count (#26238): a key the protocol adds is + // judged by the narrower-than-protocol zero below, derived on both sides. + for (const key of PROTOCOL_LIST_VIEW_KEYS) expect(keys).toContain(key); // Firing control + its negative twin, on the same query that produces every // "the protocol declares this nowhere" zero below. expect(keys).toContain(NAMED_VIEW_READ_CONTROL); expect(keys).not.toContain(NAMED_VIEW_ABSENT_CONTROL); }); - it('eight of the 51 are RETIREMENT TOMBSTONES, refused BY NAME — so objectui not declaring them LIVE is agreement, not narrowness', () => { - expect(protocolRetiredKeys()).toEqual([...PROTOCOL_RETIRED_KEYS]); + it('the eight pinned RETIREMENT TOMBSTONES are refused BY NAME — so objectui not declaring them LIVE is agreement, not narrowness', () => { + // Membership (#26238): a retirement the protocol adds later is judged by + // the derived narrower-than-protocol zero below. + for (const key of PROTOCOL_RETIRED_KEYS) expect(protocolRetiredKeys()).toContain(key); const base = { label: 'Directory', columns: ['name', 'email'] }; // The accept leg first: without it every refusal below is a schema that // refuses everything rather than a reading of these eight keys. @@ -1825,8 +1828,8 @@ describe('objectui#7924 — the unread members, in the FOUR buckets the ruling w it('objectui declares every LIVE protocol key on this surface — the narrower-than-protocol direction is EMPTY (inverted from "but the one pending its ruling" at objectui#12341)', () => { const declared = new Set(namedListViewMembers().names); const live = protocolListViewKeys().filter((k) => !protocolRetiredKeys().includes(k)); - // 43 since objectui#13261 took the retirement of `filterableFields`. - expect(live).toHaveLength(43); + // Non-vacuity floor; the live count is the protocol's, not this file's (#26238). + expect(live.length).toBeGreaterThan(0); const missing = live.filter((k) => !declared.has(k)); // ⭐ objectui#8979 measured SEVENTEEN live protocol keys missing here; the // objectui#8980 ruling declared them (PR #9534) and this is the zero that diff --git a/ui/packages/types/src/__tests__/record-highlights-fields-icon-9280.test.ts b/ui/packages/types/src/__tests__/record-highlights-fields-icon-9280.test.ts index 15ffee85397..ce759dc1d7a 100644 --- a/ui/packages/types/src/__tests__/record-highlights-fields-icon-9280.test.ts +++ b/ui/packages/types/src/__tests__/record-highlights-fields-icon-9280.test.ts @@ -181,14 +181,17 @@ function specEntryArm(): { keys: string[]; strict: boolean } { } describe('objectui#9280 — record:highlights `fields[]` entry against the installed spec', () => { - it('PREMISE: the contract declares exactly four entry keys and refuses a fifth', () => { + it('PREMISE: the contract declares the entry keys this face reads, not `icon`, and refuses an undeclared one', () => { // Read off the schema object rather than transcribed, so a spec that adds // or drops a key fails HERE first — before the pins above start asserting // a shape the contract no longer has. ⭐ If this ever reports `icon` among // the keys, the contract WIDENED and this whole retirement is reversed by // a new card, not by loosening the assertions below. const arm = specEntryArm(); - expect(arm.keys).toEqual(['label', 'name', 'readonly', 'type']); + // By name (#26238): the keys the entry face declares are present and the + // retired `icon` is absent; an unrelated key the spec adds moves neither. + expect(arm.keys).toEqual(expect.arrayContaining(['label', 'name', 'readonly', 'type'])); + expect(arm.keys).not.toContain('icon'); // The load-bearing half. A non-strict arm would STRIP `icon` silently // instead of refusing, which is a different defect with a different repair. diff --git a/ui/packages/types/src/__tests__/record-highlights-layout-9187.test.ts b/ui/packages/types/src/__tests__/record-highlights-layout-9187.test.ts index 93bc4a1d61d..ee8166afc0d 100644 --- a/ui/packages/types/src/__tests__/record-highlights-layout-9187.test.ts +++ b/ui/packages/types/src/__tests__/record-highlights-layout-9187.test.ts @@ -132,28 +132,16 @@ const nonsenseRefused: RecordHighlightsComponentProps = { }; describe('objectui#9187 — record:highlights `layout` against the installed spec', () => { - it('PREMISE: the contract declares exactly two members, read off the live schema', () => { + it('PREMISE: the contract declares both layout members, read off the live schema', () => { // Read off the schema object rather than transcribed, so a spec that adds // or drops a member fails HERE first — before the pins above start // asserting a shape the contract no longer has. const layout = RecordHighlightsProps.def.shape.layout; const entries = Object.keys(layout.def.innerType.def.entries).sort(); - expect(entries).toEqual(['horizontal', 'vertical']); - // `@objectstack/spec` 17.5.0 added the record-block security members - // (`requiredPermissions`, `redactFields`, `enforceFieldSecurity`) beside - // `layout` (objectui#11073). `layout`'s closed set above did not move, so - // every pin in this file still describes the contract as it stands. The - // spec after 18.0.0-next.3 added `emptyState` to the bound record blocks - // (objectstack#24270), beside `layout` again (objectui#12971). - expect(Object.keys(RecordHighlightsProps.def.shape).sort()).toEqual([ - 'aria', - 'emptyState', - 'enforceFieldSecurity', - 'fields', - 'layout', - 'redactFields', - 'requiredPermissions', - ]); + expect(entries).toEqual(expect.arrayContaining(['horizontal', 'vertical'])); + // The block's props by name: the exact key list this used to pin was + // re-pointed twice for members unrelated to `layout` (#26238). + expect(Object.keys(RecordHighlightsProps.def.shape)).toEqual(expect.arrayContaining(['fields', 'layout'])); }); it("PREMISE: `grid` is refused with `invalid_value` at `layout` — the card's repro", () => { diff --git a/ui/packages/types/src/__tests__/related-list-add-picker-filter-mirror-9964.test.ts b/ui/packages/types/src/__tests__/related-list-add-picker-filter-mirror-9964.test.ts index 8909e6c1dc2..7aee35782e6 100644 --- a/ui/packages/types/src/__tests__/related-list-add-picker-filter-mirror-9964.test.ts +++ b/ui/packages/types/src/__tests__/related-list-add-picker-filter-mirror-9964.test.ts @@ -237,7 +237,8 @@ describe('objectui#9964 — the mirror declares the contract’s rule array', () // Read off the artifact in the same run: an empty or moved population would // make every assertion below vacuous. const picker = RecordRelatedListProps.shape.add.unwrap().shape.picker; - expect(Object.keys(picker.shape).sort()).toEqual(['filter', 'labelField', 'object', 'valueField']); + // By name (#26238): `filter` is this pin's subject. + expect(Object.keys(picker.shape)).toEqual(expect.arrayContaining(['filter', 'object'])); expect(picker.shape.filter.safeParse([{ field: 'active', operator: 'equals', value: true }]).success).toBe(true); // …and it is not an everything-set, which would make the refusals unfalsifiable. expect(picker.shape.filter.safeParse({ active: true }).success).toBe(false); diff --git a/ui/packages/types/src/__tests__/select-option-form-view-boundary-12736.test.ts b/ui/packages/types/src/__tests__/select-option-form-view-boundary-12736.test.ts index 7f423dd2310..46b225f74ed 100644 --- a/ui/packages/types/src/__tests__/select-option-form-view-boundary-12736.test.ts +++ b/ui/packages/types/src/__tests__/select-option-form-view-boundary-12736.test.ts @@ -184,8 +184,9 @@ describe('the form option stops at the spec form-view option boundary (objectui# }); } - it('(b) the derived difference of the two spec shapes is `default` and `kind`', () => { - expect(objectFieldOnlyKeys).toEqual(['default', 'kind']); + it('(b) the derived difference of the two spec shapes holds `default` and `kind`', () => { + // Membership (#26238): these two are the boundary this file is about. + expect(objectFieldOnlyKeys).toEqual(expect.arrayContaining(['default', 'kind'])); }); it('(b) the twin states the form-view keys, the two UI-only keys, and the refusals — nothing else', () => { diff --git a/ui/packages/types/src/__tests__/select-option-spec-extension-7014.test.ts b/ui/packages/types/src/__tests__/select-option-spec-extension-7014.test.ts index c9ea01e3b55..e821f47a0f9 100644 --- a/ui/packages/types/src/__tests__/select-option-spec-extension-7014.test.ts +++ b/ui/packages/types/src/__tests__/select-option-spec-extension-7014.test.ts @@ -86,20 +86,12 @@ const issueAtPath = (result: { success: boolean; error?: { issues: readonly any[ : result.error!.issues.find((i) => (i.path ?? []).at(-1) === key); describe('spec SelectOptionSchema — `description` is inside the vocabulary, `icon`/`disabled` are not', () => { - it('declares exactly the seven keys 18.0.0-next.3 leaves it with', () => { - // If this list changes again, the comments in field-types.ts and - // select-option.ts must be re-pointed with it rather than left stale. - // 17.3.0 added `description`; 18.0.0-next.3 added `kind`, the option's - // lifecycle category (objectstack `adad73f`). - expect(SPEC_OPTION_KEYS).toEqual([ - 'color', - 'default', - 'description', - 'kind', - 'label', - 'value', - 'visibleWhen', - ]); + it('declares `description` and neither objectui-only key', () => { + // By name, not as the spec's whole key list (#26238): 17.3.0 added + // `description`, the key this file is about, and `icon` / `disabled` are + // refused by name below. + expect(SPEC_OPTION_KEYS).toEqual(expect.arrayContaining(['description', 'label', 'value'])); + for (const key of ['icon', 'disabled']) expect(SPEC_OPTION_KEYS).not.toContain(key); }); it('accepts the control option', () => {