@@ -63,17 +63,27 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
6363 * ONLY read refusals whose log frames may be withheld here.
6464 *
6565 * `beforeEach` boots a kernel with no datasource and attaches sqlite late, then
66- * syncs exactly four objects. Every approval decision below therefore probes
67- * the six authz tables `resolveUserAuthzGrants` reads plus
68- * `sys_approval_delegation`, which ` ApprovalService.lookupActiveDelegation`
69- * reads best-effort on each decision. All are fail-soft, so the reads are
70- * EXPECTED — but the driver and the engine each log the fault on the way out.
66+ * syncs its objects. Every approval decision below probes `sys_organization`
67+ * (`ObjectQL.probeInstallOrganizations`) and `sys_approval_delegation`, which
68+ * `ApprovalService.lookupActiveDelegation` reads best-effort on each decision.
69+ * Both are fail-soft, so the reads are EXPECTED — but the driver and the engine
70+ * each log the fault on the way out.
7171 *
72- * ⛔ MEASURED, not copied from the probers' source: a run of this file at
73- * `logger: { level: 'info' }` emitted 25 `refused a read on '<t>'` driver lines
74- * and 25 matching `ERROR Find operation failed` engine frames — sys_user 5 /
75- * sys_member 4 / sys_position 4 / sys_user_position 4 /
76- * sys_user_permission_set 4 / sys_organization 2 / sys_approval_delegation 2.
72+ * ⚠️ [#17985] The five authz tables `resolveUserAuthzGrants` reads USED TO BE on
73+ * this list. They are not any more, because this fixture now provisions them
74+ * (see `authzResolverObjects` below) — which is the outcome this channel's own
75+ * contract names: "a table that started resolving means the fixture now
76+ * provisions it". Being fail-soft, their refusal was not just noise: `tryFind`
77+ * answers `[]` for a missing table, so every approval decision here resolved
78+ * its grants from reads that never happened. ⛔ The count fell because the
79+ * reads SUCCEED, not because anything was quietened.
80+ *
81+ * ⛔ MEASURED, not copied from the probers' source. At `225197cdb`-era HEAD,
82+ * before this fixture provisioned anything, a run emitted 25 `refused a read on
83+ * '<t>'` driver lines and 25 matching engine frames — sys_user 5 / sys_member 4
84+ * / sys_position 4 / sys_user_position 4 / sys_user_permission_set 4 /
85+ * sys_organization 2 / sys_approval_delegation 2. The 21 authz ones are gone;
86+ * the remaining two channels are the ones declared below.
7787 *
7888 * ## Why a capture instead of the blanket `silent` this replaces
7989 *
@@ -87,15 +97,102 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
8797 * happen. A capture nobody asserts is a mute.
8898 */
8999const EXPECTED_ABSENT_PROBE_TABLES = [
90- 'sys_user' ,
91- 'sys_member' ,
92- 'sys_user_position' ,
93- 'sys_user_permission_set' ,
94- 'sys_position' ,
95100 'sys_organization' ,
96101 'sys_approval_delegation' ,
97102] as const ;
98103
104+ /**
105+ * ⚠️ [#17985] The authz objects a real approval decision makes core's
106+ * `resolveUserAuthzGrants` read. They belong to `@objectstack/plugin-auth` and
107+ * `@objectstack/plugin-security`; they are spelled LOCALLY here, with only the
108+ * columns that resolver reads, so this fixture adds no dependency edge onto
109+ * either package (the `find-envelope-limb-removal.test.ts` /
110+ * `signup-existing-address-refusal.test.ts` precedent, applied by PR #17982 to
111+ * the two `plugin-auth` fixtures that carried the same defect).
112+ *
113+ * Columns, and why each one is here — every other column of the real objects is
114+ * deliberately absent, because no read on this path touches it:
115+ * `sys_user` id (filter), email (the RLS owner-email fallback)
116+ * `sys_member` user_id / organization_id (filters), role
117+ * `sys_user_position` user_id (filter), position, organization_id
118+ * `sys_user_permission_set` user_id (filter), permission_set_id, organization_id
119+ * `sys_position` name (filter), id, active (`isRowActive`),
120+ * organization_id (the driver's tenant scope)
121+ *
122+ * ⛔ `sys_position_permission_set` and `sys_permission_set` are NOT here: the
123+ * resolver reaches them only once a `sys_position` row resolves and a
124+ * permission-set id is collected, and these cases seed neither — registering
125+ * them would add fixture surface no read in this file touches. The ADR-0091
126+ * validity columns are absent for the reason `sys_member` lacks them today:
127+ * `isGrantActive` reads an absent bound as unbounded, so declaring them would
128+ * change no verdict here.
129+ */
130+ const authzResolverObjects = [
131+ {
132+ owner : '@objectstack/plugin-auth' ,
133+ def : {
134+ name : 'sys_user' ,
135+ label : 'User' ,
136+ fields : {
137+ id : { name : 'id' , type : 'text' as const , primaryKey : true } ,
138+ email : { name : 'email' , type : 'text' as const } ,
139+ } ,
140+ } ,
141+ } ,
142+ {
143+ owner : '@objectstack/plugin-auth' ,
144+ def : {
145+ name : 'sys_member' ,
146+ label : 'Member' ,
147+ fields : {
148+ id : { name : 'id' , type : 'text' as const , primaryKey : true } ,
149+ user_id : { name : 'user_id' , type : 'text' as const } ,
150+ organization_id : { name : 'organization_id' , type : 'text' as const } ,
151+ role : { name : 'role' , type : 'text' as const } ,
152+ } ,
153+ } ,
154+ } ,
155+ {
156+ owner : '@objectstack/plugin-security' ,
157+ def : {
158+ name : 'sys_user_position' ,
159+ label : 'User Position' ,
160+ fields : {
161+ id : { name : 'id' , type : 'text' as const , primaryKey : true } ,
162+ user_id : { name : 'user_id' , type : 'text' as const } ,
163+ position : { name : 'position' , type : 'text' as const } ,
164+ organization_id : { name : 'organization_id' , type : 'text' as const } ,
165+ } ,
166+ } ,
167+ } ,
168+ {
169+ owner : '@objectstack/plugin-security' ,
170+ def : {
171+ name : 'sys_user_permission_set' ,
172+ label : 'User Permission Set' ,
173+ fields : {
174+ id : { name : 'id' , type : 'text' as const , primaryKey : true } ,
175+ user_id : { name : 'user_id' , type : 'text' as const } ,
176+ permission_set_id : { name : 'permission_set_id' , type : 'text' as const } ,
177+ organization_id : { name : 'organization_id' , type : 'text' as const } ,
178+ } ,
179+ } ,
180+ } ,
181+ {
182+ owner : '@objectstack/plugin-security' ,
183+ def : {
184+ name : 'sys_position' ,
185+ label : 'Position' ,
186+ fields : {
187+ id : { name : 'id' , type : 'text' as const , primaryKey : true } ,
188+ name : { name : 'name' , type : 'text' as const } ,
189+ active : { name : 'active' , type : 'boolean' as const } ,
190+ organization_id : { name : 'organization_id' , type : 'text' as const } ,
191+ } ,
192+ } ,
193+ } ,
194+ ] as const ;
195+
99196/** [#11081] Shared by both kernels this file boots; asserted once in `afterAll`. */
100197const noise = captureExpectedReadRefusals ( [ ...EXPECTED_ABSENT_PROBE_TABLES ] ) ;
101198
@@ -207,6 +304,12 @@ describe('an approval decision cascades as the deciding user (#3783)', () => {
207304 for ( const def of [ opportunity , SysApprovalRequest , SysApprovalAction , SysApprovalApprover ] ) {
208305 objectql . registry . registerObject ( def as any , 'approvals-test' , 'approvals-test' ) ;
209306 }
307+ // [#17985] The authz resolver's own reads, so a decision resolves its
308+ // grants from reads that HAPPEN rather than from `tryFind`'s empty answer
309+ // for a missing table.
310+ for ( const o of authzResolverObjects ) {
311+ objectql . registry . registerObject ( o . def as any , o . owner ) ;
312+ }
210313 // Real DDL for all four objects — including the three sys_approval_* tables
211314 // the ApprovalService writes through.
212315 await objectql . syncSchemas ( ) ;
0 commit comments