From 7962a0c680a531f07d55dcec4e84b08d53c68ac3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 00:03:03 +0000 Subject: [PATCH 1/3] feat(app-shell): metadata-admin reads `/meta/types` `allowEnvironmentOverlay`; the read-only banner names `OS_METADATA_WRITABLE` and no registry flip (objectui#12141) The server renames the type entry's overlay flag (objectstack-ai/objectstack#22340 S2). Every reader in the metadata-admin designer and Studio now reads the new name, with no fallback to the old one, and `RichMetadataTypeEntry` and `resolveResourceConfig` carry the renamed member. The read-only type banner named an env var the server does not read and offered a registry flip nobody can perform; it now names `OS_METADATA_WRITABLE` with the hatch's reach (ADR-0131 D6). The permission matrix's artifact hint no longer offers the hatch for a packaged set, which the hatch does not open. Claude-Session: https://claude.ai/code/session_016djJF12Qt14ejKR5Vjt4sK Co-authored-by: Claude --- content/docs/guide/console.md | 2 +- ...ConditionWidget.conditionSubjects.test.tsx | 2 +- .../views/metadata-admin/DirectoryPage.tsx | 8 +- ...mbeddedItemEditor.draftSave-12027.test.tsx | 2 +- ...beddedItemEditor.layersRead-11799.test.tsx | 2 +- ...ditor.picklistServedOptions-11692.test.tsx | 2 +- .../EmbeddedItemEditor.preview.test.tsx | 2 +- ...dedItemEditor.refusalIssues-11379.test.tsx | 2 +- .../metadata-admin/EmbeddedItemEditor.tsx | 2 +- .../src/views/metadata-admin/PageShell.tsx | 4 +- ...rmissionMatrixEditor.artifactTier.test.tsx | 33 +-- ...issionMatrixEditor.autosave-11787.test.tsx | 2 +- .../PermissionMatrixEditor.badge.test.tsx | 2 +- .../PermissionMatrixEditor.basics.test.tsx | 2 +- ...missionMatrixEditor.bulkMergeKeys.test.tsx | 2 +- ...sionMatrixEditor.cloneToCustomize.test.tsx | 19 +- .../PermissionMatrixEditor.dirty.test.tsx | 2 +- .../PermissionMatrixEditor.embedded.test.tsx | 2 +- .../PermissionMatrixEditor.fieldBulk.test.tsx | 2 +- ...missionMatrixEditor.fieldEnvelope.test.tsx | 2 +- ...nMatrixEditor.layersUnsaved-11799.test.tsx | 2 +- ...ionMatrixEditor.packageDoorFacets.test.tsx | 2 +- ...ixEditor.packageDoorRereadRefusal.test.tsx | 2 +- ...MatrixEditor.provenanceBadge-7611.test.tsx | 2 +- ...nMatrixEditor.readDecorationStrip.test.tsx | 2 +- .../PermissionMatrixEditor.readonly.test.tsx | 34 +-- ...nMatrixEditor.readonlyHeaderBadge.test.tsx | 34 +-- ...MatrixEditor.retiredLifecycleKeys.test.tsx | 2 +- .../PermissionMatrixEditor.scope.test.tsx | 2 +- ...ionMatrixEditor.setupCatalog-7611.test.tsx | 2 +- .../metadata-admin/PermissionMatrixEditor.tsx | 34 +-- .../ResourceEditPage.artifactTier.test.tsx | 6 +- .../ResourceEditPage.celGate.test.tsx | 4 +- ...itPage.choiceWithoutOptions-11253.test.tsx | 2 +- .../ResourceEditPage.conditionScope.test.tsx | 2 +- .../ResourceEditPage.defaultGate.test.tsx | 2 +- .../ResourceEditPage.docEditor-10188.test.tsx | 2 +- ...itPage.draftVersionConflict-11773.test.tsx | 2 +- ...EditPage.environmentOverlay-12141.test.tsx | 204 ++++++++++++++++++ ...ceEditPage.lateRegistration-11939.test.tsx | 2 +- ...ourceEditPage.layersUnsaved-11799.test.tsx | 2 +- .../ResourceEditPage.lockBanner.test.tsx | 2 +- ...esourceEditPage.navDeepLink-11153.test.tsx | 2 +- ...eEditPage.pageRequiresStamp-11357.test.tsx | 2 +- ...EditPage.permissionSetsFeed-12126.test.tsx | 2 +- ...esourceEditPage.pickerLoadFailure.test.tsx | 2 +- ...ceEditPage.picklistReadOnly-10202.test.tsx | 4 +- ...tPage.picklistServedOptions-10202.test.tsx | 2 +- ...rceEditPage.publishPackageBinding.test.tsx | 2 +- ...ourceEditPage.readDecorationStrip.test.tsx | 2 +- ...urceEditPage.referencesErrorState.test.tsx | 2 +- ...tPage.requiredInGatedSection-6900.test.tsx | 2 +- .../ResourceEditPage.resetVerdict.test.tsx | 2 +- .../ResourceEditPage.schemaAdvisory.test.tsx | 2 +- ...itPage.servedDraftTakenAsIs-10765.test.tsx | 4 +- ...esourceEditPage.serverRefusalGate.test.tsx | 2 +- ...esourceEditPage.setupCatalog-7611.test.tsx | 4 +- .../views/metadata-admin/ResourceEditPage.tsx | 41 ++-- ...rceListPage.environmentScope-7611.test.tsx | 6 +- .../views/metadata-admin/ResourceListPage.tsx | 2 +- .../SchemaForm.topLevelUnion-11251.test.tsx | 2 +- .../src/views/metadata-admin/i18n.ts | 29 ++- .../previews/PicklistPreview.tsx | 2 +- .../src/views/metadata-admin/registry.ts | 6 +- .../src/views/metadata-admin/useMetadata.ts | 17 +- ...ilderLanding.readonlyRoutes-11808.test.tsx | 2 +- .../views/studio-design/BuilderLanding.tsx | 6 +- .../StudioDesignSurface.accessGuard.test.tsx | 2 +- ...ignSurface.accessRailLabels-11862.test.tsx | 2 +- ...ce.createAppOpensInterfaces-11794.test.tsx | 2 +- ...DesignSurface.moreKeepsName-11794.test.tsx | 2 +- ...tudioDesignSurface.pillarNavGuard.test.tsx | 2 +- 72 files changed, 422 insertions(+), 181 deletions(-) create mode 100644 packages/app-shell/src/views/metadata-admin/ResourceEditPage.environmentOverlay-12141.test.tsx diff --git a/content/docs/guide/console.md b/content/docs/guide/console.md index 81a7e9fcef..bd19f28f17 100644 --- a/content/docs/guide/console.md +++ b/content/docs/guide/console.md @@ -34,7 +34,7 @@ The console opens at **http://localhost:5180** (the port is fixed in `apps/conso | **Package-less Flows in Studio** | Flows that belong to no package — such as a clone of a packaged flow, made from Setup › Packaged automation — are listed and edited at `/studio/~org/automations`, reached from the Studio home ("Not in a package") or the package switcher. They open editable, edits save as drafts, and Publish promotes those drafts. | | **Studio Automations Rail** | The Automations pillar's flow rail has a search box that matches a flow's label or its machine name, case-insensitively. A long flow name wraps instead of being cut off. | | **Create App in Studio** | In a package that has no app yet, the Studio header's **Create app** saves the new app as a draft and then opens the **Interfaces** pillar, where that app is designed. Leaving a pillar that holds an unsent edit asks first. | -| **Read-only Packages in Studio** | The Studio home lists code and installed packages under "Installed (read-only · browsable)". Clicking a card opens the package for browsing (`/studio/:packageId/data`). The card's **Customize with an overlay** link opens the package's metadata directory (`/apps/setup/metadata?package=:packageId`). Whether an item can take an org overlay is decided per metadata type (`allowOrgOverride`), not per package, and the directory marks each type. Unless the runtime reports `features.marketplace: false`, the section also links to the marketplace (`/apps/setup/system/marketplace`), the same route as Home's "Start with a template". | +| **Read-only Packages in Studio** | The Studio home lists code and installed packages under "Installed (read-only · browsable)". Clicking a card opens the package for browsing (`/studio/:packageId/data`). The card's **Customize with an overlay** link opens the package's metadata directory (`/apps/setup/metadata?package=:packageId`). Whether an item can take an org overlay is decided per metadata type (`allowEnvironmentOverlay`), not per package, and the directory marks each type. Unless the runtime reports `features.marketplace: false`, the section also links to the marketplace (`/apps/setup/system/marketplace`), the same route as Home's "Start with a template". | | **Design in Studio** | Workspace admins get a top-bar entry inside a running app that opens its owning package on the Studio design surface. On an interface route — a dashboard, page, or report — it deep-links straight to that surface's design page in the Interfaces pillar (`/studio/:packageId/interfaces?surface=:`, e.g. `surface=page:showcase_crm_workbench`); elsewhere (objects, the app root) it opens the package's Data tab (`/studio/:packageId/data`). These interfaces are authored in Studio — there is no in-page edit panel. | | **Dashboard Refresh** | A dashboard page shows a **Refresh All** button above its widgets, and a dashboard that sets `refreshIntervalSeconds` (Studio's auto-refresh field) re-reads its widgets' data every that many seconds. Widgets re-read in place, so they are not remounted. `0` or no value means no automatic refresh. Known gap: a dataset-bound single-value (KPI) tile does not refresh yet. | | **Refused Dashboards and List Views** | The server lists only the dashboards and list views a user may open, so one gated by `requiredPermissions` is not in their lists. When such a dashboard or list view is opened by its URL, the console asks the server about it by name. If the server refuses it with `403 PERMISSION_DENIED`, the page says "You don't have access" and shows the server's sentence, which names the item. The page does not say "not found", and the list page does not switch to the default view. For a name that does not exist, the dashboard page still says it was not found and the list page still opens the default view. An object's own list view that the server sends whole to a user who may edit the object is listed and opens. | diff --git a/packages/app-shell/src/views/metadata-admin/ConditionWidget.conditionSubjects.test.tsx b/packages/app-shell/src/views/metadata-admin/ConditionWidget.conditionSubjects.test.tsx index 058ea77ec7..799b63c4e5 100644 --- a/packages/app-shell/src/views/metadata-admin/ConditionWidget.conditionSubjects.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ConditionWidget.conditionSubjects.test.tsx @@ -78,7 +78,7 @@ const HOOK_ENTRY = { name: 'hook', label: 'Hook', // What makes the item writable, and therefore what makes an Edit button exist. - allowOrgOverride: true, + allowEnvironmentOverlay: true, schema: { type: 'object', properties: { diff --git a/packages/app-shell/src/views/metadata-admin/DirectoryPage.tsx b/packages/app-shell/src/views/metadata-admin/DirectoryPage.tsx index 68f0ea8d90..1f98df3621 100644 --- a/packages/app-shell/src/views/metadata-admin/DirectoryPage.tsx +++ b/packages/app-shell/src/views/metadata-admin/DirectoryPage.tsx @@ -168,11 +168,11 @@ export function MetadataDirectoryPage() { }, [scopedEntries]); const writableCount = scopedEntries.filter( - (e) => e.allowOrgOverride || e.allowRuntimeCreate, + (e) => e.allowEnvironmentOverlay || e.allowRuntimeCreate, ).length; const filtered = scopedEntries.filter((e) => { - if (writableOnly && !(e.allowOrgOverride || e.allowRuntimeCreate)) return false; + if (writableOnly && !(e.allowEnvironmentOverlay || e.allowRuntimeCreate)) return false; if (domainFilter !== 'all' && (e.domain ?? 'other') !== domainFilter) return false; if (query) { const q = query.toLowerCase(); @@ -455,7 +455,7 @@ function TypeTile({ {lockedCount} )} - {entry.allowOrgOverride ? ( + {entry.allowEnvironmentOverlay ? ( diff --git a/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.draftSave-12027.test.tsx b/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.draftSave-12027.test.tsx index d28769dc69..850bc5b075 100644 --- a/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.draftSave-12027.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.draftSave-12027.test.tsx @@ -116,7 +116,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'field', label: 'Field', allowOrgOverride: true, schema: FIELD_SCHEMA }], + entries: [{ type: 'field', label: 'Field', allowEnvironmentOverlay: true, schema: FIELD_SCHEMA }], }), })); diff --git a/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.layersRead-11799.test.tsx b/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.layersRead-11799.test.tsx index 8e0d2a2646..f9327277a3 100644 --- a/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.layersRead-11799.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.layersRead-11799.test.tsx @@ -99,7 +99,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'field', label: 'Field', allowOrgOverride: true, schema: FIELD_SCHEMA }], + entries: [{ type: 'field', label: 'Field', allowEnvironmentOverlay: true, schema: FIELD_SCHEMA }], }), })); diff --git a/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.picklistServedOptions-11692.test.tsx b/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.picklistServedOptions-11692.test.tsx index fb41ef7376..4809c979f1 100644 --- a/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.picklistServedOptions-11692.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.picklistServedOptions-11692.test.tsx @@ -104,7 +104,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'field', label: 'Field', allowOrgOverride: true, schema: FIELD_SCHEMA }], + entries: [{ type: 'field', label: 'Field', allowEnvironmentOverlay: true, schema: FIELD_SCHEMA }], }), })); diff --git a/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.preview.test.tsx b/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.preview.test.tsx index d60cb5f5d9..42b21f1f6c 100644 --- a/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.preview.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.preview.test.tsx @@ -67,7 +67,7 @@ vi.mock('./useMetadata', () => ({ { type: 'validation', label: 'Validation Rule', - allowOrgOverride: true, + allowEnvironmentOverlay: true, schema: VALIDATION_SCHEMA, }, ], diff --git a/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.refusalIssues-11379.test.tsx b/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.refusalIssues-11379.test.tsx index e8d2c662c5..a4903c8468 100644 --- a/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.refusalIssues-11379.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.refusalIssues-11379.test.tsx @@ -48,7 +48,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'field', label: 'Field', allowOrgOverride: true, schema: FIELD_SCHEMA }], + entries: [{ type: 'field', label: 'Field', allowEnvironmentOverlay: true, schema: FIELD_SCHEMA }], }), })); diff --git a/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.tsx b/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.tsx index 062bbc87bb..f506b07780 100644 --- a/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.tsx +++ b/packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.tsx @@ -101,7 +101,7 @@ export function EmbeddedItemEditor({ }, [initialRaw, itemName, parentType, parentName]); const readOnly = - subEntry != null && !subEntry.allowOrgOverride && !subEntry.allowRuntimeCreate; + subEntry != null && !subEntry.allowEnvironmentOverlay && !subEntry.allowRuntimeCreate; async function doSave() { if (!embeddedPath) { diff --git a/packages/app-shell/src/views/metadata-admin/PageShell.tsx b/packages/app-shell/src/views/metadata-admin/PageShell.tsx index 2496bdc37c..af49dd3aa0 100644 --- a/packages/app-shell/src/views/metadata-admin/PageShell.tsx +++ b/packages/app-shell/src/views/metadata-admin/PageShell.tsx @@ -97,7 +97,7 @@ function WritabilityBadge({ ); } - if (entry?.allowOrgOverride) { + if (entry?.allowEnvironmentOverlay) { return ( diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.artifactTier.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.artifactTier.test.tsx index 6dc8d4e554..e7dbe26ce5 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.artifactTier.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.artifactTier.test.tsx @@ -18,7 +18,7 @@ * `}` * * The method's own doc states the split: "overlaying a packaged item" (requires - * `allowOrgOverride`) vs "authoring a DB-only item" (requires only + * `allowEnvironmentOverlay`) vs "authoring a DB-only item" (requires only * `allowRuntimeCreate`). `permission` sits exactly in the gap — `false` / * `true` — so after #4446 opened the type tier, an environment-scope edit of a * CODE-DECLARED set rendered 207 live checkboxes and a Save button that failed @@ -97,8 +97,8 @@ function makeClient() { // The stock-boot `permission` shape (objectstack#6483) unless a case says // otherwise: no per-org overlay, but runtime authoring is open. -let typeFlags: { allowOrgOverride?: boolean; allowRuntimeCreate?: boolean } = { - allowOrgOverride: false, +let typeFlags: { allowEnvironmentOverlay?: boolean; allowRuntimeCreate?: boolean } = { + allowEnvironmentOverlay: false, allowRuntimeCreate: true, }; let clientImpl: any; @@ -118,7 +118,7 @@ afterEach(() => { cleanup(); codeLayer = null; provenance = undefined; - typeFlags = { allowOrgOverride: false, allowRuntimeCreate: true }; + typeFlags = { allowEnvironmentOverlay: false, allowRuntimeCreate: true }; }); async function renderMatrix(props?: { packageId?: string; readOnly?: boolean }) { @@ -151,7 +151,7 @@ const TYPE_CAPTION = /no runtime write channel/; describe('PermissionMatrixEditPage — the ARTIFACT tier locks an env-scope overlay (#4518)', () => { /** * RED-FIRST. Environment scope (no `packageId`) + a set a code package ships - * + `allowOrgOverride: false`. The server answers this write with + * + `allowEnvironmentOverlay: false`. The server answers this write with * 403 `not_overridable`; pre-fix the editor offered Save anyway. */ it('code-declared set at environment scope is READ-ONLY — the server refuses this write', async () => { @@ -184,11 +184,16 @@ describe('PermissionMatrixEditPage — the ARTIFACT tier locks an env-scope over // NOT the package wording either — no read-only PACKAGE is involved. expect(screen.queryByText('Read-only', { exact: true })).toBeNull(); - // The hint carries the server's own reason and its own documented remedy, - // the same place the 403 text puts it. - expect(badge).toHaveAttribute('title', expect.stringContaining('allowOrgOverride')); + // The hint carries the server's own reason, the same place the 403 text + // puts it. It names the env hatch only to say the hatch does not open a set + // a package ships (ADR-0131 D6, objectui#12141): offered as a remedy here, + // it would send the operator to a door the server keeps shut. + expect(badge).toHaveAttribute('title', expect.stringContaining('allowEnvironmentOverlay')); expect(badge).toHaveAttribute('title', expect.stringContaining('not_overridable')); - expect(badge).toHaveAttribute('title', expect.stringContaining('OS_METADATA_WRITABLE')); + expect(badge).toHaveAttribute( + 'title', + expect.stringContaining('OS_METADATA_WRITABLE escape hatch does not open a set a package ships'), + ); // …and never in the label (objectui#4446's rule for this badge slot). expect(screen.queryByText(/OS_METADATA_WRITABLE/)).toBeNull(); }); @@ -210,10 +215,10 @@ describe('PermissionMatrixEditPage — the ARTIFACT tier locks an env-scope over }); it('an overlay-allowed type is still writable even when code-declared', async () => { - // `allowOrgOverride: true` IS permission to overlay a packaged item — the + // `allowEnvironmentOverlay: true` IS permission to overlay a packaged item — the // artifact tier's own condition (`artifactBacked && !overlayAllowed`) is // then false and the server accepts the write. - typeFlags = { allowOrgOverride: true, allowRuntimeCreate: false }; + typeFlags = { allowEnvironmentOverlay: true, allowRuntimeCreate: false }; codeLayer = { _packageId: 'com.example.showcase' }; await renderMatrix(); @@ -226,7 +231,7 @@ describe('PermissionMatrixEditPage — the ARTIFACT tier locks an env-scope over // The server reaches `!overlayAllowed && !runtimeCreateAllowed` FIRST, and // "this type accepts no runtime write at all" is the honest reason; the // artifact tier is only the DECIDING gate where the type tier said yes. - typeFlags = { allowOrgOverride: false, allowRuntimeCreate: false }; + typeFlags = { allowEnvironmentOverlay: false, allowRuntimeCreate: false }; codeLayer = { _packageId: 'com.example.showcase' }; await renderMatrix(); @@ -365,7 +370,7 @@ describe('PermissionMatrixEditPage — the artifact verdict asks ADR-0010 proven * RED on the sentinel-only predicate. The rehydration shape: the served * `code` layer carries a real package id, and only `provenance` says the * tenant authored it. The sentinel-only predicate called this an artifact, - * and `permission`'s `allowOrgOverride: false` then locked the whole matrix. + * and `permission`'s `allowEnvironmentOverlay: false` then locked the whole matrix. */ it("a tenant's own set rehydrated under a REAL package id (provenance 'org') is EDITABLE", async () => { codeLayer = { _packageId: 'com.example.showcase', name: 'showcase_contributor' }; @@ -393,7 +398,7 @@ describe('PermissionMatrixEditPage — the artifact verdict asks ADR-0010 proven }); it("a code-declared set (provenance 'package') is writable only where the type allows overlay", async () => { - typeFlags = { allowOrgOverride: true, allowRuntimeCreate: false }; + typeFlags = { allowEnvironmentOverlay: true, allowRuntimeCreate: false }; codeLayer = { _packageId: 'com.example.showcase', name: 'showcase_contributor' }; provenance = 'package'; await renderMatrix(); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.autosave-11787.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.autosave-11787.test.tsx index f54a0336aa..96dddb2282 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.autosave-11787.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.autosave-11787.test.tsx @@ -79,7 +79,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true }], }), })); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx index a0cc8ff0bf..6a4c941c20 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx @@ -27,7 +27,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true }], }), })); vi.mock('./AssignedUsersSection', () => ({ AssignedUsersSection: () => null })); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.basics.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.basics.test.tsx index a163daa36d..9e9b1d3d20 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.basics.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.basics.test.tsx @@ -30,7 +30,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true }], }), })); vi.mock('./AssignedUsersSection', () => ({ AssignedUsersSection: () => null })); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.bulkMergeKeys.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.bulkMergeKeys.test.tsx index da5ac831b4..edd45b993a 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.bulkMergeKeys.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.bulkMergeKeys.test.tsx @@ -85,7 +85,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true }], }), })); vi.mock('./AssignedUsersSection', () => ({ AssignedUsersSection: () => null })); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.cloneToCustomize.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.cloneToCustomize.test.tsx index 4a1c933730..2ceff0fe1a 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.cloneToCustomize.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.cloneToCustomize.test.tsx @@ -98,8 +98,8 @@ function makeClient() { } // The stock-boot `permission` shape (objectstack#6483) unless a case says otherwise. -let typeFlags: { allowOrgOverride?: boolean; allowRuntimeCreate?: boolean } = { - allowOrgOverride: false, +let typeFlags: { allowEnvironmentOverlay?: boolean; allowRuntimeCreate?: boolean } = { + allowEnvironmentOverlay: false, allowRuntimeCreate: true, }; let clientImpl: any; @@ -233,7 +233,7 @@ async function fillDialog(name = CLONE_NAME, label = CLONE_LABEL) { beforeEach(() => { codeLayerByName = { [BASE_SET]: { _packageId: 'com.example.showcase', name: BASE_SET } }; saveImpl = async (_t, _n, payload) => payload; - typeFlags = { allowOrgOverride: false, allowRuntimeCreate: true }; + typeFlags = { allowEnvironmentOverlay: false, allowRuntimeCreate: true }; localeFixture = 'en-US'; canAuthor = true; doorAnswer = okAnswer(); @@ -267,14 +267,19 @@ describe('PermissionMatrixEditPage — the locked editor offers Clone to customi const hint = badge.getAttribute('title') ?? ''; const clone = hint.indexOf('Clone to customize'); expect(clone).toBeGreaterThan(-1); - for (const secondary of ['redeploy', 'new runtime set', 'OS_METADATA_WRITABLE']) { + for (const secondary of ['redeploy', 'new runtime set']) { const at = hint.indexOf(secondary); expect(at, `"${secondary}" must be in the hint`).toBeGreaterThan(-1); expect(clone, `cloning must come before "${secondary}"`).toBeLessThan(at); } + // objectui#12141 — the env hatch is no longer a route: ADR-0131 D6 seals + // what a package ships, so the hint names it only to say it does not open + // this set, and never as something to ask an operator for. + expect(hint).toContain('OS_METADATA_WRITABLE escape hatch does not open a set a package ships'); + expect(hint).not.toMatch(/ask an operator for the documented OS_METADATA_WRITABLE/); // The server's own reason and code are still stated (objectui#4518's pins // on this hint are unchanged). - expect(hint).toContain('allowOrgOverride'); + expect(hint).toContain('allowEnvironmentOverlay'); expect(hint).toContain('not_overridable'); }); @@ -429,7 +434,7 @@ describe('PermissionMatrixEditPage — no clone prompt where the artifact tier i }); it('a type with no runtime write channel is locked by the TYPE tier: nothing to clone into', async () => { - typeFlags = { allowOrgOverride: false, allowRuntimeCreate: false }; + typeFlags = { allowEnvironmentOverlay: false, allowRuntimeCreate: false }; await renderRouted(); expect(screen.getByText(/no runtime write channel/)).toBeInTheDocument(); @@ -448,7 +453,7 @@ describe('PermissionMatrixEditPage — no clone prompt where the artifact tier i describe('PermissionMatrixEditPage — a save the server refuses as a packaged set shows the same guidance (objectui#5987 item 3)', () => { const SERVER_TEXT = "Metadata item 'permission/showcase_contributor' is provided by a code package and the type has not " - + 'opted into per-org overlay writes (allowOrgOverride=false). Edit the source artifact and redeploy, ' + + 'opted into per-org overlay writes (allowEnvironmentOverlay=false). Edit the source artifact and redeploy, ' + 'or set OS_METADATA_WRITABLE to grant a runtime escape hatch.'; it('403 NOT_OVERRIDABLE at the environment door: guidance names cloning, the primary action is offered, the server sentence is kept', async () => { diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.dirty.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.dirty.test.tsx index a662ca0c87..d6fcd4611b 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.dirty.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.dirty.test.tsx @@ -25,7 +25,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true }], }), })); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.embedded.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.embedded.test.tsx index 6b07b6f4f2..420553da24 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.embedded.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.embedded.test.tsx @@ -42,7 +42,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true }], }), })); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldBulk.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldBulk.test.tsx index 6c4db30504..4cf84a6bbf 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldBulk.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldBulk.test.tsx @@ -19,7 +19,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true }], }), })); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldEnvelope.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldEnvelope.test.tsx index b9059b6258..816d204ad2 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldEnvelope.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.fieldEnvelope.test.tsx @@ -55,7 +55,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true }], }), })); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.layersUnsaved-11799.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.layersUnsaved-11799.test.tsx index c3de3ec851..14c2e373fc 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.layersUnsaved-11799.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.layersUnsaved-11799.test.tsx @@ -106,7 +106,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: false, allowRuntimeCreate: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: false, allowRuntimeCreate: true }], }), })); vi.mock('./AssignedUsersSection', () => ({ AssignedUsersSection: () => null })); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorFacets.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorFacets.test.tsx index 313dc9d7a2..88b32febf5 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorFacets.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorFacets.test.tsx @@ -76,7 +76,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true }], }), })); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorRereadRefusal.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorRereadRefusal.test.tsx index aad9dd50e7..aeceb26c8c 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorRereadRefusal.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.packageDoorRereadRefusal.test.tsx @@ -171,7 +171,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true }], }), })); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.provenanceBadge-7611.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.provenanceBadge-7611.test.tsx index 86c426f752..681da0d7bf 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.provenanceBadge-7611.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.provenanceBadge-7611.test.tsx @@ -69,7 +69,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: false, allowRuntimeCreate: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: false, allowRuntimeCreate: true }], }), })); // The environment scope mounts who-holds-it under the definition; its reads are diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readDecorationStrip.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readDecorationStrip.test.tsx index 189d7f7e29..7f7bf325ba 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readDecorationStrip.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readDecorationStrip.test.tsx @@ -148,7 +148,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true }], }), })); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonly.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonly.test.tsx index c933711f3f..b79f556925 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonly.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonly.test.tsx @@ -12,18 +12,18 @@ * Two independent gates share one `writable` switch inside the editor: * • host gate — `readOnly` prop (read-only package in Studio), and * • type gate — the metadata type offers NO runtime write channel, i.e. - * `allowOrgOverride` AND `allowRuntimeCreate` are BOTH false. + * `allowEnvironmentOverlay` AND `allowRuntimeCreate` are BOTH false. * Either one must disable every checkbox / bulk button / input, hide Save, * and show a read-only badge — worded for ITS reason, so the badge never * contradicts the surrounding surface. * * ## The type gate is a DISJUNCTION, and this suite used to say otherwise * - * objectui#4446. Until then the switch read `allowOrgOverride` alone and this + * objectui#4446. Until then the switch read `allowEnvironmentOverlay` alone and this * header called that flag "environment-level OS_METADATA_WRITABLE". Both were * wrong, and in the same way — they collapsed two different doors into one: * - * • `allowOrgOverride` = may an ORG OVERLAY a code-shipped item; + * • `allowEnvironmentOverlay` = may an ORG OVERLAY a code-shipped item; * • `allowRuntimeCreate` = may an item be AUTHORED at runtime — which is * what this editor's Save actually does under a `packageId` * (`mode: 'draft'` + `packageId`, ADR-0086 P0/P2). @@ -31,13 +31,13 @@ * The server refuses only when BOTH are false (`!isOverlayAllowed && * !isRuntimeCreateAllowed`, metadata-protocol `protocol.ts`), so gating on the * first alone locked a surface the server accepts. `permission` is exactly that - * shape on a stock boot — `allowOrgOverride: false` (ADR-0005 forbids per-org + * shape on a stock boot — `allowEnvironmentOverlay: false` (ADR-0005 forbids per-org * overlay of a packaged permission set: silent privilege drift) with * `allowRuntimeCreate: true`, which objectstack#6483 kept open on purpose. * * And `OS_METADATA_WRITABLE` was never the type gate's reason: it does not sit - * beside `allowOrgOverride`, it FLIPS it (`getMetaTypes` emits - * `allowOrgOverride: base.allowOrgOverride || isEnvOverridden`). So there is no + * beside `allowEnvironmentOverlay`, it FLIPS it (`getMetaTypes` emits + * `allowEnvironmentOverlay: base.allowEnvironmentOverlay || isEnvOverridden`). So there is no * reachable state in which the old badge text was the honest explanation — * whenever the hatch is on for a type, that type is writable and no read-only * badge renders at all. @@ -91,10 +91,10 @@ function makeClient() { // Flipped per-test to drive the TYPE-level gate. Both flags are read straight // off the server entry, the way DirectoryPage / EmbeddedItemEditor / // ResourceEditPage read them (objectui#4446). -let allowOrgOverride = true; +let allowEnvironmentOverlay = true; // The other half of the disjunction. `false` is the historical default of this // suite (the key simply did not exist in the fixture, so it read `undefined`); -// the stock-boot `permission` shape sets it true with allowOrgOverride false. +// the stock-boot `permission` shape sets it true with allowEnvironmentOverlay false. let allowRuntimeCreate = false; // One stable client per test — the editor's load effect depends on the client // reference, so a fresh object per render would re-trigger it forever. @@ -105,7 +105,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride, allowRuntimeCreate }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay, allowRuntimeCreate }], }), })); @@ -113,7 +113,7 @@ import { PermissionMatrixEditPage } from './PermissionMatrixEditor'; afterEach(() => { cleanup(); - allowOrgOverride = true; + allowEnvironmentOverlay = true; allowRuntimeCreate = false; server.saved = []; }); @@ -188,7 +188,7 @@ describe('PermissionMatrixEditPage — read-only package gate (host readOnly)', describe('PermissionMatrixEditPage — type-level gate keeps its own wording', () => { it('no write channel at all (both flags false) hides Save and names the TYPE as the reason', async () => { - allowOrgOverride = false; + allowEnvironmentOverlay = false; allowRuntimeCreate = false; renderMatrix(); await screen.findByText('Account'); @@ -199,7 +199,7 @@ describe('PermissionMatrixEditPage — type-level gate keeps its own wording', ( // Type-gate reason, untouched by the package gate — and it names the type, // not a deployment env var (objectui#4446). The old wording claimed // "OS_METADATA_WRITABLE not enabled", which no reachable state supports: - // that hatch FLIPS allowOrgOverride to true, so when it is on this badge + // that hatch FLIPS allowEnvironmentOverlay to true, so when it is on this badge // does not render. expect(screen.queryByText(/OS_METADATA_WRITABLE/)).toBeNull(); const badge = screen.getByText(/no runtime write channel/); @@ -215,7 +215,7 @@ describe('PermissionMatrixEditPage — type-level gate keeps its own wording', ( * ── The defect objectui#4446 was filed for ──────────────────────────────── * * The stock-boot `permission` shape, measured on a live QA run - * (objectstack#7637): the registry declares `allowOrgOverride: false` and + * (objectstack#7637): the registry declares `allowEnvironmentOverlay: false` and * `allowRuntimeCreate: true`, and the server ACCEPTS the package-door write * (`PUT /api/v1/meta/permission/?package=` → 200). Pre-fix the editor * disabled all 207 checkboxes and hid Save anyway, then blamed @@ -225,7 +225,7 @@ describe('PermissionMatrixEditPage — type-level gate keeps its own wording', ( * control disabled. */ it('stock-boot permission shape (allowRuntimeCreate only) is EDITABLE — the server accepts this write', async () => { - allowOrgOverride = false; + allowEnvironmentOverlay = false; allowRuntimeCreate = true; renderMatrix(); await screen.findByText('Account'); @@ -249,7 +249,7 @@ describe('PermissionMatrixEditPage — type-level gate keeps its own wording', ( it('MUST NOT CHANGE — the package gate still wins over allowRuntimeCreate', async () => { // The "Studio 维持包级只读" half of the objectstack#5768 ruling: a // code-defined package stays locked no matter what the type permits. - allowOrgOverride = false; + allowEnvironmentOverlay = false; allowRuntimeCreate = true; renderMatrix({ readOnly: true }); await screen.findByText('Account'); @@ -262,8 +262,8 @@ describe('PermissionMatrixEditPage — type-level gate keeps its own wording', ( expect(screen.queryByText(/no runtime write channel/)).toBeNull(); }); - it('MUST NOT CHANGE — an allowOrgOverride package is byte-identical to before', async () => { - allowOrgOverride = true; + it('MUST NOT CHANGE — an allowEnvironmentOverlay package is byte-identical to before', async () => { + allowEnvironmentOverlay = true; allowRuntimeCreate = false; renderMatrix(); await screen.findByText('Account'); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonlyHeaderBadge.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonlyHeaderBadge.test.tsx index cae2890b1f..d99e1936a8 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonlyHeaderBadge.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.readonlyHeaderBadge.test.tsx @@ -14,9 +14,9 @@ * read-only-package tooltip. Both renderings answer "can you write this?" and * they disagreed, because they read different inputs: * - * • the CONTROLS read `!!resolved.allowOrgOverride && !readOnly` — the type + * • the CONTROLS read `!!resolved.allowEnvironmentOverlay && !readOnly` — the type * registry AND the host package gate; - * • the BADGE read `entry.allowOrgOverride` alone. `permission` was then one + * • the BADGE read `entry.allowEnvironmentOverlay` alone. `permission` was then one * of the 15 overlay-allowed types, so that flag was true and the badge said * "writable" no matter which package you were in. * @@ -30,19 +30,19 @@ * * The paragraph above called the controls' formula "the right answer". That was * wrong on the type half, and this suite could not see it: every case here - * drives the HOST gate (`readOnly` true/absent) with `allowOrgOverride: true` + * drives the HOST gate (`readOnly` true/absent) with `allowEnvironmentOverlay: true` * throughout, so it never rendered the one shape where the two disagree. * * `WritabilityBadge` is read-only exactly when - * `readOnly || (!allowOrgOverride && !allowRuntimeCreate)`; the controls were - * read-only when `readOnly || !allowOrgOverride`. The gap is precisely - * `!allowOrgOverride && allowRuntimeCreate` — which is the stock-boot - * `permission` shape today (objectstack#6483 set `allowOrgOverride: false` and + * `readOnly || (!allowEnvironmentOverlay && !allowRuntimeCreate)`; the controls were + * read-only when `readOnly || !allowEnvironmentOverlay`. The gap is precisely + * `!allowEnvironmentOverlay && allowRuntimeCreate` — which is the stock-boot + * `permission` shape today (objectstack#6483 set `allowEnvironmentOverlay: false` and * deliberately kept `allowRuntimeCreate: true`). There the badge said * "create-only" over 207 dead checkboxes. * * So the badge already consulted the full disjunction and the controls did not. - * Fixing the controls (`allowOrgOverride || allowRuntimeCreate`) makes the two + * Fixing the controls (`allowEnvironmentOverlay || allowRuntimeCreate`) makes the two * predicates byte-identical rather than merely closer — the convergence is * pinned below, across all four states, so neither side can drift again. * @@ -88,11 +88,11 @@ function makeClient() { // The type registry says this type IS overlay-allowed — the exact condition the // #4036 defect needed. (`permission` was in the server's overlay-allowed list // when this suite was written; objectstack#6483 has since flipped it to -// `allowOrgOverride: false` + `allowRuntimeCreate: true`. These cases keep the +// `allowEnvironmentOverlay: false` + `allowRuntimeCreate: true`. These cases keep the // original shape on purpose — it is the must-not-change control for #4446 — // while the convergence describe at the bottom drives the current one.) -let typeFlags: { allowOrgOverride?: boolean; allowRuntimeCreate?: boolean } = { - allowOrgOverride: true, +let typeFlags: { allowEnvironmentOverlay?: boolean; allowRuntimeCreate?: boolean } = { + allowEnvironmentOverlay: true, }; vi.mock('./useMetadata', () => ({ @@ -109,7 +109,7 @@ import { PermissionMatrixEditPage } from './PermissionMatrixEditor'; afterEach(() => { cleanup(); - typeFlags = { allowOrgOverride: true }; + typeFlags = { allowEnvironmentOverlay: true }; }); async function renderMatrix(props?: { readOnly?: boolean }) { @@ -198,30 +198,30 @@ describe('PermissionMatrixEditPage — header writability badge vs the package g describe('PermissionMatrixEditPage — badge and controls read ONE predicate (#4446)', () => { const cases: Array<{ label: string; - flags: { allowOrgOverride?: boolean; allowRuntimeCreate?: boolean }; + flags: { allowEnvironmentOverlay?: boolean; allowRuntimeCreate?: boolean }; readOnly?: boolean; expectWritable: boolean; }> = [ { label: 'read-only package dominates both flags', - flags: { allowOrgOverride: true, allowRuntimeCreate: true }, + flags: { allowEnvironmentOverlay: true, allowRuntimeCreate: true }, readOnly: true, expectWritable: false, }, { label: 'overlay-allowed type in a writable package', - flags: { allowOrgOverride: true, allowRuntimeCreate: false }, + flags: { allowEnvironmentOverlay: true, allowRuntimeCreate: false }, expectWritable: true, }, { // The stock-boot `permission` shape — the #4446 defect. label: 'runtime-creatable-only type in a writable package', - flags: { allowOrgOverride: false, allowRuntimeCreate: true }, + flags: { allowEnvironmentOverlay: false, allowRuntimeCreate: true }, expectWritable: true, }, { label: 'no write channel at all', - flags: { allowOrgOverride: false, allowRuntimeCreate: false }, + flags: { allowEnvironmentOverlay: false, allowRuntimeCreate: false }, expectWritable: false, }, ]; diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.retiredLifecycleKeys.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.retiredLifecycleKeys.test.tsx index 6e04a2ca9d..ca0bd95880 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.retiredLifecycleKeys.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.retiredLifecycleKeys.test.tsx @@ -63,7 +63,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true }], }), })); vi.mock('./AssignedUsersSection', () => ({ AssignedUsersSection: () => null })); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.scope.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.scope.test.tsx index c22f5730c8..6803c439c3 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.scope.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.scope.test.tsx @@ -84,7 +84,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: true }], }), })); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.setupCatalog-7611.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.setupCatalog-7611.test.tsx index 498d6f3d5d..176d57b8e6 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.setupCatalog-7611.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.setupCatalog-7611.test.tsx @@ -38,7 +38,7 @@ vi.mock('./useMetadata', () => ({ useMetadataTypes: () => ({ loading: false, error: null, - entries: [{ type: 'permission', label: 'Permission', allowOrgOverride: false, allowRuntimeCreate: true }], + entries: [{ type: 'permission', label: 'Permission', allowEnvironmentOverlay: false, allowRuntimeCreate: true }], }), })); vi.mock('./AssignedUsersSection', () => ({ diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx index 8d4190dd06..316836ab0b 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx @@ -144,7 +144,7 @@ interface ObjectSummary { * row under its REAL package id, so a tenant's own set reads back with a * code-looking `_packageId`. With (1) alone this function called that set * an artifact, and at the environment door — where `permission`'s - * `allowOrgOverride: false` then decides — the tenant's own matrix + * `allowEnvironmentOverlay: false` then decides — the tenant's own matrix * rendered read-only (objectui#4526). The framework fixed the same misread * by asking provenance (`isTenantAuthored`, cloud#970), and the server's * artifact lookup applies both exclusions. @@ -262,7 +262,7 @@ export interface PermissionMatrixEditPageProps { /** * Host-level read-only gate — set by the Studio Access pillar when the * surrounding PACKAGE is read-only. Independent of the TYPE-level - * `allowOrgOverride` writability: either gate locks the matrix (checkboxes, + * `allowEnvironmentOverlay` writability: either gate locks the matrix (checkboxes, * bulk buttons, name/label, facets), hides Save, and shows the read-only * badge — the badge hint names the package as the reason when this gate * is the one that tripped. @@ -302,17 +302,17 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, // governs it (objectui#4446, #4518): // // • TYPE gate — the metadata type must offer SOME runtime write channel. - // That is the DISJUNCTION `allowOrgOverride || allowRuntimeCreate`, not - // `allowOrgOverride` alone: those are two different doors, and this - // editor's Save goes through the second one. `allowOrgOverride` is + // That is the DISJUNCTION `allowEnvironmentOverlay || allowRuntimeCreate`, not + // `allowEnvironmentOverlay` alone: those are two different doors, and this + // editor's Save goes through the second one. `allowEnvironmentOverlay` is // permission to OVERLAY a code-shipped item per org; `allowRuntimeCreate` // is permission to author an item at runtime — which is what a save under // a `packageId` does (`mode: 'draft'` + `packageId`, ADR-0086 P0/P2). // The server's own gate is that same disjunction: `saveMetaItem` and // `promoteDraftForPublish` refuse only when BOTH are false // (`!isOverlayAllowed && !isRuntimeCreateAllowed`, metadata-protocol - // `protocol.ts`). Gating on `allowOrgOverride` alone therefore locked a - // surface the server accepts: `permission` is `allowOrgOverride: false` + // `protocol.ts`). Gating on `allowEnvironmentOverlay` alone therefore locked a + // surface the server accepts: `permission` is `allowEnvironmentOverlay: false` // (ADR-0005 forbids per-org overlay of a packaged permission set — silent // privilege drift) but `allowRuntimeCreate: true`, and objectstack `ee58392e1` // kept that second door open on purpose ("Runtime-created sets … ride @@ -322,7 +322,7 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, // when either flag is true" — DirectoryPage:171/175, EmbeddedItemEditor:93 // and ResourceEditPage:1332 all already read it that way. Read the raw // server `entry` like they do; `resolveResourceConfig` forwards - // `allowOrgOverride` ONLY, so a `resolved.allowRuntimeCreate` would be + // `allowEnvironmentOverlay` ONLY, so a `resolved.allowRuntimeCreate` would be // silently `undefined`. // // • ARTIFACT gate — the server's SECOND tier, added by objectui#4518. See @@ -348,9 +348,9 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, // } // // So for an item a code package SHIPS, `allowRuntimeCreate` is not enough — - // overwriting it is an OVERLAY, and overlaying needs `allowOrgOverride`. The + // overwriting it is an OVERLAY, and overlaying needs `allowEnvironmentOverlay`. The // method's own doc states the split: "overlaying a packaged item" (requires - // `allowOrgOverride`) vs "authoring a DB-only item" (requires only + // `allowEnvironmentOverlay`) vs "authoring a DB-only item" (requires only // `allowRuntimeCreate`). `permission` sits exactly in the gap — `false` / // `true` — so without this tier the matrix offered live checkboxes and a Save // button that failed at the end with a 403 instead of a surface that explains @@ -391,8 +391,8 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, // environment topology on the client, i.e. the probe the ruling forbids. const artifactTierApplies = !packageId && codeIsArtifact; const canWriteByType = artifactTierApplies - ? !!entry?.allowOrgOverride - : !!(entry?.allowOrgOverride || entry?.allowRuntimeCreate); + ? !!entry?.allowEnvironmentOverlay + : !!(entry?.allowEnvironmentOverlay || entry?.allowRuntimeCreate); // objectui#7611 — the fourth gate, the CALLER: the metadata door refuses a // save without `manage_metadata` (measured on objectstack main: 403 "Saving a // metadata item requires the `manage_metadata` capability."), and so does the @@ -419,7 +419,7 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, // no runtime write channel at all", which is also the refusal the server // reaches first (`!overlayAllowed && !runtimeCreateAllowed`). const lockedByArtifactTier = - artifactTierApplies && !entry?.allowOrgOverride && !!entry?.allowRuntimeCreate && canAuthor; + artifactTierApplies && !entry?.allowEnvironmentOverlay && !!entry?.allowRuntimeCreate && canAuthor; const locale = useMetadataLocale(); const t = React.useCallback((k: string) => translate(k, locale), [locale]); const OBJECT_ACTIONS = React.useMemo(() => getObjectActions(locale), [locale]); @@ -1125,7 +1125,7 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, // // Still the HOST gate only, deliberately — `WritabilityBadge` reads the // type flags itself, and its own read-only condition (`readOnly || - // (!allowOrgOverride && !allowRuntimeCreate)`) is now exactly `!writable` + // (!allowEnvironmentOverlay && !allowRuntimeCreate)`) is now exactly `!writable` // above. Passing `!writable` here instead would collapse that to one // input but make a TYPE-gate lock claim the PACKAGE as its reason, since // this branch's tooltip is `engine.studio.pkg.readonlyHint` — trading the @@ -1335,13 +1335,13 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, // DOES have a runtime write channel — a brand-new set authored // here saves fine — it is this PARTICULAR set that is packaged. // • type gate — the metadata type offers no runtime write - // channel at all (`allowOrgOverride` AND `allowRuntimeCreate` + // channel at all (`allowEnvironmentOverlay` AND `allowRuntimeCreate` // both false). It used to read "OS_METADATA_WRITABLE not // enabled", which blamed a deployment env var for what is a // per-type registry declaration. That wording had NO reachable // honest case: `OS_METADATA_WRITABLE` does not sit beside - // `allowOrgOverride`, it FLIPS it — `getMetaTypes` emits - // `allowOrgOverride: base.allowOrgOverride || isEnvOverridden` + // `allowEnvironmentOverlay`, it FLIPS it — `getMetaTypes` emits + // `allowEnvironmentOverlay: base.allowEnvironmentOverlay || isEnvOverridden` // — so whenever the hatch is on for this type the surface is // writable and this badge does not render. The env var is a // documented REMEDY (it appears in the server's own 403 text), diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.artifactTier.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.artifactTier.test.tsx index 3df88dcffd..a9bda8df94 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.artifactTier.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.artifactTier.test.tsx @@ -33,7 +33,7 @@ * artifact reading rather than unlocking on a guess. * * `object` is the type under test because it is the type the card reports and - * the one where the two tiers actually diverge: `allowOrgOverride: false` (the + * the one where the two tiers actually diverge: `allowEnvironmentOverlay: false` (the * server refuses overlays of packaged objects with `NOT_OVERRIDABLE`) with * `allowRuntimeCreate: true` (orgs author their own). A type with both flags * true would be writable under either tier and could not detect the bug. @@ -63,7 +63,7 @@ const mockClient = { }; // The registry entry for `object`: overlaying a PACKAGED object is refused by -// the server (`allowOrgOverride: false`), authoring your own is not +// the server (`allowEnvironmentOverlay: false`), authoring your own is not // (`allowRuntimeCreate: true`). Which of those two gates this page applies is // exactly what the tier test decides. vi.mock('./useMetadata', async (importOriginal) => { @@ -79,7 +79,7 @@ vi.mock('./useMetadata', async (importOriginal) => { type: 'object', name: 'object', label: 'Object', - allowOrgOverride: false, + allowEnvironmentOverlay: false, allowRuntimeCreate: true, schema: { type: 'object', diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.celGate.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.celGate.test.tsx index 23cdeb43d9..50f8d0ea4f 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.celGate.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.celGate.test.tsx @@ -52,10 +52,10 @@ vi.mock('./useMetadata', async (importOriginal) => { return { ...mod, useMetadataClient: () => mockClient, - // `allowOrgOverride` is what makes the item writable, and therefore what + // `allowEnvironmentOverlay` is what makes the item writable, and therefore what // makes a Save button exist at all. useMetadataTypes: () => ({ - entries: [{ type: 'page', name: 'page', label: 'Page', allowOrgOverride: true }], + entries: [{ type: 'page', name: 'page', label: 'Page', allowEnvironmentOverlay: true }], }), }; }); diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.choiceWithoutOptions-11253.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.choiceWithoutOptions-11253.test.tsx index 436ebee255..9c8e1a8665 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.choiceWithoutOptions-11253.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.choiceWithoutOptions-11253.test.tsx @@ -38,7 +38,7 @@ const OBJECT_ENTRY = { name: 'object', label: 'Object', // What makes the item writable, and so what turns the designer on. - allowOrgOverride: true, + allowEnvironmentOverlay: true, schema: { type: 'object', properties: { label: { type: 'string', title: 'Label' } } }, }; diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.conditionScope.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.conditionScope.test.tsx index de60d48be7..c86a1216dc 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.conditionScope.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.conditionScope.test.tsx @@ -72,7 +72,7 @@ const HOOK_ENTRY = { name: 'hook', label: 'Hook', // What makes the item writable, and therefore what makes an Edit button exist. - allowOrgOverride: true, + allowEnvironmentOverlay: true, schema: { type: 'object', properties: { diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.defaultGate.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.defaultGate.test.tsx index 3106b7cf64..62c4cbaa07 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.defaultGate.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.defaultGate.test.tsx @@ -60,7 +60,7 @@ vi.mock('./useMetadata', async (importOriginal) => { ...mod, useMetadataClient: () => mockClient, useMetadataTypes: () => ({ - entries: [{ type: 'view', name: 'view', label: 'View', allowOrgOverride: true }], + entries: [{ type: 'view', name: 'view', label: 'View', allowEnvironmentOverlay: true }], }), }; }); diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.docEditor-10188.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.docEditor-10188.test.tsx index b35ad72121..c072bf1325 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.docEditor-10188.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.docEditor-10188.test.tsx @@ -99,7 +99,7 @@ vi.mock('./useMetadata', async (importOriginal) => { type: 'doc', label: 'Documentation', domain: 'system', - allowOrgOverride: false, + allowEnvironmentOverlay: false, allowRuntimeCreate: true, schema: DOC_JSON_SCHEMA, }, diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.draftVersionConflict-11773.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.draftVersionConflict-11773.test.tsx index c5ef312983..b37e1604f8 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.draftVersionConflict-11773.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.draftVersionConflict-11773.test.tsx @@ -109,7 +109,7 @@ vi.mock('./useMetadata', async (importOriginal) => { ...mod, useMetadataClient: () => mockClient, useMetadataTypes: () => ({ - entries: [{ type: 'page', name: 'page', label: 'Page', allowOrgOverride: true, schema: { required: [] } }], + entries: [{ type: 'page', name: 'page', label: 'Page', allowEnvironmentOverlay: true, schema: { required: [] } }], }), }; }); diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.environmentOverlay-12141.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.environmentOverlay-12141.test.tsx new file mode 100644 index 0000000000..52fc5171b9 --- /dev/null +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.environmentOverlay-12141.test.tsx @@ -0,0 +1,204 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * objectui#12141 — the editor's write gate for a PACKAGED item reads the + * `GET /meta/types` field `allowEnvironmentOverlay`, the name the server + * writes after objectstack-ai/objectstack#22340 S2. + * + * ## Why the five overlay types are the population + * + * An item a code package ships sits in the editor's artifact tier, and that + * tier opens on the overlay flag ALONE: `allowRuntimeCreate` authors new items + * and says nothing about overwriting a shipped one. The registry grants the + * overlay to five types — `view`, `dashboard`, `report`, `translation` and + * `email_template` — so those five are the types whose packaged items go + * read-only the moment this page reads a field the server does not write. + * Nothing else would notice: objectui types the entry itself, so no build sees + * a missed rename, and the item falls back to a lock banner that reads as + * deliberate. + * + * ## What is pinned, each against a control + * + * - each of the five, served the way the server writes it (both flags true), + * opens the packaged item for editing; + * - the same five served WITHOUT the field keep the packaged item locked: + * the field is what opens it, and the editor reads no other name; + * - the negative-control type the card names: `api`, which takes no overlay + * and no runtime create, stays read-only, and its banner names the + * `OS_METADATA_WRITABLE` hatch the server reads (with the hatch's reach, + * ADR-0131 D6) and no registry flip, in both locales. + * + * The entries carry the registry flags objectstack S2 declares for these types + * (`DEFAULT_METADATA_TYPE_REGISTRY`, `@objectstack/spec`). + */ + +import '@testing-library/jest-dom/vitest'; +import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; +import { render, screen, cleanup, waitFor } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import type { RichMetadataTypeEntry } from './useMetadata'; + +const OVERLAY_TYPES = ['view', 'dashboard', 'report', 'translation', 'email_template'] as const; + +const ITEM = 'showcase_item'; +const SCHEMA = { + type: 'object', + properties: { name: { type: 'string' }, label: { type: 'string' } }, +}; + +/** The type entries the mocked `/meta/types` read serves; set per case. */ +let served: RichMetadataTypeEntry[] = []; +let localeFixture: 'en-US' | 'zh-CN' = 'en-US'; + +const mockClient = { + list: vi.fn(async () => []), + listDrafts: vi.fn(async () => []), + get: vi.fn(async () => null), + getDraft: vi.fn(async () => null), + references: vi.fn(async () => []), + layered: vi.fn(async () => packagedItem()), +}; + +vi.mock('./useMetadata', async (importOriginal) => { + const mod = await importOriginal(); + return { + ...mod, + useMetadataClient: () => mockClient, + useMetadataTypes: () => ({ loading: false, error: null, entries: served }), + }; +}); + +vi.mock('./i18n', async (importOriginal) => ({ + ...(await importOriginal>()), + useMetadataLocale: () => localeFixture, +})); + +import { MetadataResourceEditPage } from './ResourceEditPage'; + +/** An item a code package ships, as the layered read serves it. */ +function packagedItem() { + const def = { name: ITEM, label: 'Showcase item' }; + return { + code: { ...def, _packageId: 'com.example.showcase', _provenance: 'package' }, + overlay: null, + overlayScope: null, + effective: def, + provenance: 'package', + packageId: 'com.example.showcase', + editable: true, + }; +} + +/** The entry the S2 server writes for one of the five: both doors open. */ +function overlayEntry(type: string): RichMetadataTypeEntry { + return { + type, + label: type, + allowEnvironmentOverlay: true, + allowRuntimeCreate: true, + overrideSource: 'registry', + supportsOverlay: true, + schema: SCHEMA, + }; +} + +async function renderEditor(type: string) { + render( + + + , + ); + await waitFor(() => expect(mockClient.layered).toHaveBeenCalled()); +} + +function banner(): HTMLElement | null { + return screen.queryByTestId('readonly-banner'); +} + +async function lockBanner(): Promise { + return waitFor(() => { + const b = banner(); + expect(b).not.toBeNull(); + return b as HTMLElement; + }); +} + +beforeEach(() => { + localeFixture = 'en-US'; +}); + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +describe('ResourceEditPage — a packaged item of an overlay type opens on `allowEnvironmentOverlay` (objectui#12141)', () => { + it.each(OVERLAY_TYPES)('%s: the packaged item is editable when the server writes the field', async (type) => { + served = [overlayEntry(type)]; + await renderEditor(type); + + // Edit is offered only when `canWrite` holds (the page draws it in more + // than one place), and the lock banner is absent. + expect((await screen.findAllByRole('button', { name: /^Edit$/ })).length).toBeGreaterThan(0); + expect(banner()).toBeNull(); + }); + + it.each(OVERLAY_TYPES)('%s: without the field the same packaged item stays locked', async (type) => { + // A server that does not write `allowEnvironmentOverlay`: the runtime-create + // door is still open, and it does not open an item a package ships. + const { allowEnvironmentOverlay: _omitted, ...withoutField } = overlayEntry(type); + served = [withoutField]; + await renderEditor(type); + + const el = await lockBanner(); + expect(el.textContent).toContain('provided by an installed package'); + expect(screen.queryAllByRole('button', { name: /^Edit$/ })).toHaveLength(0); + }); +}); + +describe('ResourceEditPage — the negative control: a type without the field stays read-only (objectui#12141)', () => { + const API_ENTRY: RichMetadataTypeEntry = { + type: 'api', + label: 'API Endpoint', + allowEnvironmentOverlay: false, + allowRuntimeCreate: false, + overrideSource: 'registry', + supportsOverlay: false, + schema: SCHEMA, + }; + + it('`api` is read-only, and its banner names OS_METADATA_WRITABLE and no registry flip', async () => { + served = [API_ENTRY]; + await renderEditor('api'); + + const el = await lockBanner(); + expect(screen.queryAllByRole('button', { name: /^Edit$/ })).toHaveLength(0); + const text = el.textContent ?? ''; + expect(text).toContain('read-only for safety'); + // The one spelling the server reads, drawn as a code token… + expect(el.querySelector('code')?.textContent).toBe('OS_METADATA_WRITABLE'); + expect(text).toContain('OS_METADATA_WRITABLE'); + // …with its reach (ADR-0131 D6): it never opens what a package ships. + expect(text).toContain('never an item a package ships'); + // Not the legacy name, and no remedy nobody can perform. + expect(text).not.toContain('OBJECTSTACK_METADATA_WRITABLE'); + expect(text).not.toMatch(/registry/i); + expect(text).not.toContain('allowEnvironmentOverlay'); + expect(text).not.toContain('{'); + }); + + it('zh-CN: the same banner names OS_METADATA_WRITABLE and no registry flip', async () => { + localeFixture = 'zh-CN'; + served = [API_ENTRY]; + await renderEditor('api'); + + const el = await lockBanner(); + const text = el.textContent ?? ''; + expect(text).toContain('出于安全考虑'); + expect(text).toContain('OS_METADATA_WRITABLE'); + expect(text).toContain('永远不会开放由包提供的条目'); + expect(text).not.toContain('OBJECTSTACK_METADATA_WRITABLE'); + expect(text).not.toContain('注册表'); + expect(text).not.toContain('{'); + }); +}); diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.lateRegistration-11939.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.lateRegistration-11939.test.tsx index cba599f571..c9b54b5beb 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.lateRegistration-11939.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.lateRegistration-11939.test.tsx @@ -49,7 +49,7 @@ vi.mock('./useMetadata', async (importOriginal) => { ...mod, useMetadataClient: () => mockClient, useMetadataTypes: () => ({ - entries: [{ type: TYPE, name: TYPE, label: 'Gizmo', allowOrgOverride: true }], + entries: [{ type: TYPE, name: TYPE, label: 'Gizmo', allowEnvironmentOverlay: true }], }), }; }); diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.layersUnsaved-11799.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.layersUnsaved-11799.test.tsx index f00b021cec..c6fc478e5e 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.layersUnsaved-11799.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.layersUnsaved-11799.test.tsx @@ -108,7 +108,7 @@ vi.mock('./useMetadata', async (importOriginal) => { useMetadataTypes: () => ({ loading: false, entries: [ - { type: 'doc', label: 'Documentation', domain: 'system', allowOrgOverride: false, allowRuntimeCreate: true, schema: DOC_JSON_SCHEMA }, + { type: 'doc', label: 'Documentation', domain: 'system', allowEnvironmentOverlay: false, allowRuntimeCreate: true, schema: DOC_JSON_SCHEMA }, ], }), }; diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.lockBanner.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.lockBanner.test.tsx index 54a2b8ffb2..15469371d1 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.lockBanner.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.lockBanner.test.tsx @@ -78,7 +78,7 @@ vi.mock('./useMetadata', async (importOriginal) => { type: 'object', name: 'object', label: 'Object', - allowOrgOverride: false, + allowEnvironmentOverlay: false, allowRuntimeCreate: true, schema: { type: 'object', diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.navDeepLink-11153.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.navDeepLink-11153.test.tsx index 5653deb37e..1a63432fcd 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.navDeepLink-11153.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.navDeepLink-11153.test.tsx @@ -59,7 +59,7 @@ vi.mock('./useMetadata', async (importOriginal) => { type: 'app', name: 'app', label: 'App', - allowOrgOverride: typeTier.writable, + allowEnvironmentOverlay: typeTier.writable, allowRuntimeCreate: typeTier.writable, }, ], diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.pageRequiresStamp-11357.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.pageRequiresStamp-11357.test.tsx index 38cef59f85..c88e2b65ec 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.pageRequiresStamp-11357.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.pageRequiresStamp-11357.test.tsx @@ -187,7 +187,7 @@ vi.mock('./useMetadata', async (importOriginal) => { ...mod, useMetadataClient: () => mockClient, useMetadataTypes: () => ({ - entries: [{ type: 'page', name: 'page', label: 'Page', allowOrgOverride: true, schema: { required: [] } }], + entries: [{ type: 'page', name: 'page', label: 'Page', allowEnvironmentOverlay: true, schema: { required: [] } }], }), }; }); diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.permissionSetsFeed-12126.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.permissionSetsFeed-12126.test.tsx index a88beda464..a673cf9ef0 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.permissionSetsFeed-12126.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.permissionSetsFeed-12126.test.tsx @@ -90,7 +90,7 @@ vi.mock('./useMetadata', async (importOriginal) => { type: 'position', name: 'position', label: 'Position', - allowOrgOverride: false, + allowEnvironmentOverlay: false, allowRuntimeCreate: true, schema: SCHEMA, form: form.current, diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.pickerLoadFailure.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.pickerLoadFailure.test.tsx index a25c1e6726..d2f10de959 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.pickerLoadFailure.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.pickerLoadFailure.test.tsx @@ -131,7 +131,7 @@ vi.mock('./useMetadata', async (importOriginal) => { type: 'object', name: 'object', label: 'Object', - allowOrgOverride: true, + allowEnvironmentOverlay: true, allowRuntimeCreate: true, schema: { type: 'object', properties: schemaProps.current }, }, diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.picklistReadOnly-10202.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.picklistReadOnly-10202.test.tsx index 0cea1e6557..f9dd96e73b 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.picklistReadOnly-10202.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.picklistReadOnly-10202.test.tsx @@ -5,7 +5,7 @@ * resource route (ruling 6006062645 item (c)). * * The kind is package-owned: its `/meta/types` row declares - * `allowRuntimeCreate: false` and `allowOrgOverride: false`, and the layered + * `allowRuntimeCreate: false` and `allowEnvironmentOverlay: false`, and the layered * read of a packaged list answers `lock: 'full'`, `editable: false`, * `deletable: false` (all measured on objectstack `main`). The generic list and * edit pages already honour that row, so this card adds no read-only mechanism @@ -28,7 +28,7 @@ const PICKLIST_ENTRY = { label: 'Picklist', description: 'Shared option list that select fields reference by name', supportsOverlay: false, - allowOrgOverride: false, + allowEnvironmentOverlay: false, allowRuntimeCreate: false, supportsVersioning: false, domain: 'data', diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.picklistServedOptions-10202.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.picklistServedOptions-10202.test.tsx index b6cb852f6d..0d36375459 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.picklistServedOptions-10202.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.picklistServedOptions-10202.test.tsx @@ -70,7 +70,7 @@ const OBJECT_ENTRY = { type: 'object', name: 'object', label: 'Object', - allowOrgOverride: true, + allowEnvironmentOverlay: true, schema: { type: 'object', properties: { label: { type: 'string', title: 'Label' } } }, }; diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.publishPackageBinding.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.publishPackageBinding.test.tsx index 9ac9aeeb27..5f88876850 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.publishPackageBinding.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.publishPackageBinding.test.tsx @@ -91,7 +91,7 @@ vi.mock('./useMetadata', async (importOriginal) => { ...mod, useMetadataClient: () => mockClient, useMetadataTypes: () => ({ - entries: [{ type: 'page', name: 'page', label: 'Page', allowOrgOverride: true }], + entries: [{ type: 'page', name: 'page', label: 'Page', allowEnvironmentOverlay: true }], }), }; }); diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.readDecorationStrip.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.readDecorationStrip.test.tsx index b4c4bb9742..2be9116367 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.readDecorationStrip.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.readDecorationStrip.test.tsx @@ -126,7 +126,7 @@ vi.mock('./useMetadata', async (importOriginal) => { type: 'page', name: 'page', label: 'Page', - allowOrgOverride: true, + allowEnvironmentOverlay: true, // The most lenient live server there can be. It is handed to the // cross-repo spec-skew cure, which only drops TOP-LEVEL absent // required fields — it structurally cannot suppress an diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.referencesErrorState.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.referencesErrorState.test.tsx index fda4ac3fd6..58a7c8f9ec 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.referencesErrorState.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.referencesErrorState.test.tsx @@ -104,7 +104,7 @@ vi.mock('./useMetadata', async (importOriginal) => { type: 'object', name: 'object', label: 'Object', - allowOrgOverride: true, + allowEnvironmentOverlay: true, allowRuntimeCreate: true, schema: { type: 'object', diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.requiredInGatedSection-6900.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.requiredInGatedSection-6900.test.tsx index f89b567fbb..c2dc5ed2c6 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.requiredInGatedSection-6900.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.requiredInGatedSection-6900.test.tsx @@ -99,7 +99,7 @@ vi.mock('./useMetadata', async (importOriginal) => { ...mod, useMetadataClient: () => mockClient, useMetadataTypes: () => ({ - entries: [{ type: 'view', name: 'view', label: 'View', allowOrgOverride: true }], + entries: [{ type: 'view', name: 'view', label: 'View', allowEnvironmentOverlay: true }], }), }; }); diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.resetVerdict.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.resetVerdict.test.tsx index 79b18e6009..efe6f7e160 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.resetVerdict.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.resetVerdict.test.tsx @@ -89,7 +89,7 @@ vi.mock('./useMetadata', async (importOriginal) => { type: 'object', name: 'object', label: 'Object', - allowOrgOverride: true, + allowEnvironmentOverlay: true, allowRuntimeCreate: true, schema: { type: 'object', diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.schemaAdvisory.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.schemaAdvisory.test.tsx index c349d7bc89..d08fb2522c 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.schemaAdvisory.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.schemaAdvisory.test.tsx @@ -103,7 +103,7 @@ vi.mock('./useMetadata', async (importOriginal) => { type: 'page', name: 'page', label: 'Page', - allowOrgOverride: true, + allowEnvironmentOverlay: true, // The live server schema handed to the skew root-cure. `required: []` // is the most lenient server there can be; it still cannot suppress // an `unrecognized_keys` issue, whose path is empty. diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.servedDraftTakenAsIs-10765.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.servedDraftTakenAsIs-10765.test.tsx index 0bcaf427da..5f9ae583a6 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.servedDraftTakenAsIs-10765.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.servedDraftTakenAsIs-10765.test.tsx @@ -173,8 +173,8 @@ vi.mock('./useMetadata', async (importOriginal) => { useMetadataClient: () => mockClient, useMetadataTypes: () => ({ entries: [ - { type: 'report', name: 'report', label: 'Report', allowOrgOverride: true, schema: { required: [] } }, - { type: 'page', name: 'page', label: 'Page', allowOrgOverride: true, schema: { required: [] } }, + { type: 'report', name: 'report', label: 'Report', allowEnvironmentOverlay: true, schema: { required: [] } }, + { type: 'page', name: 'page', label: 'Page', allowEnvironmentOverlay: true, schema: { required: [] } }, ], }), }; diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.serverRefusalGate.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.serverRefusalGate.test.tsx index 6e1fc82ccd..794e00e870 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.serverRefusalGate.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.serverRefusalGate.test.tsx @@ -150,7 +150,7 @@ vi.mock('./useMetadata', async (importOriginal) => { type: 'page', name: 'page', label: 'Page', - allowOrgOverride: true, + allowEnvironmentOverlay: true, // The most lenient server schema there can be — so nothing below is // an artefact of the client's own Zod pass finding something. schema: { required: [] }, diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.setupCatalog-7611.test.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.setupCatalog-7611.test.tsx index debd8bdb52..7de3aabb75 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.setupCatalog-7611.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.setupCatalog-7611.test.tsx @@ -42,8 +42,8 @@ vi.mock('./useMetadata', async (importOriginal) => { loading: false, error: null, entries: [ - { type: 'position', name: 'position', label: 'Position', allowOrgOverride: false, allowRuntimeCreate: true, schema: SCHEMA }, - { type: 'dashboard', name: 'dashboard', label: 'Dashboard', allowOrgOverride: false, allowRuntimeCreate: true, schema: SCHEMA }, + { type: 'position', name: 'position', label: 'Position', allowEnvironmentOverlay: false, allowRuntimeCreate: true, schema: SCHEMA }, + { type: 'dashboard', name: 'dashboard', label: 'Dashboard', allowEnvironmentOverlay: false, allowRuntimeCreate: true, schema: SCHEMA }, ], }), }; diff --git a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx index cda2f3c897..0fd2d0c129 100644 --- a/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx +++ b/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx @@ -734,7 +734,7 @@ function MetadataResourceEditPageImpl({ // Form edit mode. The form is read-only by default — admins land in a // "view" state and must click Edit to mutate, mirroring the Salesforce / // Notion convention. createMode is always editing (you can't view what - // doesn't exist yet). Truly read-only types (no allowOrgOverride) stay + // doesn't exist yet). Truly read-only types (no allowEnvironmentOverlay) stay // read-only regardless. const [editing, setEditing] = React.useState(!!createMode); // Currently selected sub-element (e.g. a dashboard widget). The @@ -1332,7 +1332,7 @@ function MetadataResourceEditPageImpl({ // ── Is this item a packaged ARTIFACT, or org-authored content? ──────── // The two-tier authorization model (PR-10d.7) applies a different gate to - // each: overlaying a code-shipped artifact needs `allowOrgOverride`, while + // each: overlaying a code-shipped artifact needs `allowEnvironmentOverlay`, while // authoring org content needs only `allowRuntimeCreate`. Several call sites // below key off the answer, so it is derived ONCE here — the previous two // in-place copies are how the two drifted from the server (objectui#4308). @@ -1439,8 +1439,8 @@ function MetadataResourceEditPageImpl({ // this used to be an in-place copy of the artifact heuristic, so the two // could (and did) answer differently for one item (objectui#4308). const cw = isArtifactItem - ? !!entry?.allowOrgOverride - : !!(entry?.allowOrgOverride || entry?.allowRuntimeCreate); + ? !!entry?.allowEnvironmentOverlay + : !!(entry?.allowEnvironmentOverlay || entry?.allowRuntimeCreate); if (!cw) return; designerAutoOnRef.current = key; setEditing(true); @@ -1859,8 +1859,8 @@ function MetadataResourceEditPageImpl({ // Two-tier authorization (PR-10d.7) — hoisted above the early `loading` // return so the auto-save / keyboard / blocker effects below can read // them. Recomputed cheaply on every render. - // - artifact-backed items need allowOrgOverride - // - org-authored items need allowOrgOverride OR allowRuntimeCreate + // - artifact-backed items need allowEnvironmentOverlay + // - org-authored items need allowEnvironmentOverlay OR allowRuntimeCreate // - createMode is always writable (the server will gate on intent) // Which tier this item is in is decided once, next to the designer // auto-on effect above — see `isArtifactItem` for why a non-null `code` @@ -1879,10 +1879,10 @@ function MetadataResourceEditPageImpl({ const lockReason = layered?.lockReason; const isLocked = layered?.lock && layered.lock !== 'none'; const canWriteByType = createMode - ? !!(entry?.allowOrgOverride || entry?.allowRuntimeCreate) + ? !!(entry?.allowEnvironmentOverlay || entry?.allowRuntimeCreate) : isArtifactItem - ? !!entry?.allowOrgOverride - : !!(entry?.allowOrgOverride || entry?.allowRuntimeCreate); + ? !!entry?.allowEnvironmentOverlay + : !!(entry?.allowEnvironmentOverlay || entry?.allowRuntimeCreate); // objectui#7611 — the CALLER tier. The metadata door refuses every save // without `manage_metadata` (measured on objectstack main: 403 "Saving a // metadata item requires the `manage_metadata` capability."), so a caller @@ -2117,7 +2117,7 @@ function MetadataResourceEditPageImpl({ // When the form is "live" but not yet in edit mode, it renders as // read-only. createMode is always editing; truly read-only types - // (no allowOrgOverride) ignore the editing toggle entirely. + // (no allowEnvironmentOverlay) ignore the editing toggle entirely. const formReadOnly = readOnly || (!editing && !createMode); // Note: URL `?tab=` deep-links were repurposed to open side-panel @@ -2275,7 +2275,7 @@ function MetadataResourceEditPageImpl({ - Form-only types: keep the Salesforce-style Edit / Cancel convention (View → click Edit → mutate → Save or Cancel). - createMode: always editing, Save only. - - Truly read-only types (no allowOrgOverride): no buttons. */} + - Truly read-only types (no allowEnvironmentOverlay): no buttons. */} {canWrite && !createMode && !editing && !PreviewComponent && (