From 50214f30719b9431708cb341298e1c6cf91cf349 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 03:53:22 +0000 Subject: [PATCH 1/4] fix(plugin-detail): record:quick_actions.requiredPermissions publishes the contract's shared describe (objectui#10224) The registration described the key as "Hide the whole bar unless the user holds these permissions". The renderer reads the ADR-0066 capability set (hasCapabilities, objectui#10058), draws an insufficient-permissions notice in place of the bar, and fails open when capabilities are unreported. @objectstack/spec 17.5.0 carries one describe for this key on all four record blocks; objectui#8649 published it on the other three, and this input now publishes the same text, verbatim. The gate pin re-reads the installed describe every run, adds rows for the clauses it did not yet cover (read/update as capability names, fail-open with no provider and with an unreported systemPermissions), and its prose and test names now say what it asserts. The console parity file's MEMBER_PINS row and its slice-4 note stop describing the retired perms.can() read and a bar that is hidden. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude --- .../10224-quick-actions-permissions-text.md | 18 +++ .../registry-inputs-spec-parity.test.ts | 15 ++- packages/plugin-detail/src/index.tsx | 14 ++- ...-actions.requiredPermissions-gate.test.tsx | 105 ++++++++++++++++-- 4 files changed, 136 insertions(+), 16 deletions(-) create mode 100644 .changeset/10224-quick-actions-permissions-text.md diff --git a/.changeset/10224-quick-actions-permissions-text.md b/.changeset/10224-quick-actions-permissions-text.md new file mode 100644 index 0000000000..e46200f3b8 --- /dev/null +++ b/.changeset/10224-quick-actions-permissions-text.md @@ -0,0 +1,18 @@ +--- +'@object-ui/plugin-detail': patch +--- + +fix(plugin-detail): `record:quick_actions.requiredPermissions` publishes what the gate does — the contract's shared record-block describe, verbatim + +The `record:quick_actions` registration described `requiredPermissions` as "Hide +the whole bar unless the user holds these permissions". The renderer does not +hide the bar. It reads the ADR-0066 capability set, and when a declared +capability is missing it draws an insufficient-permissions notice where the bar +would be. When the client cannot resolve capabilities (no permission provider, +or a backend that does not report `systemPermissions`) it renders the bar. + +`@objectstack/spec` 17.5.0 gives this key one describe, shared with +`record:details`, `record:highlights` and `record:related_list`, and those three +already publish it. The quick-actions input now publishes the same text, so +`sdui.manifest.json` and the JSX authoring types carry it. No input name, type +or shape changes, and no rendering or gating behaviour changes. diff --git a/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts b/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts index a100d6f341..92b641473d 100644 --- a/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts +++ b/apps/console/src/__tests__/registry-inputs-spec-parity.test.ts @@ -2439,11 +2439,14 @@ const MULTI_KIND_MEMBER_CONTRACTS: Record = { // (the shape this card's own dispatch names, over an arbitrary four). Neither // key needed `NO_READ_SITE_TO_PIN`: `actionNames` drives a real // `useMetadataItem` lookup against the object's own `actions`, and -// `requiredPermissions` is a block-level gate (`required.every((p) => -// perms.can(objectName, p))`) that hides the whole bar before anything is -// drawn — a DIFFERENT mechanism from an `ActionDef`'s own per-action field of -// the same name, which a pre-existing fixture already exercised without -// touching this key at all. Measured over this file's own ledger, before and +// `requiredPermissions` is a block-level gate that puts an +// insufficient-permissions notice in place of the whole bar before any action +// is drawn. It reads the capability set (`perms.hasCapabilities(required)`) +// since objectui#10058; slice 4 found it reading `perms.can(objectName, p)` +// (sentence corrected by objectui#10224). It is a DIFFERENT mechanism from an +// `ActionDef`'s own per-action field of the same name, which a pre-existing +// fixture already exercised without touching this key at all. Measured over +// this file's own ledger, before and // after: 90 array/object-armed inputs, 40 pinned, 50 exempt -> 90, 42 pinned, // 48 exempt, and `MEMBER_PIN_EXEMPTION_CEILING` follows 50 -> 48 in the same // commit. One of the two pins PROMOTES a pre-existing file @@ -3036,7 +3039,7 @@ const MEMBER_PINS: Record = { }, 'record:quick_actions.requiredPermissions': { file: 'packages/plugin-detail/src/renderers/__tests__/record-quick-actions.requiredPermissions-gate.test.tsx', - pins: 'The BLOCK-LEVEL gate (`required.every((p) => perms.can(objectName, p))`) that hides the whole bar before any action is drawn — distinct from an `ActionDef`\'s own per-action `requiredPermissions`, which `record-quick-actions.declared-action-ids-7182.test.tsx`\'s `gated` fixture already covers. No key at all: the bar renders on the (empty) grant set, so the gate is provably driven by the key\'s PRESENCE. A single held permission gates as expected, and the discriminating row is a PARTIAL grant on a two-entry array — gated only when read as `.every` over the WHOLE array rather than its first element — with the all-granted case as that row\'s own positive control. New file: no existing test drove `schema.requiredPermissions` itself, only the unrelated per-action field of the same name (objectui#8071).', + pins: 'The BLOCK-LEVEL ADR-0066 capability gate, read through `perms.hasCapabilities(required)` (objectui#10058) — distinct from an `ActionDef`\'s own per-action `requiredPermissions`, which `record-quick-actions.declared-action-ids-7182.test.tsx`\'s `gated` fixture already covers. Members are CAPABILITY names and the bar needs ALL of them, driven through the real renderer under a REAL stock `MePermissionsProvider`: an unheld capability and an unrecognised one each put the insufficient-permissions notice in place of the whole bar with no action drawn, a held one renders the bar, an EMPTY array and an ABSENT key are no gate at all, and the discriminating row is a PARTIAL grant on a two-entry array — gated only when the members are read as `.every` over the WHOLE array rather than its first element — with the all-granted case as that row\'s own positive control. Discriminators pin that a member is a capability and not an object action (`allowRead` on the object does not open the gate, a held capability opens it with `allowRead: false`, the enum member `manage` is gated rather than resolved to the read bit, and `read` / `update` are gated despite `allowRead` / `allowEdit`), a detector pins that the object-permission path is never asked about a member, and the gate holds with no `objectName` in the record context. A client that cannot resolve capabilities fails open: the role-based `PermissionProvider`, no provider at all, and a stock provider whose backend omits `systemPermissions`. The same file pins the registration\'s description as the installed spec\'s shared record-block describe, verbatim, and as the text the three sibling record blocks publish (objectui#10224). New file at objectui#8071 slice 4: no existing test drove `schema.requiredPermissions` itself, only the unrelated per-action field of the same name.', }, 'record:related_list.actions': { file: 'packages/plugin-detail/src/__tests__/RecordRelatedListRenderer.authoredActions-11163.test.tsx', diff --git a/packages/plugin-detail/src/index.tsx b/packages/plugin-detail/src/index.tsx index 1c8caf2c6b..a11ca227c3 100644 --- a/packages/plugin-detail/src/index.tsx +++ b/packages/plugin-detail/src/index.tsx @@ -876,7 +876,19 @@ ComponentRegistry.register('quick_actions', RecordQuickActionsRenderer, { // Implementing the fallback would be a behaviour expansion and needs its own // card; pinned by `recordQuickActionsInputs.actionNamesFallback.test.tsx`. { name: 'actionNames', type: 'array', of: 'string', description: 'Action names to expose, in order — resolved from the actions declared on the object. With no names (and no host-supplied actions) nothing is looked up and the bar renders its empty placeholder' }, - { name: 'requiredPermissions', type: 'array', of: 'string', description: 'Hide the whole bar unless the user holds these permissions' }, + // The contract's shared record-block describe, verbatim (`@objectstack/spec` + // 17.5.0, objectstack#18159): `record:quick_actions` carries the one text + // `record:details`, `record:highlights` and `record:related_list` share, and + // objectui#8649 already publishes it on those three. This input used to read + // "Hide the whole bar unless the user holds these permissions", which is not + // what the renderer does: the gate reads the CAPABILITY set + // (`perms.hasCapabilities`, objectui#10058), puts a `role="status"` + // insufficient-permissions notice where the bar would be, and fails open when + // capabilities are unreported (objectui#10224). Each clause is pinned on this + // block by `record-quick-actions.requiredPermissions-gate.test.tsx`, which + // also re-reads the installed describe every run, so a spec that rewords it + // turns that file red rather than leaving this text to drift. + { name: 'requiredPermissions', type: 'array', of: 'string', description: '[ADR-0066] Capabilities the user must ALL hold — names that permission sets grant through `systemPermissions`, not object actions: `read` or `update` here is an ordinary capability name, not the object\'s read or edit permission. When the client has resolved the user\'s capabilities and any of these is missing, this block does not render its content; wherever it would otherwise render, an insufficient-permissions notice takes its place. Presentation only: it authorises nothing, and the data API still serves the same data to the same user. A client that cannot resolve the user\'s capabilities (no permission provider, or one that does not report `systemPermissions`) renders this block as if they were held — it fails open.' }, // Derived from the spec's own vocabulary rather than restated — #3019. { name: 'location', type: 'enum', enum: [...ACTION_LOCATIONS], description: 'Which declared action location this bar renders' }, { name: 'align', type: 'enum', enum: ['start', 'center', 'end'] }, diff --git a/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.requiredPermissions-gate.test.tsx b/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.requiredPermissions-gate.test.tsx index 9bcb4f519d..0b96aefab7 100644 --- a/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.requiredPermissions-gate.test.tsx +++ b/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.requiredPermissions-gate.test.tsx @@ -7,9 +7,17 @@ */ /** - * `record:quick_actions.requiredPermissions` — the BLOCK-LEVEL gate the - * published contract describes as "Hide the whole bar unless the current user - * holds every named permission on this object" (objectui#8071 slice 4). + * `record:quick_actions.requiredPermissions` — the BLOCK-LEVEL gate + * (objectui#8071 slice 4). The installed contract (`@objectstack/spec` 17.5.0, + * objectstack#18159) describes it with the one record-block describe this key + * shares with `record:details`, `record:highlights` and `record:related_list`: + * ALL the named capabilities must be held, and when one is missing "an + * insufficient-permissions notice takes its place". The registration publishes + * that describe verbatim, and the last block below re-reads it off the + * installed spec (objectui#10224). Through 17.4.0 the contract said "Hide the + * whole bar unless the current user holds every named permission on this + * object", and the registration said "Hide the whole bar unless the user holds + * these permissions"; neither named the notice the renderer draws. * * ## What this file pins, and why it was rewritten (objectui#10058) * @@ -17,7 +25,8 @@ * carries on `action`, `app`, `field` and `bulkAction` (ruling batch #192 item * 5 letter B). The renderer reads it through the permission context's * capability path and gates fail-closed: an unheld or unrecognised capability - * hides the whole bar. + * puts a `role="status"` insufficient-permissions notice in place of the whole + * bar, so no action is drawn. * * It used to read `perms.can(objectName, name)`, whose second argument is the * closed object-action enum. Under the stock `/me/permissions` provider a name @@ -27,9 +36,11 @@ * the previous ones mocked `usePermissions` down to a `can` stub and therefore * pinned the defect: they passed on a gate that does not gate. * - * ⭐ Every pin below mounts a REAL stock provider and reads its real verdicts. - * A mocked `usePermissions` cannot discriminate the two reading paths — it IS - * whichever path the mock chooses to implement. + * ⭐ Every gate pin below mounts a REAL stock provider and reads its real + * verdicts. A mocked `usePermissions` cannot discriminate the two reading + * paths — it IS whichever path the mock chooses to implement. The one gate pin + * that mounts no provider does so on purpose: no provider at all is the case + * it pins. * * Not to be confused with an `ActionDef`'s OWN `requiredPermissions` — a * per-action field the `gated` fixture in @@ -41,10 +52,15 @@ import * as React from 'react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; +import { ComponentRegistry } from '@object-ui/core'; import { MetadataCtx, RecordContextProvider } from '@object-ui/react'; import type { MetadataContextValue } from '@object-ui/react'; import { MePermissionsProvider, PermissionProvider, usePermissions, type MePermissionsResponse } from '@object-ui/permissions'; +import { RecordQuickActionsProps } from '@objectstack/spec/ui'; import { RecordQuickActionsRenderer } from '../record-quick-actions'; +// Registers `record:quick_actions` and its three sibling record blocks, whose +// published `inputs` the last describe block reads. +import '../../index'; /** * Records what the OBJECT-PERMISSION path was asked, without replacing it. @@ -157,7 +173,7 @@ beforeEach(() => { }); describe('record:quick_actions.requiredPermissions — capability gate on MePermissionsProvider (objectui#10058)', () => { - it('hides the WHOLE bar when the declared capability is not held (REPORTED-empty capability set)', async () => { + it('puts the notice in place of the WHOLE bar when the declared capability is not held (REPORTED-empty capability set)', async () => { render({bar({ ...BY_NAME, requiredPermissions: ['crm.manage'] })}); expect(await refused()).toBeInTheDocument(); noButton(); @@ -169,7 +185,7 @@ describe('record:quick_actions.requiredPermissions — capability gate on MePerm noRefusal(); }); - it('an UNKNOWN capability name hides the bar — unrecognised is refused, not waved through', async () => { + it('an UNKNOWN capability name gets the notice, not the bar — unrecognised is refused, not waved through', async () => { render({bar({ ...BY_NAME, requiredPermissions: ['not.a.real.capability'] })}); expect(await refused()).toBeInTheDocument(); noButton(); @@ -237,6 +253,23 @@ describe('the reading path is the capability set, NOT `perms.can()` (objectui#10 noButton(); }); + // DISCRIMINATOR ④ (objectui#10224): the published describe says in as many + // words that `read` and `update` here are ordinary capability names, not the + // object's read or edit permission. They are the stock provider's MAPPED + // verbs, a different leg from ③'s unmapped `manage`: the object-action read + // answered each off its own bit, TRUE in its row, and would have drawn the bar. + it('DISCRIMINATOR ④a: `read` is a capability name — `allowRead` on the object does not open the gate', async () => { + render({bar({ ...BY_NAME, requiredPermissions: ['read'] })}); + expect(await refused()).toBeInTheDocument(); + noButton(); + }); + + it('DISCRIMINATOR ④b: `update` is a capability name — `allowEdit` on the object does not open the gate', async () => { + render({bar({ ...BY_NAME, requiredPermissions: ['update'] })}); + expect(await refused()).toBeInTheDocument(); + noButton(); + }); + it('DETECTOR: the renderer never asks the OBJECT-permission path about a capability name', async () => { render({bar({ ...BY_NAME, requiredPermissions: ['crm.manage', 'manage'] })}); expect(await refused()).toBeInTheDocument(); @@ -278,6 +311,60 @@ describe('role-based PermissionProvider — capabilities are UNREPORTED here (ob }); }); +describe('a client that cannot resolve capabilities fails OPEN — the describe\'s last clause (objectui#10224)', () => { + /** + * The published describe names two such clients: "no permission provider, + * or one that does not report `systemPermissions`". The role-based provider + * above is one of the second kind; these two rows are the other two shapes + * a real page meets. The lit control is the first block's opening row: the + * SAME stock provider reporting an EMPTY set gates the same declaration, so + * the open verdict here is driven by "unreported", not by a gate that never + * closes. + */ + it('renders with NO permission provider mounted at all', async () => { + render(bar({ ...BY_NAME, requiredPermissions: ['crm.manage'] })); + expect(await shown()).toBeInTheDocument(); + noRefusal(); + }); + + it('renders under the stock provider when the backend does not report `systemPermissions`', async () => { + render({bar({ ...BY_NAME, requiredPermissions: ['crm.manage'] })}); + expect(await shown()).toBeInTheDocument(); + noRefusal(); + }); +}); + +describe('the published description is the contract\'s shared record-block describe, verbatim (objectui#10224)', () => { + /** + * The registration's `inputs` are the published authoring surface + * (`gen-manifest.ts` serializes them into `sdui.manifest.json` and the JSX + * authoring types). The describe is the text of record, read off the + * INSTALLED spec every run rather than restated, so a spec that rewords it + * turns this row red instead of leaving the manifest to drift. Every clause + * of that text is a row above: ALL capabilities required (the partial-grant + * pair), capability names rather than object actions (④), the notice in + * place of the bar, and fail-open when capabilities are unreported. + */ + const shapeOf = () => + (RecordQuickActionsProps.shape as Record) + .requiredPermissions; + const contractText = () => shapeOf()?.description ?? shapeOf()?.def?.innerType?.description; + const publishedText = (type: string) => + ComponentRegistry.getConfig(type)?.inputs?.find((i) => i.name === 'requiredPermissions')?.description; + + it('`record:quick_actions` publishes the installed describe text, verbatim', () => { + // Non-vacuity: an undefined describe would compare equal to a missing one. + expect(contractText() ?? '', 'the contract carries no describe to publish').not.toBe(''); + expect(publishedText('record:quick_actions')).toBe(contractText()); + }); + + it('the text is the one the three sibling record blocks publish — one describe, four blocks', () => { + for (const type of ['record:details', 'record:highlights', 'record:related_list']) { + expect(publishedText(type), type).toBe(publishedText('record:quick_actions')); + } + }); +}); + describe('CONTROL — `can()` answers exactly what it answered before this card (objectui#10058)', () => { /** * The ruling moves this ONE call site and no other caller: `can()`'s own From 4e45246a3af2bb204b4eceb97d9068a5899f42b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 04:07:24 +0000 Subject: [PATCH 2/4] docs(plugin-detail): drop spec version stamps from the new prose; correct the pending objectui#10058 changeset (objectui#10224) check:installed-pin-claims read "Through 17.4.0" in the gate pin's header as a present-tense claim about the installed spec and failed it. The new prose now names objectstack#18159 instead of a version, in the pin header and in the registration comment, so the next spec bump has nothing to restamp. The pending objectui#10058 changeset says an unheld capability "hides the whole bar" and quotes the contract's retired wording. It ships in the same release as this change, which publishes the notice behaviour, so it gets a dated, append-only correction in the shape objectui#8649 gave the sibling objectui#10155 changeset. Its frontmatter and every earlier byte are unchanged. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude --- .../10058-quick-actions-capability-gate.md | 13 ++++++++++++ packages/plugin-detail/src/index.tsx | 8 ++++---- ...-actions.requiredPermissions-gate.test.tsx | 20 +++++++++---------- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/.changeset/10058-quick-actions-capability-gate.md b/.changeset/10058-quick-actions-capability-gate.md index 23c93ad6b0..7835d50aa3 100644 --- a/.changeset/10058-quick-actions-capability-gate.md +++ b/.changeset/10058-quick-actions-capability-gate.md @@ -42,3 +42,16 @@ REPORTED empty array is a real answer and gates strictly. `perms.can()` itself is untouched: no other caller moves, and the full before/after truth table of both stock providers is pinned as unchanged. + +**Correction, 2026-10-01 (objectui#10224).** The phrase "hides the whole bar" +above is wrong about what the reader sees. When a declared capability is unheld or +unrecognised, no action is drawn and an insufficient-permissions notice +(`role="status"`) renders where the bar would be. That is what the contract's +shared record-block `requiredPermissions` describe says: "this block does not +render its content; wherever it would otherwise render, an +insufficient-permissions notice takes its place". The `record:quick_actions` +registration now publishes that describe verbatim, in place of "Hide the whole +bar unless the user holds these permissions", and the contract wording quoted +in the first paragraph is retired upstream (objectstack#18159). Everything +else above stands: the capabilities the gate asks for, its fail-closed +verdict, and the fail-open when capabilities are unreported. diff --git a/packages/plugin-detail/src/index.tsx b/packages/plugin-detail/src/index.tsx index a11ca227c3..0bda81d748 100644 --- a/packages/plugin-detail/src/index.tsx +++ b/packages/plugin-detail/src/index.tsx @@ -876,10 +876,10 @@ ComponentRegistry.register('quick_actions', RecordQuickActionsRenderer, { // Implementing the fallback would be a behaviour expansion and needs its own // card; pinned by `recordQuickActionsInputs.actionNamesFallback.test.tsx`. { name: 'actionNames', type: 'array', of: 'string', description: 'Action names to expose, in order — resolved from the actions declared on the object. With no names (and no host-supplied actions) nothing is looked up and the bar renders its empty placeholder' }, - // The contract's shared record-block describe, verbatim (`@objectstack/spec` - // 17.5.0, objectstack#18159): `record:quick_actions` carries the one text - // `record:details`, `record:highlights` and `record:related_list` share, and - // objectui#8649 already publishes it on those three. This input used to read + // The contract's shared record-block describe, verbatim (objectstack#18159): + // `record:quick_actions` carries the one text `record:details`, + // `record:highlights` and `record:related_list` share, and objectui#8649 + // already publishes it on those three. This input used to read // "Hide the whole bar unless the user holds these permissions", which is not // what the renderer does: the gate reads the CAPABILITY set // (`perms.hasCapabilities`, objectui#10058), puts a `role="status"` diff --git a/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.requiredPermissions-gate.test.tsx b/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.requiredPermissions-gate.test.tsx index 0b96aefab7..90e0901cfe 100644 --- a/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.requiredPermissions-gate.test.tsx +++ b/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.requiredPermissions-gate.test.tsx @@ -8,16 +8,16 @@ /** * `record:quick_actions.requiredPermissions` — the BLOCK-LEVEL gate - * (objectui#8071 slice 4). The installed contract (`@objectstack/spec` 17.5.0, - * objectstack#18159) describes it with the one record-block describe this key - * shares with `record:details`, `record:highlights` and `record:related_list`: - * ALL the named capabilities must be held, and when one is missing "an - * insufficient-permissions notice takes its place". The registration publishes - * that describe verbatim, and the last block below re-reads it off the - * installed spec (objectui#10224). Through 17.4.0 the contract said "Hide the - * whole bar unless the current user holds every named permission on this - * object", and the registration said "Hide the whole bar unless the user holds - * these permissions"; neither named the notice the renderer draws. + * (objectui#8071 slice 4). Since objectstack#18159 the contract describes it + * with the one record-block describe this key shares with `record:details`, + * `record:highlights` and `record:related_list`: ALL the named capabilities + * must be held, and when one is missing "an insufficient-permissions notice + * takes its place". The registration publishes that describe verbatim, and the + * last block below re-reads it off the installed spec (objectui#10224). Before + * objectstack#18159 the contract said "Hide the whole bar unless the current + * user holds every named permission on this object", and the registration said + * "Hide the whole bar unless the user holds these permissions"; neither named + * the notice the renderer draws. * * ## What this file pins, and why it was rewritten (objectui#10058) * From 105cddc65af61a82b1bb8ca32f16e0a6697301b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 04:18:25 +0000 Subject: [PATCH 3/4] docs(plugin-detail): the record-block capability gates say they draw a notice, not that they hide (objectui#10224) Round 2, comment and test-title text only. The record:quick_actions gate docblock said an unheld capability "hides the whole bar", directly above the code that renders a role=status insufficient-permissions notice in the bar's place. It now says that, cites the shared record-block describe the registration publishes, and points at its existing paragraph on the fail-open for unreported capabilities. The sibling pin record-blocks.requiredPermissions-gate.test.tsx carried the same "hides" wording in its header and two test titles for blocks that draw the notice. Those texts now name the notice. No assertion, fixture or helper moves, and no ledger or locator reads either title. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude --- .../record-blocks.requiredPermissions-gate.test.tsx | 12 +++++++----- .../src/renderers/record-quick-actions.tsx | 10 ++++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/plugin-detail/src/renderers/__tests__/record-blocks.requiredPermissions-gate.test.tsx b/packages/plugin-detail/src/renderers/__tests__/record-blocks.requiredPermissions-gate.test.tsx index 5c38e6876e..e32925e4e9 100644 --- a/packages/plugin-detail/src/renderers/__tests__/record-blocks.requiredPermissions-gate.test.tsx +++ b/packages/plugin-detail/src/renderers/__tests__/record-blocks.requiredPermissions-gate.test.tsx @@ -16,9 +16,11 @@ * The key is an **ADR-0066 system capability set** — the one meaning the word * carries on `action`, `app`, `field` and `bulkAction` — read through the * permission context's capability path and gating **fail-closed**: an unheld - * or unrecognised capability hides the block. objectui#10058 settled that for - * `record:quick_actions`; these three carried the identical call and were - * untouched by it. + * or unrecognised capability withholds the block's content, and an + * insufficient-permissions notice (`role="status"`) renders in its place — the + * block is not hidden (wording corrected by objectui#10224). objectui#10058 + * settled the capability read for `record:quick_actions`; these three carried + * the identical call and were untouched by it. * * All three used to read `perms.can(objectName, name)`, whose second argument * is the closed object-action enum. Under the stock `/me/permissions` provider @@ -175,7 +177,7 @@ describe.each(BLOCKS)('$key — the `requiredPermissions` capability gate on MeP const noBody = () => expect(screen.queryByTestId(block.shown)).not.toBeInTheDocument(); const noRefusal = () => expect(screen.queryByText(block.refusal)).not.toBeInTheDocument(); - it('hides the WHOLE block when the declared capability is not held (REPORTED-empty capability set)', async () => { + it('puts the notice in place of the WHOLE block when the declared capability is not held (REPORTED-empty capability set)', async () => { render({bound(block, { requiredPermissions: ['crm.manage'] })}); expect(await refused()).toBeInTheDocument(); noBody(); @@ -187,7 +189,7 @@ describe.each(BLOCKS)('$key — the `requiredPermissions` capability gate on MeP noRefusal(); }); - it('an UNKNOWN capability name hides the block — unrecognised is refused, not waved through', async () => { + it('an UNKNOWN capability name gets the notice, not the block — unrecognised is refused, not waved through', async () => { render({bound(block, { requiredPermissions: ['not.a.real.capability'] })}); expect(await refused()).toBeInTheDocument(); noBody(); diff --git a/packages/plugin-detail/src/renderers/record-quick-actions.tsx b/packages/plugin-detail/src/renderers/record-quick-actions.tsx index d5a44899ab..eff51b7b6c 100644 --- a/packages/plugin-detail/src/renderers/record-quick-actions.tsx +++ b/packages/plugin-detail/src/renderers/record-quick-actions.tsx @@ -237,8 +237,14 @@ export const RecordQuickActionsRenderer: React.FC Date: Thu, 1 Oct 2026 04:36:55 +0000 Subject: [PATCH 4/4] docs(plugin-detail): the changeset names only what carries the published text (objectui#10224) Round 3, from the at-tier review. The 10224 changeset said the new description is carried by sdui.manifest.json "and the JSX authoring types". The manifest carries it: manifestFromConfigs copies each input's description. generateDts in sdui-parser's codegen.ts reads no description, so the generated intrinsics carry none, before or after this change. The sentence now names the manifest and the runtime registry inputs only. The gate pin's docblock made the same pairing about the registration's inputs; it now says the manifest carries descriptions and the JSX types take only names and value types. Claude-Session: https://claude.ai/code/session_01VhxTqosz7wn54ahqyxgERT Co-authored-by: Claude --- .changeset/10224-quick-actions-permissions-text.md | 7 ++++--- ...ecord-quick-actions.requiredPermissions-gate.test.tsx | 9 ++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.changeset/10224-quick-actions-permissions-text.md b/.changeset/10224-quick-actions-permissions-text.md index e46200f3b8..9efee30e3c 100644 --- a/.changeset/10224-quick-actions-permissions-text.md +++ b/.changeset/10224-quick-actions-permissions-text.md @@ -13,6 +13,7 @@ or a backend that does not report `systemPermissions`) it renders the bar. `@objectstack/spec` 17.5.0 gives this key one describe, shared with `record:details`, `record:highlights` and `record:related_list`, and those three -already publish it. The quick-actions input now publishes the same text, so -`sdui.manifest.json` and the JSX authoring types carry it. No input name, type -or shape changes, and no rendering or gating behaviour changes. +already publish it. The quick-actions input now publishes the same text. It is +carried by `sdui.manifest.json`, and at runtime by +`ComponentRegistry.getConfig('record:quick_actions').inputs`. No input name, +type or shape changes, and no rendering or gating behaviour changes. diff --git a/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.requiredPermissions-gate.test.tsx b/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.requiredPermissions-gate.test.tsx index 90e0901cfe..185dbcdf0c 100644 --- a/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.requiredPermissions-gate.test.tsx +++ b/packages/plugin-detail/src/renderers/__tests__/record-quick-actions.requiredPermissions-gate.test.tsx @@ -336,9 +336,12 @@ describe('a client that cannot resolve capabilities fails OPEN — the describe\ describe('the published description is the contract\'s shared record-block describe, verbatim (objectui#10224)', () => { /** - * The registration's `inputs` are the published authoring surface - * (`gen-manifest.ts` serializes them into `sdui.manifest.json` and the JSX - * authoring types). The describe is the text of record, read off the + * The registration's `inputs` are the published authoring surface: + * `gen-manifest.ts` serializes them into `sdui.manifest.json`, descriptions + * included. The generated JSX authoring types take only each input's name + * and value type (`generateDts` reads no `description`), so the manifest and + * the runtime registry are where this text is published. The describe is + * the text of record, read off the * INSTALLED spec every run rather than restated, so a spec that rewords it * turns this row red instead of leaving the manifest to drift. Every clause * of that text is a row above: ALL capabilities required (the partial-grant