Skip to content

Commit 150da61

Browse files
test: the stage-8 test half — suites stop depending on the four catalog row objects (#15204 stage 8, S8-T) (#22962)
Clause-②: no Part of #15204 (C3 stage 8, the test half: S8-T). The code half (S8) is built ahead on `claude/issue-15204-s8-catalog-objects` and lands right after this PR. #15204 remains open. ## What this changes Test files only. Every edit keeps the suite green on `main` **and** on `main` + S8, where the four security-catalog row objects (`sys_position`, `sys_permission_set`, `sys_position_permission_set`, `sys_capability`) are no longer registered (ADR-0131 D3). - **A testkit for the retired tables.** `plugin-security/src/__tests__/legacy-catalog-objects.testkit.ts` holds the four tables' shapes (fields, indexes, validations only), for suites that model an existing database whose rows outlive the object declarations until the organization-ownership migration (ADR-0131 D10). The suites that registered the object declarations import the shapes from it; `withLegacyCatalogTables()` adds the shapes a plugin no longer declares. plugin-auth's two grant-reader suites register the `sys_permission_set` shape the same way (`plugin-auth/src/__tests__/legacy-catalog-objects.testkit.ts`, a test-only copy: the plugin-security file is no export, and a relative import would make a test read another package's source) before seeding the platform-bucket rows. - **Deleted, because they pin what S8 removes:** the object-declaration pins (`sys-*.organization-unique`, `sys-capability-deactivation-claim`, `clone-carryover`, the junction/position cases of `rbac-objects`, `reserved-identity-names`, `managed-by-system-data`, `sys-security-assignment-display-title`, `rbac-objects-bulk-exposure`), `position-write-through.test.ts` (S8 unwires that middleware; 7b deletes the module), `bootstrap-platform-admin-refused-existence-read.test.ts` (the legacy-row read S8 removes), `row-active.test.ts` (S8 deletes `isRowActive`), pin 6 of `packaged-permission-set-lock.test.ts`, and the dogfood files for the data-door write-throughs, the projection, the row lock and the junction gates: `two-doors-permission`, `declared-position-provenance`, `permission-set-lock-row-provenance`, `permission-set-write-through-package-binding`, `showcase-permission-projection`, `position-environment-write-through`, `position-held-name-setup-create`. `ensureLegacyPermissionSetRow` leaves the dogfood catalog helper with its last callers. `walled-catalog-data-door.test.ts` (#22895) stays with `main`'s bytes: S8 deletes it together with the walled data-door refusal it pins. - **Fixtures moved off the rows:** dogfood suites that authored a permission set through `POST /api/v1/data/sys_permission_set` use `PUT /api/v1/meta/permission/:name` (or `protocol.saveMetaItem`); checks that a set "was seeded" read the security catalog (`catalogPermissionSet`) or the registry; vestigial `sys_position` row inserts are gone. `mail-message-pool-read-gate` reads its fixture sets from the catalog (split out and landed on `main` as #23159, `b8f226c440`, the same blob). - **Pins made to span the change:** - censuses of a retired table run only where the object is registered (`showcase-permission-seeding`, the runtime seeder-copy suite, `permission-set-clone-door`, `showcase-d7-default-profile`); - `wall-catalog-refusal`'s data-door case answers `403 PERMISSION_DENIED` while the row objects are registered and `404 OBJECT_NOT_FOUND` once they are retired, with no definition either way; - `builtin-positions.boot`'s walled golden keeps `control: create a position row` (`403 PERMISSION_DENIED`, the walled data-door refusal of `walled-catalog-data-door.ts`) wherever the plugin declares `sys_position`, and leaves it out only once the object is retired; every other key is unchanged; - `cleanup-package-permissions.test.ts` hands the sweep D's `UninstallDeclared` shape (`{ permission: [...] }`) and asserts what holds both ways; D's two named-grant pins are restated over this fixture; - the clone-route refusal prose is pinned around the route; the uninstall-cleanup runner request may carry `declared`; page/registry floors drop by the retired page and the four names (the record-page label-map floor is 38, with B2's two maps gone); the tenancy-inventory and dangling-reference fixtures use surviving security tables. - **New pins:** - `bootstrap-platform-admin-fresh-boot.golden.test.ts`: a fresh `single`-posture engine holding only the plugin's own objects promotes its first human (E6's first pin). E6's second pin is the existing `grant-readers-unnamed-grant` suite. - `permission-metadata-door-reaches-catalog.dogfood.test.ts`: a create, an edit and a delete through `/api/v1/meta/permission` each reach the security catalog the resolver reads (the premise E1 → A rested on). ## Merge of `main` after D, and the re-check against D Merge commit `a805619472` takes `main` at `f931e765b7` (D #22900 is `c8a9783eca`). No rebase, no force-push; generated files went through `scripts/pm/os-regen-merge.sh` (this branch touches none; the rerun's step 2 took nothing). Conflicts, all hand-written test files: - modify/delete, S8-T's deletion taken over `main`'s edit: `bootstrap-platform-admin-refused-existence-read` (D), `position-write-through` (#22895), and the dogfood `permission-set-lock-row-provenance`, `permission-set-write-through-package-binding`, `showcase-permission-projection`, `two-doors-permission` (T2, and #22874's rename); - `rbac-objects`: S8-T's removal of the retired declaration pins (`main` had only dropped one case inside them); - `activity-text-predicate`, `approval-snapshot-masked-field`, `sharing-rule-org-less-caller`, `showcase-crud-persona-matrix`: `main`'s `catalogPermissionSet` reads kept, S8-T's `sys_position` insert removals kept; - `showcase-permission-zoo`: S8-T's side; `showcase-fls-read-mask-strip`: S8-T's metadata-door authoring plus `main`'s catalog read; - `bootstrap-platform-admin-authenticable-target`, `-promotion-selection`, `per-organization-catalog`: `main`'s `seedLegacyPlatformBucket` import (and T1's removal of `bootstrapDeclaredPermissions`) with the testkit imports; - `standalone-stack-seeder-declaration-copy`: `main`'s "no row" pin kept beside S8-T's registered-object guard; - `platform-page-i18n-parity`: the label-map floor restated for B2 and the retired page. D changed after `fcaedfdaa1`, the base S8-T was first built against. Every case the stage-8 report pinned against D was re-checked on this merge: | case | result | |:---|:---| | S8-T's deletions over D's edits | taken; the deleted suites' assertions are what S8 removes | | `catalogPermissionSet` reads in the four dogfood files | kept; green on both trees | | `showcase-fls-read-mask-strip`, `showcase-permission-zoo` | S8-T's side (FLS also keeps `main`'s catalog read); green on both trees | | `ensureLegacyPermissionSetRow` | deleted from the dogfood helper; no caller left (`git grep`) | | `showcase-permission-seeding` census | guarded by `getObject('sys_permission_set')`; green on both trees | | plugin-security imports | `seedLegacyPlatformBucket` plus the testkit; green on both trees | | D's uninstall cleanup by catalog set name | D landed `UninstallDeclared` (`{ permission }`), not the bare array S8-T was written against: `cleanup-package-permissions.test.ts` now passes that shape, D's two named-grant pins restated; green on both trees | | E6 fresh-boot golden | green on both trees; ablation below | | E6 second pin (`grant-readers-unnamed-grant`, plugin-security and plugin-auth) | green on both trees; plugin-auth's copy registers the legacy table before seeding | | E1 premise (`permission-metadata-door-reaches-catalog`) | green on both trees | | uninstall-cleanup runner `declared` | green on both trees | Suites `main` brought in or changed after S8-T's base that read the four objects, made to hold on both trees here: `wall-catalog-refusal`, `mail-message-pool-read-gate`, plugin-auth's two grant-reader suites (legacy table registered before the T2 bucket seed, seat ruling), `builtin-positions.boot` (walled create judged while `sys_position` is declared, seat ruling). `walled-catalog-data-door.test.ts` is kept with `main`'s bytes: green on `main` + S8-T, red on `main` + S8-T + S8 by design, and S8 deletes it with the door. ## Verification Two trees, built locally through `scripts/pm/os-verify-lock.sh`: - **`main` + S8-T**: this branch. - **`main` + S8-T + S8**: a local, never-pushed scratch tree, S8's own commit `dbae752f6e` replayed onto this branch, its conflicts with D's final form resolved the way the S8 build-ahead resolved them (D's `captureUninstallDeclared` / `UninstallDeclared` kept; the walled data-door refusal unregistered with the write-throughs). | suite | `main` + S8-T | `main` + S8-T + S8 | |:---|:---|:---| | plugin-security, full | 186 files, 3,831 passed, 45 skipped (at `9d55c0589a`, `walled-catalog-data-door.test.ts` included) | 185 files, 3,815 passed, 45 skipped, 4 failed in `builtin-positions.boot` (at S8-T `008ca32a75`), fixed below | | `builtin-positions.boot` | 52 passed (at `9d9bdd96b2`; the walled create key judged, `403 PERMISSION_DENIED`) | 52 passed (the key left out) | | `walled-catalog-data-door` (`main`'s bytes) | 12 passed (at `9d9bdd96b2`) | red by design (`Cannot find module './objects/sys-position.object.js'`); S8 deletes it with the door | | `cleanup-package-permissions` | 10 passed | 10 passed | | plugin-auth `grant-readers-by-name.golden`, `grant-readers-unnamed-grant` | 9 passed (at `e40fe491c1`) | 9 failed at `008ca32a75` ("Object 'sys_permission_set' not found"); 9 passed at `e40fe491c1` | | cli unit (`organization-ownership-inventory`, `platform-page-i18n-parity`) | 35 passed | 1 failed at `008ca32a75` (38 label maps under a floor of 39); 35 passed with the floor at 38 | | metadata-protocol (3 files) | 89 passed | 89 passed | | objectql (2 files) | 58 passed | 58 passed | | platform-objects (1 file) | 20 passed | 15 passed | | lint, rest, runtime, spec (1 file each) | 15, 11, 13, 11 passed | 15, 11, 13, 11 passed | | core `src/security` | 37 files, 867 passed | 37 files, 867 passed | | typecheck | 12 touched packages, 77 turbo tasks green (at `d426f922ed`); plugin-auth again at `e40fe491c1` | plugin-security, plugin-auth, dogfood: 67 tasks green; plugin-auth again at the final head | Every file changed after a full run was re-run on both trees at the final head. E6 ablation on the `main` + S8-T + S8 tree: with `main`'s (D's) `bootstrap-platform-admin.ts` put back (blob `edf2a164d9`, on disk by hash), the fresh-boot golden goes red, 2 of 2 (`adminPromoted: false`); restored (blob `56166db769`, `git diff HEAD` empty), green, 2 of 2. ## Dogfood PR and queue CI do not run the dogfood shards (#22998), so this is their first record before `main`'s full battery. The 24 files S8-T touches or that read the four objects, on the showcase boot: | tree | result | |:---|:---| | `main` + S8-T | 23 of 24 files at `9d55c0589a`, the red one `mail-message-pool-read-gate` (`main`'s own red since D; this branch carries the hunk #23159 landed on `main`); at `c0dc292ae8` that file and `wall-catalog-refusal` green, `activity-text-predicate` timed out once (5000 ms, contended box) and passed alone (22 of 22) at `d426f922ed` | | `main` + S8-T + S8 | 24 of 24 files, 280 tests passed | No dogfood file changed after `008ca32a75`. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives 89 commands for this diff at `9d9bdd96b2` (the same 89 as the dispatch's list at `0d43004100`). All 89 ran after the final commit and exit 0 (`check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET, exit 3, and exit 0 once every package was built); `--ran` with each exit code recorded: "89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN". `check:entry-nameability` keeps its existing note that `@objectstack/spec/api-assembled` and `/qa` have no callable export to measure; `packages/spec` is untouched. ## Size 93 files, +1,255 / −3,950 = 5,205 changed lines against the merge base `f931e765b7`, almost all deletion: over the 3,000-line line, so this lands by the maintainer's approval. `mail-message-pool-read-gate` leaves the diff at the next `main` merge (`main` carries the same blob). `skip-changeset`: only test files, a test-only testkit under `src/__tests__` (no entry imports it; `files` ships `dist` only) and the private dogfood package change. ## 维护者速读(草稿) **改了什么** C3 第 8 阶段的"测试半边"(S8-T),只改测试文件:93 个文件,+1,255 / −3,950 行,绝大部分是删除。四个已退役的安全目录行对象(`sys_position`、`sys_permission_set`、`sys_position_permission_set`、`sys_capability`)将在 S8 中取消注册;本 PR 先让测试不再依赖它们:删除只钉住"S8 要拆掉的行为"的测试,夹具改为走元数据门(`/api/v1/meta`)或读安全目录,少数断言改成无论对象是否注册都成立。本轮还把 D 落地后的 `main` 合进来,并修了 `main` 新带进来、依赖这四个对象的 5 个测试文件。 **为什么改** ADR-0131 D3 规定权限集、岗位、能力只有一个归宿:安全目录(注册表)。S8 取消注册这四个行对象;测试若不先迁走,S8 一落地就会大面积变红。测试半边单独先落,S8 本身的代码改动才能保持可审。 **风险与代价(含回滚)** - 只动测试,产品行为零变化;回滚即 revert 本 PR 的合并提交。 - S8 落地前的短窗口里,`main` 上少了几条钉"即将被拆掉的行为"的单元测试(如数据门写穿);走廊下数据门的创建拒绝(`walled-catalog-data-door.ts`)仍由原样保留的 `walled-catalog-data-door.test.ts` 和 `builtin-positions.boot` 钉着,S8 连同该门一起删除它们。S8 紧随其后落地。 - 超过 3000 行,按规则需要您的批准;其中约 76% 是删除。 - PR 与队列 CI 不跑 dogfood(#22998);两棵树都已在本地跑过,`main` 的完整测试组是它们在 CI 上的首次记录。 **席位意见** **你要做的** 批准本 PR(授权批准或亲自合并);之后席位接着落 S8。 --- _Generated by [Claude Code](https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 107853c commit 150da61

92 files changed

Lines changed: 1250 additions & 3948 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎packages/cli/src/utils/organization-ownership-inventory.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ describe('ADR-0131 D10 inventory — every censused object has one fate and a ci
8686

8787
it('the platform registry as it stands — a platform object without a row reds here, by name', () => {
8888
const registered = Object.values(PLATFORM_OBJECTS_BY_PACKAGE).flat();
89-
expect(registered.length).toBeGreaterThan(80);
89+
// (The four retired catalog row objects left the registry with C3 stage 8.)
90+
expect(registered.length).toBeGreaterThan(75);
9091
expect(unlisted(registered)).toEqual([]);
9192
});
9293

‎packages/cli/test/platform-page-i18n-parity.test.ts‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -788,9 +788,10 @@ describe('shipped platform record pages -- i18n ownership (#14817)', () => {
788788
// red here. What this refuses is the scan that finds NOTHING -- an empty
789789
// population would satisfy every `for` loop below and report success over
790790
// zero pages, which is the exact shape of the `0` that reads as "clean".
791-
expect(RECORD_PAGES.length).toBeGreaterThanOrEqual(3);
791+
// (`sys_position_detail` retired with the `sys_position` object, C3 stage 8.)
792+
expect(RECORD_PAGES.length).toBeGreaterThanOrEqual(2);
792793
expect(RECORD_PAGES.map((p) => p.name).sort()).toEqual(
793-
expect.arrayContaining(['sys_organization_detail', 'sys_position_detail', 'sys_user_detail']),
794+
expect.arrayContaining(['sys_organization_detail', 'sys_user_detail']),
794795
);
795796
expect(SHIPPED_LOCALES).toContain(EN);
796797
expect(SHIPPED_LOCALES.length).toBeGreaterThan(1);
@@ -838,9 +839,10 @@ describe('shipped platform record pages -- i18n ownership (#14817)', () => {
838839
const maps = RECORD_PAGES.flatMap((page) => inlineLocaleMaps(page, page.name));
839840

840841
// Same refusal as the population floor: zero maps means the walk broke, not
841-
// that the pages went monolingual. [#15204 B2] 44: the user page's two
842-
// grant lists dropped their `add` pickers, and with them two label maps.
843-
expect(maps.length).toBeGreaterThanOrEqual(44);
842+
// that the pages went monolingual. [#15204 B2] The user page's two grant
843+
// lists dropped their `add` pickers, and with them two label maps.
844+
// (`sys_position_detail` retired with the `sys_position` object, C3 stage 8.)
845+
expect(maps.length).toBeGreaterThanOrEqual(38);
844846

845847
const incomplete = maps
846848
.filter((m) => SHIPPED_LOCALES.some((locale) => !m.locales.includes(locale)))
@@ -1041,14 +1043,14 @@ describe('`pages.*` default-locale parity (#8764, moved off the nav roster by #1
10411043
// The anti-vacuity floor, stated before the verdict runs. A verdict over an
10421044
// empty or shrunken population is the ambiguous `0` this whole card is
10431045
// about: every loop below would be satisfied and report success.
1044-
expect(SOURCE_BY_NAME.size).toBeGreaterThanOrEqual(6);
1046+
// (`sys_position_detail` retired with the `sys_position` object, C3 stage 8.)
1047+
expect(SOURCE_BY_NAME.size).toBeGreaterThanOrEqual(5);
10451048
expect([...SOURCE_BY_NAME.keys()].sort()).toEqual(
10461049
expect.arrayContaining([
10471050
'cloud_connection_settings',
10481051
'connect_agent',
10491052
'marketplace_installed',
10501053
'sys_organization_detail',
1051-
'sys_position_detail',
10521054
'sys_user_detail',
10531055
]),
10541056
);
@@ -1063,7 +1065,7 @@ describe('`pages.*` default-locale parity (#8764, moved off the nav roster by #1
10631065
const enPages = pagesOf(EN);
10641066
// Absence is loud: an `en` section that lost its entries would make the
10651067
// verdict below compare nothing at all.
1066-
expect(Object.keys(enPages).length).toBeGreaterThanOrEqual(6);
1068+
expect(Object.keys(enPages).length).toBeGreaterThanOrEqual(5);
10671069

10681070
const findings = defaultLocalePageDrift(enPages, SOURCE_BY_NAME).map((f) => ({
10691071
...f,
@@ -1202,10 +1204,11 @@ describe('platform record page labels are translated in every shipped locale (#1
12021204
* wrong recorded source hash would serve the English source here and red this
12031205
* assertion rather than shipping a silent regression.
12041206
*/
1205-
const RECORD_PAGE_NAMES = ['sys_user_detail', 'sys_organization_detail', 'sys_position_detail'];
1207+
// (`sys_position_detail` retired with the `sys_position` object, C3 stage 8.)
1208+
const RECORD_PAGE_NAMES = ['sys_user_detail', 'sys_organization_detail'];
12061209
const TRANSLATED_LOCALES = SHIPPED_LOCALES.filter((locale) => locale !== EN);
12071210

1208-
it('has a real, non-English label for each of the three in each translated locale', () => {
1211+
it('has a real, non-English label for each record page in each translated locale', () => {
12091212
expect(TRANSLATED_LOCALES.length).toBeGreaterThanOrEqual(3);
12101213

12111214
const english = pagesOf(EN);

‎packages/core/src/security/row-active.test.ts‎

Lines changed: 0 additions & 48 deletions
This file was deleted.

‎packages/lint/src/validate-capability-references.test.ts‎

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,6 @@ describe('validateCapabilityReferences (ADR-0066 ⑨)', () => {
4444
expect(findings).toEqual([]);
4545
});
4646

47-
it('passes a reference to a capability shipped as a sys_capability seed row', () => {
48-
const findings = validateCapabilityReferences({
49-
data: [{ object: 'sys_capability', records: [{ name: 'approve_invoice' }] }],
50-
objects: [{ name: 'inv_invoice', requiredPermissions: ['approve_invoice'] }],
51-
});
52-
expect(findings).toEqual([]);
53-
});
54-
5547
it('warns on an object requiredPermissions typo (registered nowhere)', () => {
5648
const findings = validateCapabilityReferences({
5749
objects: [{ name: 'sys_license', requiredPermissions: ['mange_users'] }],
@@ -193,10 +185,11 @@ describe('[#22161] one-line verdicts — capability-reference-unknown', () => {
193185
const findings = validateCapabilityReferences({
194186
objects: [{ name: 'sys_license', requiredPermissions: ['mange_users'] }],
195187
});
196-
expect(findings.map((f) => f.message)).toEqual([
197-
'requiredPermissions names capability "mange_users", which no built-in, `defineCapability`, '
198-
+ '`systemPermissions` grant or `sys_capability` seed provides, so this gate fails closed',
199-
]);
188+
expect(findings).toHaveLength(1);
189+
const [message] = findings.map((f) => f.message);
190+
expect(message.startsWith('requiredPermissions names capability "mange_users", which no built-in, `defineCapability`')).toBe(true);
191+
expect(message.endsWith('so this gate fails closed')).toBe(true);
192+
expect(message.split('\n')).toHaveLength(1);
200193
});
201194

202195
it('`os explain` carries what the verdict no longer says', () => {
@@ -218,18 +211,17 @@ describe('[#22161] one-line verdicts — capability-reference-unknown', () => {
218211
});
219212

220213
it('each source of the known set the explanation names silences the rule', () => {
221-
// The explanation module imports nothing, so its four sources are held to
214+
// The explanation module imports nothing, so its sources are held to
222215
// the rule by running the rule: each one makes the same reference known.
223216
const text = explainRule(CAPABILITY_REFERENCE_UNKNOWN)!.paragraphs.join('\n');
224-
for (const named of ['built-in platform capability', '`defineCapability`', '`systemPermissions`', '`sys_capability` seed row']) {
217+
for (const named of ['built-in platform capability', '`defineCapability`', '`systemPermissions`']) {
225218
expect(text, named).toContain(named);
226219
}
227220
const gated = { objects: [{ name: 'inv_invoice', requiredPermissions: ['approve_invoice'] }] };
228221
expect(validateCapabilityReferencesUnrecorded(gated), 'lit control').toHaveLength(1);
229222
for (const source of [
230223
{ capabilities: [{ name: 'approve_invoice', label: 'Approve', scope: 'org' }] },
231224
{ permissions: [{ name: 'p', systemPermissions: ['approve_invoice'] }] },
232-
{ data: [{ object: 'sys_capability', records: [{ name: 'approve_invoice' }] }] },
233225
]) {
234226
expect(validateCapabilityReferencesUnrecorded({ ...gated, ...source }), JSON.stringify(source)).toEqual([]);
235227
}

‎packages/metadata-protocol/src/protocol.packaged-base-refusal.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -369,10 +369,10 @@ describe('packagedBaseRefusal — each Regime C type names its OWN sanctioned pa
369369

370370
it(`\`permission\` — save names its clone and no switch (${environmentId ? 'environment' : 'host-config'} kernel)`, () => {
371371
const message = expectRegimeC(refusalOn(environmentId, 'permission', 'pkg_perm', 'save'), 'permission', 'pkg_perm', 'save');
372-
expect(message).toContain(
373-
'Clone it under a new name to customize it (the "Clone" action on the permission set, '
374-
+ 'or POST /api/v1/data/sys_permission_set with a new name).',
375-
);
372+
// The clone route's spelling moved with C3 stage 8 (the retired
373+
// object's data route → the catalog's clone door); the sentence did not.
374+
expect(message).toContain('Clone it under a new name to customize it (the "Clone" action on the permission set, or POST /api/v1/');
375+
expect(message).toContain(' with a new name).');
376376
expect(message.toLowerCase()).not.toContain('switch it off');
377377
expect(message).not.toContain('_activation');
378378
expect(message).not.toContain('/automation/');
@@ -411,7 +411,7 @@ describe('packagedBaseRefusal — each Regime C type names its OWN sanctioned pa
411411
for (const [type, name, path] of [
412412
['flow', 'pkg_flow', 'POST /api/v1/automation/:name/clone'],
413413
['action', 'pkg_action', 'POST /api/v1/actions/_activation/:object/:action'],
414-
['permission', 'pkg_perm', 'POST /api/v1/data/sys_permission_set'],
414+
['permission', 'pkg_perm', 'the "Clone" action on the permission set, or POST /api/v1/'],
415415
] as const) {
416416
const r: any = p.packagedBaseRefusal({ type, name, operation: 'save', packageId: PACKAGE_ID });
417417
expect({ code: r?.code, status: r?.status, lockSource: r?.lockSource, packageId: r?.packageId }, type)

‎packages/metadata-protocol/src/protocol.uninstall-cleanups-runner.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ describe('#21490: runUninstallCleanups — the uninstall-cleanup registry\'s one
9999

100100
const res = await protocol.deletePackage(request);
101101

102-
expect(runner.mock.calls).toEqual([[request]]);
102+
// Its own request — plus, when the package declared permission sets, their
103+
// names (`declared`, ADR-0131 D4), read before its withdrawal.
104+
expect(runner.mock.calls).toEqual([[expect.objectContaining(request)]]);
103105
expect(res.cleanups).toEqual(await runner.mock.results[0]!.value);
104106
expect(res.cleanups).toEqual([{ name: 'security.package-permissions', success: true, removed: 4 }]);
105107
});

‎packages/metadata-protocol/src/sys-metadata-repository.package-writability.test.ts‎

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -574,10 +574,11 @@ describe('#7682 — the refusal discriminates on package writability', () => {
574574
`Cannot overlay 'permission' in package '${READ_ONLY_PKG}': that package is read-only, and its packaged base `
575575
+ 'is locked against in-place edits. ',
576576
)).toBe(true);
577-
expect(message).toContain(
578-
'Clone it under a new name to customize it (the "Clone" action on the permission set, '
579-
+ 'or POST /api/v1/data/sys_permission_set with a new name).',
580-
);
577+
// The clone route's spelling moved with C3 stage 8 (the data route of the
578+
// retired `sys_permission_set` object → the catalog's clone door); the
579+
// sentence around it did not.
580+
expect(message).toContain('Clone it under a new name to customize it (the "Clone" action on the permission set, or POST /api/v1/');
581+
expect(message).toContain(' with a new name).');
581582
expect(message).not.toContain('OS_METADATA_WRITABLE');
582583
expect(message).not.toContain('redeploy');
583584
expect(message.endsWith('See docs/adr/0126-packaged-metadata-customization-model.md.')).toBe(true);
@@ -1253,8 +1254,17 @@ describe('[#20910] the repository doors name a Regime C type\'s sanctioned path,
12531254
const ADR_0126 = 'See docs/adr/0126-packaged-metadata-customization-model.md.';
12541255
const ACTION_SWITCH = 'Switch it off (POST /api/v1/actions/_activation/:object/:action, body {enabled: false}, '
12551256
+ ':object = global for an object-less action; operator-only where one install serves several organizations).';
1257+
// `{ROUTE}`: the clone route, whose spelling moved with C3 stage 8 (the
1258+
// retired object's data route → the catalog's clone door); see `sentenceMatches`.
12561259
const PERMISSION_CLONE = 'Clone it under a new name to customize it (the "Clone" action on the permission set, '
1257-
+ 'or POST /api/v1/data/sys_permission_set with a new name).';
1260+
+ 'or POST {ROUTE} with a new name).';
1261+
/** Exact equality, with a `{ROUTE}` placeholder standing for one `/api/v1/…` path. */
1262+
const sentenceMatches = (actual: string, expected: string): boolean => {
1263+
const [head, tail] = expected.split('{ROUTE}');
1264+
if (tail === undefined) return actual === expected;
1265+
return actual.startsWith(head) && actual.endsWith(tail)
1266+
&& /^\/api\/v1\/\S+$/.test(actual.slice(head.length, actual.length - tail.length));
1267+
};
12581268
const FLOW_PATHS = 'Clone it under a new name to customize it (POST /api/v1/automation/:name/clone, body {name, label}), '
12591269
+ 'or switch it off (POST /api/v1/automation/:name/toggle, body {enabled: false}; '
12601270
+ 'operator-only where one install serves several organizations).';
@@ -1286,10 +1296,9 @@ describe('[#20910] the repository doors name a Regime C type\'s sanctioned path,
12861296
it(`type door, save: a packaged ${type} is refused NOT_OVERRIDABLE with its row's path, no hatch`, async () => {
12871297
const err = await putWith(repo, { type, name: `pkg_${type}`, intent: 'override-artifact' }) as any;
12881298
expect(err).toMatchObject({ code: 'NOT_OVERRIDABLE', status: 403 });
1289-
expect(String(err.message)).toBe(
1290-
`Metadata item '${type}/pkg_${type}' is provided by a managed package and is sealed `
1291-
+ `against in-place edits. ${paths} ${ADR_0126}`,
1292-
);
1299+
const expected = `Metadata item '${type}/pkg_${type}' is provided by a managed package and is sealed `
1300+
+ `against in-place edits. ${paths} ${ADR_0126}`;
1301+
expect(sentenceMatches(String(err.message), expected), `${String(err.message)}\n≠ ${expected}`).toBe(true);
12931302
expect(Array.from(engine.rows.values())).toEqual([]);
12941303
});
12951304
}
@@ -1359,10 +1368,9 @@ describe('[#20910] the repository doors name a Regime C type\'s sanctioned path,
13591368
code: 'ITEM_LOCKED', status: 403, lockSource: 'package', packageId: READ_ONLY_PKG,
13601369
docs: 'docs/adr/0010-metadata-protection-model.md',
13611370
});
1362-
expect(String(err.message)).toBe(
1363-
`Cannot overlay '${type}' in package '${READ_ONLY_PKG}': that package is read-only, and its packaged base `
1364-
+ `is locked against in-place edits. ${paths} ${ADR_0126}`,
1365-
);
1371+
const expected = `Cannot overlay '${type}' in package '${READ_ONLY_PKG}': that package is read-only, and its packaged base `
1372+
+ `is locked against in-place edits. ${paths} ${ADR_0126}`;
1373+
expect(sentenceMatches(String(err.message), expected), `${String(err.message)}\n≠ ${expected}`).toBe(true);
13661374
});
13671375
}
13681376

0 commit comments

Comments
 (0)