Skip to content

Commit 07bc34a

Browse files
test(dogfood): fixtures read permission sets from the security catalog (#15204 stage 6b-2, T2) (#22898)
Part of #15204 Clause-②: no **T2 of stage 6b-2**: tests only. Fixtures that looked up a permission set's seeded `sys_permission_set` row now read the set from the security catalog (ADR-0131 D3, the catalog is its one home). Fixtures that model an **existing** database write the legacy row themselves. It is split off the 6b-2 code PR (#22900) so that PR falls under 3,000 changed lines. **Landing, as sequenced by the seat:** T1, T2 and the 6b-2 code PR (#22900) stay drafts until #22900's own gates clear (the #22865 pickers and objectui PR-4), then land back to back, **T1 → T2 → #22900**. That way the publish materializer and the seeders are never live on `main` with their pins gone. 6b-2a (#22899) is independent and goes to the maintainer separately. This branch is green on `main` as it is today, while the seeder still runs. No production file changes. ## What changes - New `packages/qa/dogfood/test/catalog-permission-set.ts`: - `catalogPermissionSet(ql, name)` returns the set the catalog serves (via `securityCatalogReaderOf`; it throws if no reader is bound, so a missing catalog cannot pass as an absent set); - `ensureLegacyPermissionSetRow(ql, name, { managedBy })` writes the organization-less legacy row with its full body from the catalog definition, for the suites whose data-door cases need a row. - About 30 dogfood suites move from `findOne('sys_permission_set')` preconditions to `catalogPermissionSet`. - `showcase-permission-zoo`, `two-doors-permission`, `showcase-permission-projection`, `permission-set-write-through-package-binding` and `permission-set-lock-row-provenance` write the legacy row where their cases edit one. `two-doors-permission` 块1 (a publish materializes a row) goes; the materializer is deleted in 6b-2. - `armed.dogfood.test.ts`: the seeded controls use signed-up members, not `usr_system`. - `permission-set-clone-boot-unowned-warning.dogfood.test.ts` is deleted (it pins the seeder's boot warning). - plugin-security: new `src/__tests__/legacy-catalog-rows.testkit.ts` (`seedLegacyPlatformBucket`). Eight suites that model an existing database call it before their bootstrap. - **plugin-auth (added after the 6b-2 wider-suites run, item (d))**: the two grant-reader suites, `grant-readers-by-name.golden.test.ts` and `grant-readers-unnamed-grant.test.ts`, read the platform-bucket rows the bootstrap seeded: `organization_admin` to clone per organization, and `viewer_readonly` to point a legacy id at. Each now calls `seedLegacyPlatformBucket` before the bootstrap, through a plugin-auth copy of the testkit at `src/__tests__/legacy-catalog-rows.testkit.ts`. The copy has the same shape (organization-less, idempotent); a test-only helper is not exported across the package boundary. Without it, #22900's tree turns 3 of their cases red. ## Size 58 files against `main`, +379 / −831 (1,210 changed lines). The −831 includes the `bootstrap-platform-admin` suites this branch retires, with their ledger baseline entries. ## Verification On this branch, with the seeder still running: - full build; dogfood typecheck green; - the 8 plugin-security fixture suites: 8 files, 72 passed; - the 40 migrated dogfood suites: 40 files, 447 passed, 1 skipped; - the three double-contract gates, `check:changeset-no-major` and `check:nul-bytes` green. The same fixtures also pass with the seeder deleted: they are inside the 6b-2 branch's full dogfood run (248 files green). After merging `main` (`74c4b3435e`): - full dogfood: 254 files, 2,071 passed, 9 skipped; - plugin-security: 196 files, 4,042 passed; - `dispatch-gates --ran`: every derived family run, exit 0 (67 of 67 at `a38b54f6f`). The plugin-auth item: both suites, 9 passed, on this branch alone and again with #22900's tree on top; plugin-auth typecheck green in both. Test-only: `skip-changeset`. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_013XKtmoR5QCUSTMjiQ3os49 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 965a1bf commit 07bc34a

58 files changed

Lines changed: 379 additions & 831 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.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Test scaffolding: the organization-less `sys_permission_set` rows an EARLIER
5+
* release's platform-admin bootstrap seeded for the platform's default sets
6+
* (the "platform bucket").
7+
*
8+
* [C3 stage 6b-2] No boot writes these rows any more (ADR-0131 D3: the sets are
9+
* served from the security catalog, and a grant names its set). A suite that
10+
* models an EXISTING database — a grant written before the name column, whose
11+
* `permission_set_id` points at one of these rows; an organization's copy cloned
12+
* from the bucket — writes them itself, here, in the shape the retired seeder
13+
* wrote: `managed_by: 'platform'`, active, the set's facets as JSON columns.
14+
* Idempotent: a name that already has an organization-less row is left alone.
15+
*
16+
* The plugin-auth copy of plugin-security's testkit of the same name, in the
17+
* same shape: a test-only helper is not exported across the package boundary.
18+
*/
19+
20+
import type { PermissionSet } from '@objectstack/spec/security';
21+
22+
const SYS = { isSystem: true } as const;
23+
24+
/** The legacy row id for a default set — stable, so a fixture can point a grant at it. */
25+
export function legacyBucketRowId(name: string): string {
26+
return `ps_bucket_${name}`;
27+
}
28+
29+
/** Write the legacy platform-bucket row for each set; returns `name → id`. */
30+
export async function seedLegacyPlatformBucket(
31+
engine: { find: (...args: any[]) => Promise<any>; insert: (...args: any[]) => Promise<any> },
32+
sets: readonly PermissionSet[],
33+
): Promise<Record<string, string>> {
34+
const ids: Record<string, string> = {};
35+
for (const ps of sets) {
36+
if (!ps.name) continue;
37+
const existing = ((await engine.find('sys_permission_set', { where: { name: ps.name }, context: SYS })) ?? []) as any[];
38+
const own = existing.find((r) => !r.organization_id);
39+
if (own?.id) {
40+
ids[ps.name] = String(own.id);
41+
continue;
42+
}
43+
const id = legacyBucketRowId(ps.name);
44+
await engine.insert('sys_permission_set', {
45+
id,
46+
name: ps.name,
47+
label: ps.label ?? ps.name,
48+
description: (ps as any).description ?? null,
49+
object_permissions: JSON.stringify(ps.objects ?? {}),
50+
field_permissions: JSON.stringify(ps.fields ?? {}),
51+
system_permissions: JSON.stringify(ps.systemPermissions ?? []),
52+
row_level_security: JSON.stringify(ps.rowLevelSecurity ?? []),
53+
tab_permissions: JSON.stringify(ps.tabPermissions ?? {}),
54+
admin_scope: (ps as any).adminScope ? JSON.stringify((ps as any).adminScope) : null,
55+
active: true,
56+
managed_by: 'platform',
57+
}, { context: SYS });
58+
ids[ps.name] = id;
59+
}
60+
return ids;
61+
}

‎packages/plugins/plugin-auth/src/grant-readers-by-name.golden.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { SysMetadataActivation } from '@objectstack/platform-objects/system';
4040

4141
import { registerLastAdminGuard, type LastAdminGuardEngine } from './last-admin-guard.js';
4242
import { ensureDefaultOrganization } from './ensure-default-organization.js';
43+
import { seedLegacyPlatformBucket } from './__tests__/legacy-catalog-rows.testkit.js';
4344

4445
const SYS = { isSystem: true } as const;
4546
const POSTURE_ENV = 'OS_TENANCY_POSTURE';
@@ -153,6 +154,8 @@ async function standingByReader(posture: Posture): Promise<Record<string, unknow
153154
await user('usr_member', 'member@gra.example', '2025-03-01T00:00:00.000Z');
154155
await user('usr_agent', 'agent@gra.example', '2025-04-01T00:00:00.000Z');
155156

157+
// An existing database: the platform-bucket rows an earlier release seeded.
158+
await seedLegacyPlatformBucket(engine, bootstrapSets);
156159
const boot = await bootstrapPlatformAdmin(engine, bootstrapSets);
157160
expect(boot.adminPromoted).toBe(posture === 'single');
158161
if (walled) {

‎packages/plugins/plugin-auth/src/grant-readers-unnamed-grant.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { SysUser, SysAccount, SysMember, SysOrganization } from '@objectstack/pl
3636

3737
import { registerLastAdminGuard, type LastAdminGuardEngine } from './last-admin-guard.js';
3838
import { ensureDefaultOrganization } from './ensure-default-organization.js';
39+
import { seedLegacyPlatformBucket } from './__tests__/legacy-catalog-rows.testkit.js';
3940

4041
const SYS = { context: { isSystem: true } } as any;
4142
const GUARD_PACKAGE = 'test.grant-readers-unnamed';
@@ -122,6 +123,8 @@ async function soleGrantAnchoredAdmin(): Promise<Rig> {
122123
'sys_account', { id: `acc_${id}`, user_id: id, account_id: `${id}@un.example`, provider_id: 'credential' }, SYS,
123124
);
124125
}
126+
// An existing database: the platform-bucket rows an earlier release seeded.
127+
await seedLegacyPlatformBucket(engine, (manifest.permissions ?? []) as any[]);
125128
const boot = await bootstrapPlatformAdmin(engine, (manifest.permissions ?? []) as any[]);
126129
expect(boot).toMatchObject({ adminPromoted: true });
127130
return { engine };
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Test scaffolding: the organization-less `sys_permission_set` rows an EARLIER
5+
* release's platform-admin bootstrap seeded for the platform's default sets
6+
* (the "platform bucket").
7+
*
8+
* [C3 stage 6b-2] No boot writes these rows any more (ADR-0131 D3: the sets are
9+
* served from the security catalog, and a grant names its set). A suite that
10+
* models an EXISTING database — a grant written before the name column, whose
11+
* `permission_set_id` points at one of these rows; an organization's copy cloned
12+
* from the bucket — writes them itself, here, in the shape the retired seeder
13+
* wrote: `managed_by: 'platform'`, active, the set's facets as JSON columns.
14+
* Idempotent: a name that already has an organization-less row is left alone.
15+
*/
16+
17+
import type { PermissionSet } from '@objectstack/spec/security';
18+
19+
const SYS = { isSystem: true } as const;
20+
21+
/** The legacy row id for a default set — stable, so a fixture can point a grant at it. */
22+
export function legacyBucketRowId(name: string): string {
23+
return `ps_bucket_${name}`;
24+
}
25+
26+
/** Write the legacy platform-bucket row for each set; returns `name → id`. */
27+
export async function seedLegacyPlatformBucket(
28+
engine: { find: (...args: any[]) => Promise<any>; insert: (...args: any[]) => Promise<any> },
29+
sets: readonly PermissionSet[],
30+
): Promise<Record<string, string>> {
31+
const ids: Record<string, string> = {};
32+
for (const ps of sets) {
33+
if (!ps.name) continue;
34+
const existing = ((await engine.find('sys_permission_set', { where: { name: ps.name }, context: SYS })) ?? []) as any[];
35+
const own = existing.find((r) => !r.organization_id);
36+
if (own?.id) {
37+
ids[ps.name] = String(own.id);
38+
continue;
39+
}
40+
const id = legacyBucketRowId(ps.name);
41+
await engine.insert('sys_permission_set', {
42+
id,
43+
name: ps.name,
44+
label: ps.label ?? ps.name,
45+
description: (ps as any).description ?? null,
46+
object_permissions: JSON.stringify(ps.objects ?? {}),
47+
field_permissions: JSON.stringify(ps.fields ?? {}),
48+
system_permissions: JSON.stringify(ps.systemPermissions ?? []),
49+
row_level_security: JSON.stringify(ps.rowLevelSecurity ?? []),
50+
tab_permissions: JSON.stringify(ps.tabPermissions ?? {}),
51+
admin_scope: (ps as any).adminScope ? JSON.stringify((ps as any).adminScope) : null,
52+
active: true,
53+
managed_by: 'platform',
54+
}, { context: SYS });
55+
ids[ps.name] = id;
56+
}
57+
return ids;
58+
}

‎packages/plugins/plugin-security/src/bootstrap-platform-admin-authenticable-target.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { ObjectQL } from '@objectstack/objectql';
5050
import { SqlDriver } from '@objectstack/driver-sql';
5151
import { SysUser, SysAccount } from '@objectstack/platform-objects/identity';
5252
import { bootstrapPlatformAdmin, shouldReplayBootstrapFor } from './bootstrap-platform-admin.js';
53+
import { seedLegacyPlatformBucket } from './__tests__/legacy-catalog-rows.testkit.js';
5354
import { SysPermissionSet } from './objects/sys-permission-set.object.js';
5455
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
5556
import { defaultPermissionSets } from './objects/default-permission-sets.js';
@@ -319,6 +320,8 @@ describe('#14348 — the promotion target is the oldest human that can AUTHENTIC
319320
// written by hand — which is exactly the row the old selector would have
320321
// picked.
321322
const { logger: seedLogger } = collectingLogger();
323+
// An existing database: the platform-bucket rows an earlier release seeded.
324+
await seedLegacyPlatformBucket(engine as any, defaultPermissionSets);
322325
const seedPass = await bootstrapPlatformAdmin(engine as any, defaultPermissionSets, {
323326
logger: seedLogger,
324327
});

‎packages/plugins/plugin-security/src/bootstrap-platform-admin-promotion-selection.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import {
7070
PLATFORM_ADMIN_CANDIDATE_PAGE_SIZE,
7171
PLATFORM_ADMIN_CANDIDATE_SCAN_CEILING,
7272
} from './bootstrap-platform-admin.js';
73+
import { seedLegacyPlatformBucket } from './__tests__/legacy-catalog-rows.testkit.js';
7374
import { SysPermissionSet } from './objects/sys-permission-set.object.js';
7475
import { SysUserPermissionSet } from './objects/sys-user-permission-set.object.js';
7576
import { defaultPermissionSets } from './objects/default-permission-sets.js';
@@ -744,6 +745,8 @@ describe('#16682 — the promotion target is chosen, not sampled', () => {
744745
// this change's to make. A declared owner must not move an existing grant.
745746
const engine = await boot();
746747
await seedUser(engine as any, 'usr_person0', 'person0@demo.example', '2025-01-01T00:00:00.000Z', false);
748+
// An existing database: the platform-bucket rows an earlier release seeded.
749+
await seedLegacyPlatformBucket(engine as any, defaultPermissionSets);
747750
const first = await bootstrapPlatformAdmin(engine as any, defaultPermissionSets, {});
748751
expect(first.reason).toBe('no_authenticable_user');
749752
const sets = await findRows(engine, 'sys_permission_set', { name: 'admin_full_access' });

0 commit comments

Comments
 (0)