22
33import { describe , it , expect , beforeEach , vi } from 'vitest' ;
44import { assertEngineDeleteDispatch } from '@objectstack/objectql' ;
5- import { SharingService } from './sharing-service.js' ;
5+ import { SharingService , type SharingServiceOptions } from './sharing-service.js' ;
66import { buildSharingMiddleware } from './sharing-plugin.js' ;
77import { bootRequestContext } from './exec-context-seam.testkit.js' ;
88
@@ -1206,6 +1206,15 @@ describe('[#5859] resolveOwnerScopeIds fills the AUTHORITATIVE organization', ()
12061206 } ) ;
12071207 }
12081208
1209+ /**
1210+ * [ADR-0105 D1] The deployment posture, stated the way the `tenancy` service
1211+ * states it. `single` = no organization wall (the pure single-tenant end of
1212+ * the spectrum the ADR-0057 D1 proofs boot); `group` / `isolated` = a wall is
1213+ * in force. Every fixture below says which deployment it is talking about,
1214+ * because after #5859 the answer to "no active organization" depends on it.
1215+ */
1216+ const posture = ( p : string ) => ( ) => ( { posture : p } ) ;
1217+
12091218 let engine : ReturnType < typeof makeFakeEngine > ;
12101219 beforeEach ( ( ) => {
12111220 engine = makeFakeEngine ( { account : ACCOUNT_SCHEMA , sys_record_share : { } } ) ;
@@ -1233,6 +1242,7 @@ describe('[#5859] resolveOwnerScopeIds fills the AUTHORITATIVE organization', ()
12331242 engine,
12341243 securityService : ( ) => MANAGER_PROBE ,
12351244 hierarchyResolver : orgScopedResolver ( seen ) ,
1245+ tenancy : posture ( 'isolated' ) ,
12361246 } ) ;
12371247 const bob = await bootRequestContext ( { userId : 'bob' , activeOrganizationId : 'org_a' } ) ;
12381248
@@ -1252,6 +1262,7 @@ describe('[#5859] resolveOwnerScopeIds fills the AUTHORITATIVE organization', ()
12521262 engine,
12531263 securityService : ( ) => MANAGER_PROBE ,
12541264 hierarchyResolver : orgScopedResolver ( seen ) ,
1265+ tenancy : posture ( 'group' ) ,
12551266 } ) ;
12561267 const bob = await bootRequestContext ( {
12571268 userId : 'bob' ,
@@ -1275,6 +1286,7 @@ describe('[#5859] resolveOwnerScopeIds fills the AUTHORITATIVE organization', ()
12751286 const svc = new SharingService ( {
12761287 engine,
12771288 hierarchyResolver : orgScopedResolver ( seen ) ,
1289+ tenancy : posture ( 'group' ) ,
12781290 // NO securityService: no `modifyAllRecords` bypass, and no owner-only RLS
12791291 // anywhere — the sharing service is the ONLY gate in this fixture, which
12801292 // is the deployment shape #5852 named as unprotected (the probe app was
@@ -1296,31 +1308,99 @@ describe('[#5859] resolveOwnerScopeIds fills the AUTHORITATIVE organization', ()
12961308 expect ( filter ) . toEqual ( { owner_id : { $in : [ 'alice' , 'bob' ] } } ) ;
12971309 } ) ;
12981310
1299- it ( 'no active organization is reported as an HONEST null — never the deprecated alias, never a stand-in' , async ( ) => {
1311+ // ── [ADR-0105 D1] The posture fork on "no active organization" ────────
1312+ // Same caller, same absent org, two deployments, two answers — the same
1313+ // fork Layer 0 already makes (`computeTenantLayer0Filter`: `single` inert,
1314+ // walled postures deny). Both directions are pinned; neither is a default.
1315+
1316+ it ( 'single posture: no organization at all → DEPTH still widens, and the null is HONEST' , async ( ) => {
13001317 const seen : any [ ] = [ ] ;
13011318 const svc = new SharingService ( {
13021319 engine,
13031320 securityService : ( ) => MANAGER_PROBE ,
13041321 hierarchyResolver : orgScopedResolver ( seen ) ,
1322+ // The pure single-tenant end of the spectrum — the shape the verify
1323+ // harness boots deliberately (`autoDefaultOrganization: false`) and the
1324+ // ADR-0057 D1 dogfood proofs run in. "No org" here is the one implicit
1325+ // tenant, not "every org", so refusing would retire DEPTH for every
1326+ // org-less deployment.
1327+ tenancy : posture ( 'single' ) ,
13051328 } ) ;
1306- // A session with no active organization — the supported pure-single-tenant
1307- // shape (the verify harness boots it deliberately: `autoDefaultOrganization:
1308- // false`), not an anomaly this layer invents a verdict for.
13091329 const orgless = await bootRequestContext ( { userId : 'bob' , activeOrganizationId : null } ) ;
13101330 expect ( ( orgless as any ) . tenantId ) . toBeUndefined ( ) ;
13111331
1312- await svc . canManageShares ( 'account' , 'a1' , orgless ) ;
1332+ expect ( await svc . canManageShares ( 'account' , 'a1' , orgless ) ) . toBe ( true ) ;
13131333 expect ( seen ) . toHaveLength ( 1 ) ;
1314- // `string | null` per the contract: the producer states the absence rather
1315- // than omitting the key (which is what let #5852's resolver read
1316- // `undefined` and query unscoped without anyone noticing).
1334+ // `string | null` per the contract: the producer STATES the absence rather
1335+ // than omitting the key (omission is what let a resolver read `undefined`
1336+ // and query unscoped without anyone noticing). What a resolver must then do
1337+ // with that null is its own obligation — cloud#1148's half.
13171338 expect ( seen [ 0 ] ) . toHaveProperty ( 'organizationId' ) ;
13181339 expect ( seen [ 0 ] . organizationId ) . toBeNull ( ) ;
1319- // What the resolver must DO with that null is its own contract obligation
1320- // ("Fail CLOSED on a missing organization … 'no org' is not 'every org'",
1321- // IHierarchyScopeResolver.resolveOwnerIds) — cloud#1148's half. Whether the
1322- // OPEN edition should additionally refuse to consult it is the open
1323- // tenancy-posture question on #5859; deliberately not decided here.
1340+ } ) ;
1341+
1342+ it . each ( [ 'group' , 'isolated' ] ) (
1343+ '%s posture: no active organization → the resolver is NOT consulted, loudly' ,
1344+ async ( p ) => {
1345+ const seen : any [ ] = [ ] ;
1346+ const warn = vi . fn ( ) ;
1347+ const svc = new SharingService ( {
1348+ engine,
1349+ securityService : ( ) => MANAGER_PROBE ,
1350+ hierarchyResolver : orgScopedResolver ( seen ) ,
1351+ tenancy : posture ( p ) ,
1352+ logger : { warn } ,
1353+ } ) ;
1354+ const orgless = await bootRequestContext ( { userId : 'bob' , activeOrganizationId : null } ) ;
1355+
1356+ // A wall is in force and the caller carries no organization to scope by:
1357+ // owner-only, never widened — and the resolver is not even asked, so an
1358+ // out-of-tree implementation cannot answer for every org on its own.
1359+ expect ( await svc . canManageShares ( 'account' , 'a1' , orgless ) ) . toBe ( false ) ;
1360+ expect ( await svc . canEdit ( 'account' , 'b1' , { ...( orgless as any ) , __writeScope : 'unit' } ) ) . toBe ( false ) ;
1361+ expect ( seen ) . toHaveLength ( 0 ) ;
1362+ expect ( warn ) . toHaveBeenCalled ( ) ;
1363+ const [ message , meta ] = warn . mock . calls [ 0 ] ;
1364+ expect ( String ( message ) ) . toContain ( 'organization wall is in force' ) ;
1365+ expect ( String ( message ) ) . toContain ( 'ADR-0095 D1 / ADR-0105 D1' ) ;
1366+ expect ( meta ) . toMatchObject ( { userId : 'bob' } ) ;
1367+ } ,
1368+ ) ;
1369+
1370+ it ( 'an UNRESOLVABLE posture is not evidence of `single` — it refuses too' , async ( ) => {
1371+ const orgless = await bootRequestContext ( { userId : 'bob' , activeOrganizationId : null } ) ;
1372+ const probes : Array < SharingServiceOptions [ 'tenancy' ] > = [
1373+ undefined , // no `tenancy` wired at all
1374+ ( ) => null , // service not registered
1375+ ( ) => { throw new Error ( 'tenancy unavailable' ) ; } ,
1376+ ( ) => ( { posture : 'not-a-posture' } ) , // outside the vocabulary
1377+ ] ;
1378+ for ( const tenancy of probes ) {
1379+ const seen : any [ ] = [ ] ;
1380+ const svc = new SharingService ( {
1381+ engine,
1382+ securityService : ( ) => MANAGER_PROBE ,
1383+ hierarchyResolver : orgScopedResolver ( seen ) ,
1384+ tenancy,
1385+ } ) ;
1386+ expect ( await svc . canManageShares ( 'account' , 'a1' , orgless ) ) . toBe ( false ) ;
1387+ expect ( seen ) . toHaveLength ( 0 ) ;
1388+ }
1389+ } ) ;
1390+
1391+ it ( 'the legacy `isolationActive: false` shape still states "no wall" (single)' , async ( ) => {
1392+ const seen : any [ ] = [ ] ;
1393+ const svc = new SharingService ( {
1394+ engine,
1395+ securityService : ( ) => MANAGER_PROBE ,
1396+ hierarchyResolver : orgScopedResolver ( seen ) ,
1397+ // Pre-ADR-0105 `tenancy` shape — a POSITIVE statement that no wall is
1398+ // enforced, unlike a missing/unknown posture.
1399+ tenancy : ( ) => ( { isolationActive : false } ) ,
1400+ } ) ;
1401+ const orgless = await bootRequestContext ( { userId : 'bob' , activeOrganizationId : null } ) ;
1402+ expect ( await svc . canManageShares ( 'account' , 'a1' , orgless ) ) . toBe ( true ) ;
1403+ expect ( seen ) . toHaveLength ( 1 ) ;
13241404 } ) ;
13251405
13261406 it ( 'fail closed: a THROWING resolver falls back to owner-only and SAYS so' , async ( ) => {
@@ -1331,6 +1411,7 @@ describe('[#5859] resolveOwnerScopeIds fills the AUTHORITATIVE organization', ()
13311411 hierarchyResolver : ( ) => ( {
13321412 async resolveOwnerIds ( ) : Promise < string [ ] > { throw new Error ( 'resolver exploded' ) ; } ,
13331413 } ) ,
1414+ tenancy : posture ( 'isolated' ) ,
13341415 logger : { warn } ,
13351416 } ) ;
13361417 const bob = await bootRequestContext ( { userId : 'bob' , activeOrganizationId : 'org_a' } ) ;
@@ -1348,6 +1429,7 @@ describe('[#5859] resolveOwnerScopeIds fills the AUTHORITATIVE organization', ()
13481429 engine,
13491430 securityService : ( ) => MANAGER_PROBE ,
13501431 hierarchyResolver : orgScopedResolver ( seen ) ,
1432+ tenancy : posture ( 'single' ) ,
13511433 } ) ;
13521434 const blank = await bootRequestContext ( { userId : 'bob' , activeOrganizationId : ' ' } ) ;
13531435 await svc . canManageShares ( 'account' , 'a1' , blank ) ;
@@ -1356,4 +1438,17 @@ describe('[#5859] resolveOwnerScopeIds fills the AUTHORITATIVE organization', ()
13561438 // a literal that silently matches no rows and reads as "scoped" in a log.
13571439 expect ( seen [ 0 ] . organizationId ) . toBeNull ( ) ;
13581440 } ) ;
1441+
1442+ it ( 'a blank organization is ALSO an absent one under a wall (same normalization, refusing side)' , async ( ) => {
1443+ const seen : any [ ] = [ ] ;
1444+ const svc = new SharingService ( {
1445+ engine,
1446+ securityService : ( ) => MANAGER_PROBE ,
1447+ hierarchyResolver : orgScopedResolver ( seen ) ,
1448+ tenancy : posture ( 'isolated' ) ,
1449+ } ) ;
1450+ const blank = await bootRequestContext ( { userId : 'bob' , activeOrganizationId : ' ' } ) ;
1451+ expect ( await svc . canManageShares ( 'account' , 'a1' , blank ) ) . toBe ( false ) ;
1452+ expect ( seen ) . toHaveLength ( 0 ) ;
1453+ } ) ;
13591454} ) ;
0 commit comments