@@ -8,6 +8,7 @@ import { dirname, join, resolve } from 'node:path';
88// Loaded at module top, so the first transform is paid during collection
99// rather than inside a clocked test body (`check:test-source-alias`).
1010import { AppPlugin } from '@objectstack/runtime' ;
11+ import { buildIndexName } from '@objectstack/driver-sql' ;
1112import { loadConfig } from './config.js' ;
1213import { bootSchemaStack } from './schema-migrate.js' ;
1314
@@ -24,17 +25,22 @@ import { bootSchemaStack } from './schema-migrate.js';
2425 *
2526 * The fixture is cloud#1695's shape, built by the platform rather than by hand:
2627 * a host `objectstack.config.ts` composing `SecurityPlugin`, the tables created
27- * through the migrate path itself, and then `sys_position`'s per-organization
28- * composite unique swapped back to the pre-#8323 global spelling
29- * (`uniq_sys_position_name`) — the exact index a deployed control DB was still
30- * carrying.
28+ * through the migrate path itself, and then a per-organization composite unique
29+ * swapped back to its pre-#8323 global spelling — the drift a deployed control
30+ * DB was carrying. The measured index was `sys_position`'s
31+ * (`uniq_sys_position_name`); C3 stage 8 retired that object (ADR-0131 D3), so
32+ * the swap is now made on the one organization-respelled unique the plugin
33+ * still declares: `sys_audience_binding_suggestion`'s
34+ * `(package_id, permission_set_name, anchor)`, the #8323 declared-index arm of
35+ * the same `replace_unique_index` plan.
3136 *
3237 * Four things are pinned, and the fourth is an ABSENCE. Composing a host
33- * config means composing arbitrary code, and `SecurityPlugin` seeds its
34- * built-in permission sets from `start()`: measured, that produced 14 insert
35- * attempts against `sys_permission_set` during a run documented as writing
36- * nothing. `sys_permission_set` staying EMPTY across a plan is what proves the
37- * declaration-phase composition actually holds.
38+ * config means composing arbitrary code. Measured before the catalog seeders
39+ * retired, `SecurityPlugin.start()` made 14 insert attempts against
40+ * `sys_permission_set` during a run documented as writing nothing. That table
41+ * is retired with its object; the plugin's tables holding exactly the
42+ * fixture's rows across a plan is what still shows the declaration-phase
43+ * composition holds.
3844 */
3945
4046const require_ = createRequire ( import . meta. url ) ;
@@ -63,6 +69,19 @@ const ARTIFACTLESS_BASELINE_TABLES = [
6369 'sys_metadata_history' ,
6470] ;
6571
72+ /**
73+ * The organization-respelled declared unique the drift cases below put back to
74+ * its global spelling: `sys_audience_binding_suggestion`'s
75+ * `(package_id, permission_set_name, anchor)`, declared `unique: 'organization'`.
76+ * Both names are the driver's own (`buildIndexName`), never spelled by hand.
77+ */
78+ const RESPELLED = {
79+ table : 'sys_audience_binding_suggestion' ,
80+ listed : [ 'package_id' , 'permission_set_name' , 'anchor' ] ,
81+ } as const ;
82+ const LEGACY_UNIQUE = buildIndexName ( RESPELLED . table , [ ...RESPELLED . listed ] , true ) ;
83+ const ORG_UNIQUE = buildIndexName ( RESPELLED . table , [ 'organization_id' , ...RESPELLED . listed ] , true ) ;
84+
6685describe ( 'os migrate plan/apply compose the deployment\'s own object set (#12938)' , ( ) => {
6786 let dir : string ;
6887 let dbFile : string ;
@@ -147,9 +166,9 @@ describe('os migrate plan/apply compose the deployment\'s own object set (#12938
147166 } ;
148167
149168 /**
150- * Put `sys_position` back into the pre-respelling physical shape — the
151- * per-organization composite retired, the global single-column unique
152- * present. This is cloud#1695's index, verbatim .
169+ * Put { @link RESPELLED} back into the pre-respelling physical shape — the
170+ * per-organization composite retired, the global unique on the listed
171+ * columns present. cloud#1695's shape, on a table the plugin still declares .
153172 *
154173 * Idempotent, and called by every test that needs it, so the cases below do
155174 * not depend on each other's order. An integration file whose third test only
@@ -160,18 +179,20 @@ describe('os migrate plan/apply compose the deployment\'s own object set (#12938
160179 const stack = await bootLikeMigrate ( ) ;
161180 try {
162181 const k = ( stack . driver as any ) . knex ;
163- const present = await indexNames ( stack . driver , 'sys_position' ) ;
164- if ( present . includes ( 'uniq_sys_position_organization_id_name' ) ) {
165- await k . raw ( ' DROP INDEX uniq_sys_position_organization_id_name' ) ;
182+ const present = await indexNames ( stack . driver , RESPELLED . table ) ;
183+ if ( present . includes ( ORG_UNIQUE ) ) {
184+ await k . raw ( ` DROP INDEX ${ ORG_UNIQUE } ` ) ;
166185 }
167- if ( ! present . includes ( 'uniq_sys_position_name' ) ) {
168- await k . raw ( ' CREATE UNIQUE INDEX uniq_sys_position_name ON sys_position (name)' ) ;
186+ if ( ! present . includes ( LEGACY_UNIQUE ) ) {
187+ await k . raw ( ` CREATE UNIQUE INDEX ${ LEGACY_UNIQUE } ON ${ RESPELLED . table } ( ${ RESPELLED . listed . join ( ', ' ) } )` ) ;
169188 }
170- if ( await countRows ( stack . driver , 'sys_position' ) === 0 ) {
171- await k ( 'sys_position' ) . insert ( {
172- id : 'pos_1' ,
173- name : 'org_admin' ,
174- label : 'Org Admin' ,
189+ if ( await countRows ( stack . driver , RESPELLED . table ) === 0 ) {
190+ await k ( RESPELLED . table ) . insert ( {
191+ id : 'abs_1' ,
192+ package_id : 'com.acme.crm' ,
193+ permission_set_name : 'sales_readonly' ,
194+ anchor : 'everyone' ,
195+ status : 'pending' ,
175196 organization_id : 'org_jia' ,
176197 } ) ;
177198 }
@@ -196,15 +217,18 @@ describe('os migrate plan/apply compose the deployment\'s own object set (#12938
196217 expect ( stack . composition . hostConfigPath ) . toBe ( join ( dir , 'objectstack.config.ts' ) ) ;
197218
198219 // The number the consumer-side coverage gate reads. Strictly greater than
199- // the baseline, and by more than one: the security plugin alone brings six
200- // RBAC tables and the platform floor another four.
220+ // the baseline, and by more than one: the security plugin alone brings
221+ // three tables (its assignment objects and the audience-binding
222+ // suggestions; the catalog row objects retired in C3 stage 8) and the
223+ // platform floor another four.
201224 expect ( stack . managedTableCount ) . toBeGreaterThan ( ARTIFACTLESS_BASELINE_TABLES . length ) ;
202225
203226 const pending = stack . pendingSchemaWork . map ( ( p ) => p . table ) ;
204- // The two tables the issue names by hand, neither of which was reachable
205- // from a migrate boot at all.
206- expect ( pending ) . toContain ( 'sys_position' ) ;
207- expect ( pending ) . toContain ( 'sys_permission_set' ) ;
227+ // The issue named `sys_position` and `sys_permission_set` by hand, neither
228+ // reachable from a migrate boot at all; both objects are retired now
229+ // (ADR-0131 D3), and the security plugin's tables that remain stand in.
230+ expect ( pending ) . toContain ( 'sys_user_position' ) ;
231+ expect ( pending ) . toContain ( 'sys_user_permission_set' ) ;
208232 // …and the platform floor `serve` composes unconditionally.
209233 expect ( pending ) . toContain ( 'sys_migration' ) ;
210234 for ( const table of ARTIFACTLESS_BASELINE_TABLES ) expect ( pending ) . toContain ( table ) ;
@@ -225,15 +249,15 @@ describe('os migrate plan/apply compose the deployment\'s own object set (#12938
225249 try {
226250 const drift = await stack . driver ! . detectManagedDrift ( ) ;
227251 const entry = drift . find (
228- ( d ) => d . table === 'sys_position' && d . op . type === 'replace_unique_index' ,
252+ ( d ) => d . table === RESPELLED . table && d . op . type === 'replace_unique_index' ,
229253 ) ;
230254 expect ( entry , 'the legacy platform-wide unique must be planned, not invisible' ) . toBeDefined ( ) ;
231255 expect ( entry ! . category ) . toBe ( 'safe' ) ;
232256 expect ( entry ! . op ) . toMatchObject ( {
233- table : 'sys_position' ,
234- dropIndexNames : [ 'uniq_sys_position_name' ] ,
235- createIndexName : 'uniq_sys_position_organization_id_name' ,
236- createColumns : [ 'organization_id' , 'name' ] ,
257+ table : RESPELLED . table ,
258+ dropIndexNames : [ LEGACY_UNIQUE ] ,
259+ createIndexName : ORG_UNIQUE ,
260+ createColumns : [ 'organization_id' , ... RESPELLED . listed ] ,
237261 } ) ;
238262 // The prescription that made this a contract rather than an incomplete report.
239263 expect ( entry ! . message ) . toContain ( 'os migrate apply' ) ;
@@ -249,13 +273,15 @@ describe('os migrate plan/apply compose the deployment\'s own object set (#12938
249273 try {
250274 await stack . driver ! . detectManagedDrift ( ) ;
251275
252- // The absence that matters. `SecurityPlugin.start()` bootstraps
253- // `admin_full_access`, `organization_admin`, `member_default` and the rest;
254- // fully started, it attempted 14 inserts into this table during a plan.
255- expect ( await countRows ( stack . driver , 'sys_permission_set' ) ) . toBe ( 0 ) ;
256- expect ( await countRows ( stack . driver , 'sys_position' ) ) . toBe ( 1 ) ;
276+ // The absence that matters: the security plugin's tables hold exactly
277+ // the fixture's rows. (Fully started, `SecurityPlugin.start()` once made
278+ // 14 insert attempts into `sys_permission_set` during a plan; that seeder
279+ // and that object are retired, ADR-0131 D3.)
280+ expect ( await countRows ( stack . driver , 'sys_user_permission_set' ) ) . toBe ( 0 ) ;
281+ expect ( await countRows ( stack . driver , 'sys_user_position' ) ) . toBe ( 0 ) ;
282+ expect ( await countRows ( stack . driver , RESPELLED . table ) ) . toBe ( 1 ) ;
257283 // The legacy index is still there — a plan proposes, it does not apply.
258- expect ( await indexNames ( stack . driver , 'sys_position' ) ) . toContain ( 'uniq_sys_position_name' ) ;
284+ expect ( await indexNames ( stack . driver , RESPELLED . table ) ) . toContain ( LEGACY_UNIQUE ) ;
259285 } finally {
260286 await stack . shutdown ( ) ;
261287 }
@@ -274,10 +300,10 @@ describe('os migrate plan/apply compose the deployment\'s own object set (#12938
274300 expect ( applied . some ( ( d ) => d . op . type === 'replace_unique_index' ) ) . toBe ( true ) ;
275301 expect ( skipped ) . toHaveLength ( 0 ) ;
276302
277- const after = await indexNames ( stack . driver , 'sys_position' ) ;
278- expect ( after ) . toContain ( 'uniq_sys_position_organization_id_name' ) ;
279- expect ( after ) . not . toContain ( 'uniq_sys_position_name' ) ;
280- expect ( await countRows ( stack . driver , 'sys_position' ) ) . toBe ( 1 ) ;
303+ const after = await indexNames ( stack . driver , RESPELLED . table ) ;
304+ expect ( after ) . toContain ( ORG_UNIQUE ) ;
305+ expect ( after ) . not . toContain ( LEGACY_UNIQUE ) ;
306+ expect ( await countRows ( stack . driver , RESPELLED . table ) ) . toBe ( 1 ) ;
281307 } finally {
282308 await stack . shutdown ( ) ;
283309 }
@@ -464,11 +490,13 @@ describe('a host that brings its OWN ObjectQL engine (#13028 — cloud\'s measur
464490 // being a second, differently-derived number.
465491 expect ( stack . managedTableCount ) . toBe ( coverage ! . examinedObjects ) ;
466492
467- // The two tables #12938 named by hand as the proof the five-table set was
468- // wrong. Pre-#13028 this shape reached NEITHER.
493+ // #12938 named `sys_position` and `sys_permission_set` by hand as the proof
494+ // the five-table set was wrong; pre-#13028 this shape reached neither. Both
495+ // objects are retired now (ADR-0131 D3), so the security plugin's tables
496+ // that remain carry the same proof.
469497 const pending = stack . pendingSchemaWork . map ( ( p ) => p . table ) ;
470- expect ( pending ) . toContain ( 'sys_position ' ) ;
471- expect ( pending ) . toContain ( 'sys_permission_set ' ) ;
498+ expect ( pending ) . toContain ( 'sys_user_position ' ) ;
499+ expect ( pending ) . toContain ( 'sys_user_permission_set ' ) ;
472500
473501 // Full coverage means SILENCE about coverage: the honesty note exists to
474502 // mark a shortfall, and one that printed anyway would train readers to
@@ -493,7 +521,7 @@ describe('a host that brings its OWN ObjectQL engine (#13028 — cloud\'s measur
493521 // assignment on the driver. If it had reached `initObjects` instead, the
494522 // table would exist here.
495523 const k = ( stack . driver as any ) . knex ;
496- const exists = await k . schema . hasTable ( 'sys_permission_set ' ) ;
524+ const exists = await k . schema . hasTable ( 'sys_user_permission_set ' ) ;
497525 expect ( exists , 'a plan must not create a table on its way to a coverage number' ) . toBe ( false ) ;
498526 } finally {
499527 await stack . shutdown ( ) ;
0 commit comments