From f9ff4ee1d5779accc8396be9a189cfa955e2b05f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 20:19:50 +0000 Subject: [PATCH 1/2] test(plugin-view): record the 27 object-view keys ruled host surface (#5097) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ObjectView`'s `renderListView` delegation branch reads 31 keys off the object-view node through `(schema as any)`; 27 are not declared members of `ObjectViewSchema`. The maintainer ruling of 2026-08-18 on objectui#5097 (verbatim 「同意」) ruled them HOST-COMPOSITION surface, exempted with reasons — the branch is entered only when a host supplies the prop, and the registered renderer never does, so the schema-registration path documented to authors cannot reach them. This records that decision where the next census reads it: an exemption block holding the 27 names, the ruling, both in-tree supplier `file:line`s and the contract's (non-)verdict; a `#region` fence around the branch; and a pin that re-derives the read set from that fence, so a read added or removed without touching the list fails by name. Nothing is declared and no read is removed. The structural follow-through (typing the block as an explicit host-side prop contract) stays with the objectui#5043 family track. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE --- .../object-view-host-surface-exemption.md | 14 + packages/plugin-view/src/ObjectView.tsx | 154 +++++++++ .../__tests__/objectViewHostSurface.test.tsx | 326 ++++++++++++++++++ 3 files changed, 494 insertions(+) create mode 100644 .changeset/object-view-host-surface-exemption.md create mode 100644 packages/plugin-view/src/__tests__/objectViewHostSurface.test.tsx diff --git a/.changeset/object-view-host-surface-exemption.md b/.changeset/object-view-host-surface-exemption.md new file mode 100644 index 000000000..767fee584 --- /dev/null +++ b/.changeset/object-view-host-surface-exemption.md @@ -0,0 +1,14 @@ +--- +--- + +Records the maintainer ruling of 2026-08-18 on objectui#5097: the 27 keys `ObjectView`'s +`renderListView` delegation branch reads off the object-view node that are not declared +members of `ObjectViewSchema` are HOST-COMPOSITION surface, exempted with reasons, and are +not to be taught as schema keys. + +Nothing is released. Every edit to `packages/plugin-view/src/ObjectView.tsx` is a comment +or one of two new module-level constants that hold the exemption list for the test to +derive against; the constants are not re-exported from the package entry point, so the +published API is unchanged, no read was added or removed, and no runtime path is touched +(`git diff` on that file: 154 insertions, 0 deletions). The rest of the change is a new +test file under `src/__tests__/`. diff --git a/packages/plugin-view/src/ObjectView.tsx b/packages/plugin-view/src/ObjectView.tsx index e29be6080..d6a7c9b58 100644 --- a/packages/plugin-view/src/ObjectView.tsx +++ b/packages/plugin-view/src/ObjectView.tsx @@ -278,6 +278,129 @@ export interface ObjectViewProps { type FormMode = 'create' | 'edit' | 'view'; +/** + * HOST-COMPOSITION SURFACE on the `object-view` node — the keys the + * `renderListView` delegation branch reads off the node that are deliberately + * NOT declared members of `ObjectViewSchema`, and ⛔ not to be taught as + * schema keys anywhere in the docs (objectui#5097). + * + * ## The verdict + * + * The delegation branch in `renderContent` below reads 31 distinct keys off + * the object-view node through `(schema as any).K` and forwards them to the + * host's list renderer. Four of them — `data`, `navigation`, + * `searchableFields`, `filterableFields`, listed in + * {@link OBJECT_VIEW_DECLARED_FORWARDED_KEYS} — are declared members of + * `ObjectViewSchema`. The other 27, listed here, are not, and stay that way: + * they are HOST-COMPOSITION surface, not authored surface. This constant is + * their single home; the branch is fenced by `#region` markers so the pin in + * `src/__tests__/objectViewHostSurface.test.tsx` fails BY NAME when a read is + * added or removed without touching this list. + * + * ## The ruling that made it deliberate + * + * Maintainer, 2026-08-18, on objectui#5097, verbatim 「同意」: the 27 keys are + * HOST surface, exempted with reasons — not authored surface. Basis: measured + * reachability. The delegation branch is entered ONLY when a host passes the + * `renderListView` prop; the registered renderer does not pass it; so the + * schema-registration path documented to authors cannot reach these keys at + * all, and declaring them on `ObjectViewSchema` would promise authors a + * surface that does nothing on their path. + * + * ⛔ Do not declare these on `ObjectViewSchema`, and ⛔ do not delete a read as + * a tidy-up — deleting a read is what silently blanks a stored app-shell + * document, and it is exactly what a reader of "not authored surface" is most + * likely to think is the clean finish. The structural follow-through (typing + * this block as an explicit host-side prop contract, so host surface and + * authored surface are separated in types, and retiring the `(schema as any)` + * reads) is owned by the objectui#5043 family track, not by this exemption. + * + * ## Who supplies the prop — what makes "host surface" checkable + * + * Every in-tree non-test supplier lives in `@object-ui/app-shell`, and there + * are TWO of them. Re-measured on `main` at `e03dfa5ea`; both were already + * present at the ruling's base `9fbb9b52f`, which named only the first: + * + * - `packages/app-shell/src/views/ObjectView.tsx:1718` — the React host + * defines the callback; it is passed at `:2481` and `:2524`. + * - `packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx:2575` + * — the Studio design surface, as `renderListView={renderStudioGridList}`. + * + * The registered renderer is `ObjectViewRenderer` (`./index.tsx:58`), which + * renders `ObjectView` with `schema` and `dataSource` only and passes no + * `renderListView`. It is registered twice — under `object-view` + * (`./index.tsx:66`) and under the alias `view` (`./index.tsx:100`) — and + * neither registration can reach this branch. + * + * ## What the contract says about these keys + * + * Nothing at all, and that is the shape of the answer here — unlike the + * sibling `object-grid` exemption (objectui#5091, PR #5241), where the spec + * answered `unrecognized_keys` by name. `@objectstack/spec`'s + * `ComponentPropsMap` carries no `object-view` entry, so the repo-wide + * `registry-inputs-spec-parity` gate — which derives its expectations FROM + * `ComponentPropsMap` — never covers this node in either direction and is owed + * nothing here. The node's published authoring surface is the registry + * `inputs` list at `./index.tsx:71-86` (15 names; the alias `view` declares no + * `inputs` at all), and none of the 27 appears on either registration. + * + * ## The one asymmetry — `conditionalFormatting` + * + * 26 of the 27 are read ONLY inside the host-only branch. `conditionalFormatting` + * has a SECOND read site, in `generateViewSchema`'s kanban branch (see + * `kanbanConditionalFormatting`), which IS reachable through the registered + * renderer. For that one key the ruling's "the authored path cannot reach it" + * basis is therefore narrower than for its 26 neighbours. Recorded here rather + * than acted on: the exemption preserves the status quo either way (nothing + * declared, nothing removed), and whether that second read makes + * `conditionalFormatting` authored surface on the kanban path is a fresh + * contract question, filed separately as objectui#5248. + */ +export const OBJECT_VIEW_HOST_COMPOSITION_KEYS = [ + 'addDeleteRecordsInline', + 'addRecord', + 'addRecordViaForm', + 'allowExport', + 'allowPrinting', + 'aria', + 'bulkActions', + 'clickIntoRecordDetails', + 'collapseAllByDefault', + 'color', + 'compactToolbar', + 'conditionalFormatting', + 'emptyState', + 'fieldTextColor', + 'hiddenFields', + 'inlineEdit', + 'pagination', + 'prefixField', + 'resizable', + 'rowActionDefs', + 'rowActions', + 'selection', + 'sharing', + 'showDescription', + 'showRecordCount', + 'userFilters', + 'wrapHeaders', +] as const; + +/** + * The other four keys the same branch reads through `(schema as any)`: these + * ARE declared members of `ObjectViewSchema` (`data` via `BaseSchema`), so + * they are authored surface and are NOT part of the objectui#5097 exemption. + * They are listed so the pin can assert the split, not just the exempt half — + * a read that moves from this list into the one above is a key losing its + * declaration, and must fail loudly. + */ +export const OBJECT_VIEW_DECLARED_FORWARDED_KEYS = [ + 'data', + 'filterableFields', + 'navigation', + 'searchableFields', +] as const; + /** * ObjectView Component * @@ -762,6 +885,17 @@ export const ObjectView: React.FC = ({ // `options.kanban.conditionalFormatting` wins, then the view-level, then // the schema-level rule — matching how the grid branch resolves it. // Previously the top-level rule was dropped for kanban entirely. + // + // ⚠️ objectui#5097 — this is the ONLY read of one of the 27 + // host-composition keys that sits OUTSIDE the `#region` fence below, + // and the only one on a path the REGISTERED renderer can reach: + // `generateViewSchema` runs precisely when no host supplied + // `renderListView`. So for `conditionalFormatting` alone, the + // exemption's "the authored path cannot reach it" basis is narrower + // than for its 26 neighbours. Recorded, not acted on — see + // `OBJECT_VIEW_HOST_COMPOSITION_KEYS` ("The one asymmetry") and + // objectui#5248, which owns the contract question. ⛔ Do not delete + // this read, and ⛔ do not move it inside the fence to quiet the pin. const kanbanConditionalFormatting = kanbanCfg.conditionalFormatting ?? activeView?.conditionalFormatting ?? @@ -1029,6 +1163,25 @@ export const ObjectView: React.FC = ({ const key = `${schema.objectName}-${activeNamedView || activeView?.id || 'default'}-${currentViewType}-${refreshKey}`; // If a custom renderListView is provided, use it + // #region object-view HOST-COMPOSITION SURFACE (objectui#5097) + // + // 31 of the values assembled below are read off the object-view node with + // `(schema as any).K`. Four are declared members of `ObjectViewSchema`; + // the other 27 are NOT, deliberately: the maintainer ruling of 2026-08-18 + // on objectui#5097 (verbatim 「同意」) ruled them HOST-COMPOSITION surface, + // not authored surface, because this branch is entered only when a HOST + // passes `renderListView` and the registered renderer never does. + // + // ⛔ Do not declare these keys on `ObjectViewSchema`, ⛔ do not teach them + // as schema keys in the docs, and ⛔ do not delete a read as a tidy-up — a + // deleted read silently blanks a stored app-shell document. + // + // The list, the two supplier `file:line`s, the contract's (non-)verdict and + // the `conditionalFormatting` asymmetry live with + // `OBJECT_VIEW_HOST_COMPOSITION_KEYS` at the top of this file. The `#region` + // fence is load-bearing: `src/__tests__/objectViewHostSurface.test.tsx` + // re-derives the read set from between these two markers, so adding or + // removing a read here fails that pin BY NAME. if (renderListView) { return renderListView({ schema: { @@ -1108,6 +1261,7 @@ export const ObjectView: React.FC = ({ onAddRecord: handleCreate, }); } + // #endregion object-view HOST-COMPOSITION SURFACE (objectui#5097) // For non-grid views, use SchemaRenderer with generated schema if (currentViewType !== 'grid') { diff --git a/packages/plugin-view/src/__tests__/objectViewHostSurface.test.tsx b/packages/plugin-view/src/__tests__/objectViewHostSurface.test.tsx new file mode 100644 index 000000000..1d58aa717 --- /dev/null +++ b/packages/plugin-view/src/__tests__/objectViewHostSurface.test.tsx @@ -0,0 +1,326 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * `object-view` — the 27 keys ruled HOST-COMPOSITION SURFACE stay undeclared, + * and stay read (objectui#5097). + * + * ## The card, and the ruling + * + * objectui#5097 measured 31 distinct keys that `ObjectView`'s `renderListView` + * delegation branch reads off the object-view node through `(schema as any).K` + * and forwards to the host's list renderer. Four are declared members of + * `ObjectViewSchema`; 27 are not. + * + * The maintainer ruled on 2026-08-18 (verbatim 「同意」) that the 27 are HOST + * surface, exempted with reasons — not authored surface — on a MEASURED + * REACHABILITY basis: the branch runs only when a host passes `renderListView`, + * and the registered renderer never does, so the schema-registration path + * documented to authors cannot reach these keys at all. Declaring them would + * promise authors a surface that does nothing on their path. + * + * This file is where that word "deliberate" is said to the tooling, so the next + * census re-files the card only if something really changed. + * + * ## Why this pins the PROPERTY, not the enumeration + * + * The sibling exemption for `object-grid` (objectui#5091, PR #5241) could + * afford four assertions per key: there were three keys. Twenty-seven of them + * written out four times over would be noise nobody re-reads, and noise is how + * a stale list survives. So the three things that actually carry the ruling are + * pinned as properties instead: + * + * 1. THE SET IS WHAT THE DOC SAYS IT IS. The key set is re-derived from the + * `#region` fence in `ObjectView.tsx` at test time and compared with + * `OBJECT_VIEW_HOST_COMPOSITION_KEYS`, so adding or removing a read + * without touching the list fails BY NAME. A hand-copied list in a test + * would only ever agree with itself. + * 2. THE RULING'S BASIS HOLDS. The registered renderer passes no + * `renderListView`, so the branch is unreachable from the authored path. + * If a future change starts supplying it, 27 keys are silently promoted to + * the authored path — that must fail loudly, here, rather than be found by + * the next census. + * 3. THE READS STILL HAPPEN. Deleting a read is the one move that silently + * blanks a stored app-shell document, and it is exactly what a reader of + * "not authored surface" is most likely to think is the tidy finish. + * + * ## What is NOT owed here, and why + * + * PR #5241's assertions 2 and 3 — "the spec rejects the key by name", "the real + * validator answers `unknown-prop`" — do not transfer, because + * `@objectstack/spec` carries no `object-view` entry in `ComponentPropsMap` at + * all. That absence is itself pinned below: it is what makes the repo-wide + * `registry-inputs-spec-parity` gate (which derives its expectations FROM + * `ComponentPropsMap`) inapplicable to this node in either direction, so the + * exemption owes that gate nothing. If the spec ever starts modelling + * `object-view`, this exemption needs re-reading, and the pin says so. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { existsSync, readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import React from 'react'; +import { render } from '@testing-library/react'; +import { ComponentRegistry } from '@object-ui/core'; +import { ComponentPropsMap } from '@objectstack/spec/ui'; +import type { ObjectViewSchema } from '@object-ui/types'; + +import { + ObjectView, + OBJECT_VIEW_HOST_COMPOSITION_KEYS, + OBJECT_VIEW_DECLARED_FORWARDED_KEYS, +} from '../ObjectView'; +// Module scope, not a hook: this import IS the registration. +import '../index'; + +// The source is read from disk rather than imported: the claim being pinned is +// about the READS in the file, and `import.meta.url` is not a file URL under +// this runner. Both candidates are covered so the pin holds whether vitest is +// invoked from the repo root or from the package. +const SOURCE_PATH = [ + resolve(process.cwd(), 'src/ObjectView.tsx'), + resolve(process.cwd(), 'packages/plugin-view/src/ObjectView.tsx'), +].find((candidate) => existsSync(candidate)); +if (!SOURCE_PATH) throw new Error('objectViewHostSurface pin cannot locate plugin-view/src/ObjectView.tsx'); +const SOURCE = readFileSync(SOURCE_PATH, 'utf8'); +const REGION_OPEN = '// #region object-view HOST-COMPOSITION SURFACE (objectui#5097)'; +const REGION_CLOSE = '// #endregion object-view HOST-COMPOSITION SURFACE (objectui#5097)'; + +/** + * Comments removed before any read is derived. Not defensive tidiness: the + * exemption block deliberately SPELLS the cast form out in prose to explain + * itself, and the first run of this pin duly reported a host key named `K`. + * A derivation that reads its own documentation is not a derivation. + * Line comments are cut only where `//` is not preceded by `:`, so a URL in a + * string literal cannot swallow the rest of its line. + */ +const stripComments = (code: string): string => + code + .replace(/\/\*[\s\S]*?\*\//g, '') + .split('\n') + .map((line) => line.replace(/(^|[^:])\/\/.*$/, '$1')) + .join('\n'); + +/** Every cast read of a key off `schema` in a slice of CODE, distinct, sorted. */ +const castReadsIn = (slice: string): string[] => + [ + ...new Set( + [...stripComments(slice).matchAll(/\(schema as any\)\.([A-Za-z_$][\w$]*)/g)].map((m) => m[1]), + ), + ].sort(); + +const regionSlice = (): string => { + const start = SOURCE.indexOf(REGION_OPEN); + const end = SOURCE.indexOf(REGION_CLOSE); + expect( + start >= 0 && end > start, + 'The `#region object-view HOST-COMPOSITION SURFACE` fence in ObjectView.tsx is gone or reordered.\n' + + 'It is load-bearing, not decoration: it is how this pin knows which reads the objectui#5097\n' + + 'exemption covers. Restore it rather than deleting this test.', + ).toBe(true); + return SOURCE.slice(start, end); +}; + +// --------------------------------------------------------------------------- +// 1. The set is what the doc says it is. +// --------------------------------------------------------------------------- + +describe('the forwarded key set equals the documented exemption (objectui#5097)', () => { + const exempt = [...OBJECT_VIEW_HOST_COMPOSITION_KEYS].sort(); + const declared = [...OBJECT_VIEW_DECLARED_FORWARDED_KEYS].sort(); + + it('the fence reads exactly the 27 exempt keys plus the 4 declared ones', () => { + expect(castReadsIn(regionSlice())).toEqual([...exempt, ...declared].sort()); + }); + + it('subtracting the declared members leaves exactly the exempt list — by name', () => { + const read = castReadsIn(regionSlice()); + expect( + read.filter((k) => !declared.includes(k)), + 'A `(schema as any)` read inside the objectui#5097 fence was added or removed without\n' + + 'updating OBJECT_VIEW_HOST_COMPOSITION_KEYS. That list is the exemption record the next\n' + + 'census reads; a read it does not name is an undocumented host key, and a name with no\n' + + 'read is a stale exemption. Update the list AND its comment block, in the same change.', + ).toEqual(exempt); + }); + + it('the exemption covers 27 keys, the count the ruling was made on', () => { + expect(OBJECT_VIEW_HOST_COMPOSITION_KEYS).toHaveLength(27); + expect(OBJECT_VIEW_DECLARED_FORWARDED_KEYS).toHaveLength(4); + // Disjoint: a key cannot be both declared surface and exempt host surface. + expect(exempt.filter((k) => declared.includes(k))).toEqual([]); + }); + + it('`conditionalFormatting` is the ONLY exempt key also read outside the fence', () => { + // The measured asymmetry recorded on objectui#5248: `generateViewSchema`'s + // kanban branch reads it too, and that branch runs exactly when no host + // supplied `renderListView` — i.e. on the path the registered renderer + // takes. For this one key the ruling's "the authored path cannot reach it" + // basis is narrower than for its 26 neighbours. Pinned so the exception + // cannot be lost, and so a SECOND such read cannot appear unnoticed. + const outside = castReadsIn(SOURCE.replace(regionSlice(), '')); + expect( + outside, + 'A host-composition key is now read outside the objectui#5097 fence. Reads out there are on\n' + + 'the AUTHOR-reachable path, which is the basis the exemption rests on — so this is a\n' + + 'contract change, not a refactor. See objectui#5248 before widening this list.', + ).toEqual(['conditionalFormatting']); + }); +}); + +// --------------------------------------------------------------------------- +// 2. The ruling's basis: the registered renderer cannot reach the branch. +// --------------------------------------------------------------------------- + +describe("the registered renderer cannot reach the delegation branch — the ruling's basis (objectui#5097)", () => { + const registered = ComponentRegistry.get('object-view') as React.FC<{ schema: unknown }>; + + it('is registered, and the `view` alias is the same renderer', () => { + expect(registered).toBeTypeOf('function'); + // One renderer under two tags is two chances to disagree with itself. + expect(ComponentRegistry.get('view')).toBe(registered); + }); + + it('passes ObjectView no `renderListView`, so the branch never runs', () => { + let passed: Record | null = null; + let renderedType: unknown = null; + // The renderer is called as a plain function inside a probe component: it + // holds exactly one hook (`useContext`), the call happens once, and the + // element it returns is inspected and then discarded rather than mounted — + // so this asserts what the registration PASSES without paying for an + // ObjectView mount or a data fetch. + const Probe: React.FC = () => { + const element = registered({ + schema: { type: 'object-view', objectName: 'task' }, + }) as React.ReactElement; + renderedType = element.type; + passed = element.props as Record; + return null; + }; + render(); + + expect(renderedType).toBe(ObjectView); + expect( + passed, + 'The registered `object-view` renderer now supplies `renderListView`. That single prop is the\n' + + "whole basis of the 2026-08-18 ruling on objectui#5097: it promotes 27 keys from host\n" + + 'composition surface to the AUTHORED path, where nothing declares them and tsc cannot see\n' + + 'them. Re-open objectui#5097 before landing this.', + ).not.toHaveProperty('renderListView'); + }); + + it('publishes none of the 27 on either tag, while publishing the declared control', () => { + const inputsOf = (type: string): string[] => + ((ComponentRegistry.getConfig(type) as { inputs?: Array<{ name: string }> } | undefined)?.inputs ?? []) + .map((i) => i.name); + + for (const tag of ['object-view', 'view']) { + for (const key of OBJECT_VIEW_HOST_COMPOSITION_KEYS) { + expect( + inputsOf(tag), + `\`${tag}\` now publishes \`${key}\` as authoring surface — the objectui#5097 ruling keeps it` + + ' off the authored path.', + ).not.toContain(key); + } + } + // The control: without it, "publishes none of them" would pass just as + // happily against a registration that publishes nothing at all. + expect(inputsOf('object-view')).toEqual( + expect.arrayContaining(['navigation', 'searchableFields', 'filterableFields']), + ); + }); + + it('the spec models no `object-view`, so the parity gate is owed nothing', () => { + expect( + Object.keys(ComponentPropsMap), + '`@objectstack/spec` now models `object-view`. The objectui#5097 exemption was ruled on the\n' + + 'basis that the contract has no verdict on this node at all — with a props schema present,\n' + + 'the repo-wide registry-inputs-spec-parity gate starts covering it and the 27 keys need\n' + + 're-reading against the spec.', + ).not.toContain('object-view'); + }); +}); + +// --------------------------------------------------------------------------- +// 3. The reads themselves — unchanged by the ruling, and the half a reader of +// "not authored surface" is most likely to delete. +// --------------------------------------------------------------------------- + +/** One plausible sentinel per forwarded key, distinct enough to trace. */ +const SENTINELS: Record = { + addDeleteRecordsInline: true, + addRecord: { enabled: true }, + addRecordViaForm: true, + allowExport: true, + allowPrinting: true, + aria: { label: 'Tasks' }, + bulkActions: ['delete'], + clickIntoRecordDetails: true, + collapseAllByDefault: true, + color: '#112233', + compactToolbar: true, + conditionalFormatting: [{ field: 'stage', operator: 'eq', value: 'won', color: '#ff0000' }], + emptyState: { title: 'Nothing here yet' }, + fieldTextColor: { name: '#0000ff' }, + hiddenFields: ['secret'], + inlineEdit: true, + pagination: { pageSize: 25 }, + prefixField: 'name', + resizable: true, + rowActionDefs: [{ name: 'archive', label: 'Archive' }], + rowActions: ['edit'], + selection: { mode: 'multiple' }, + sharing: { enabled: true }, + showDescription: true, + showRecordCount: true, + userFilters: [{ field: 'owner', operator: 'eq', value: 'me' }], + wrapHeaders: true, + // The four declared members ride the same branch. + data: { api: '/api/tasks' }, + navigation: { mode: 'drawer' }, + searchableFields: ['name'], + filterableFields: ['stage'], +}; + +describe('every forwarded key still reaches the host renderer (objectui#5097)', () => { + const forwardedSchema = (): Record => { + const seen: Array> = []; + const ds = { + find: vi.fn().mockResolvedValue({ data: [], total: 0 }), + findOne: vi.fn(), + create: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + getObjectSchema: vi.fn().mockResolvedValue({ name: 'task', fields: {} }), + }; + render( + }) => { + seen.push(s); + return
; + }} + />, + ); + expect(seen.length).toBeGreaterThan(0); + return seen[0]; + }; + + it('forwards all 31 reads — 27 exempt plus 4 declared — by name', () => { + const forwarded = forwardedSchema(); + const keys = [...OBJECT_VIEW_HOST_COMPOSITION_KEYS, ...OBJECT_VIEW_DECLARED_FORWARDED_KEYS]; + const got = Object.fromEntries(keys.map((k) => [k, forwarded[k]])); + const want = Object.fromEntries(keys.map((k) => [k, SENTINELS[k]])); + expect( + got, + 'A key on the objectui#5097 exemption list stopped reaching the host list renderer. The ruling\n' + + 'kept every read: "not authored surface" is a statement about who WRITES the key, never a\n' + + 'licence to stop reading it. A dropped read silently blanks that setting in every stored\n' + + 'app-shell document that carries it.', + ).toEqual(want); + }); +}); From 3ef093d4a0d08523248bb9127d7ee872cf7b3441 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 20:27:53 +0000 Subject: [PATCH 2/2] test(plugin-view): widen the exemption pin's key arrays to string[] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `as const` narrowed both constants to literal unions, so comparing them with a set derived from the source failed tsc. The derivation is the authority here, not the literal type — widening at the comparison keeps it that way. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE --- .../src/__tests__/objectViewHostSurface.test.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/plugin-view/src/__tests__/objectViewHostSurface.test.tsx b/packages/plugin-view/src/__tests__/objectViewHostSurface.test.tsx index 1d58aa717..69b40a499 100644 --- a/packages/plugin-view/src/__tests__/objectViewHostSurface.test.tsx +++ b/packages/plugin-view/src/__tests__/objectViewHostSurface.test.tsx @@ -129,8 +129,12 @@ const regionSlice = (): string => { // --------------------------------------------------------------------------- describe('the forwarded key set equals the documented exemption (objectui#5097)', () => { - const exempt = [...OBJECT_VIEW_HOST_COMPOSITION_KEYS].sort(); - const declared = [...OBJECT_VIEW_DECLARED_FORWARDED_KEYS].sort(); + // Widened to `string[]` deliberately: the constants are `as const`, and + // comparing them against a set DERIVED from the source is the whole point — + // narrowed literal types would make the compiler, not the file, the authority + // on which keys exist. + const exempt: string[] = [...OBJECT_VIEW_HOST_COMPOSITION_KEYS].sort(); + const declared: string[] = [...OBJECT_VIEW_DECLARED_FORWARDED_KEYS].sort(); it('the fence reads exactly the 27 exempt keys plus the 4 declared ones', () => { expect(castReadsIn(regionSlice())).toEqual([...exempt, ...declared].sort());