44 createLinkedProjectTeamResourceShareService ,
55} from '../../src/design-systems/team-project-share.js' ;
66import {
7+ TeamResourceAuthorityUnavailableError ,
78 TeamResourceShareForbiddenError ,
89 type TeamResourceRequestScope ,
910 type TeamResourceShareService ,
@@ -31,6 +32,10 @@ function fixture() {
3132 let failNextProjectShare = false ;
3233 let projectUnshareFailuresRemaining = 0 ;
3334 let failNextProjectPersist = false ;
35+ let authoritativeShared : Set < string > | null = null ;
36+ let authoritativeReadError : Error | null = null ;
37+ let authoritativeCanUnshare = true ;
38+ let projectCreatorMemberId : string | null = 'member-owner' ;
3439
3540 const resource : TeamResourceShareService = {
3641 configured : true ,
@@ -52,7 +57,12 @@ function fixture() {
5257 async sharedIds ( ) {
5358 return [ ...shared ] ;
5459 } ,
55- async sharedResources ( ) {
60+ async sharedResources ( _scope , readOptions ) {
61+ if ( readOptions ?. authoritative ) {
62+ if ( authoritativeReadError ) throw authoritativeReadError ;
63+ return [ ...( authoritativeShared ?? shared ) ]
64+ . map ( ( id ) => ( { id, canUnshare : authoritativeCanUnshare } ) ) ;
65+ }
5666 return [ ...shared ] . map ( ( id ) => ( { id, canUnshare : true } ) ) ;
5767 } ,
5868 isShared ( resourceId ) {
@@ -67,7 +77,7 @@ function fixture() {
6777 projectExists : ( projectId ) => projectVisibility . has ( projectId ) ,
6878 getProjectBinding : ( ) => ( {
6979 workspaceId : 'ws-a' ,
70- createdByWorkspaceMemberId : 'member-owner' ,
80+ createdByWorkspaceMemberId : projectCreatorMemberId ,
7181 } ) ,
7282 async publishProject ( projectId ) {
7383 calls . push ( `project:team:${ projectId } ` ) ;
@@ -106,6 +116,18 @@ function fixture() {
106116 failNextProjectShare : ( ) => { failNextProjectShare = true ; } ,
107117 failNextProjectUnshare : ( ) => { projectUnshareFailuresRemaining = 1 ; } ,
108118 failNextProjectPersist : ( ) => { failNextProjectPersist = true ; } ,
119+ setAuthoritativeShared : ( ids : string [ ] ) => {
120+ authoritativeShared = new Set ( ids ) ;
121+ } ,
122+ failAuthoritativeRead : ( ) => {
123+ authoritativeReadError = new Error ( 'authoritative hub unavailable' ) ;
124+ } ,
125+ denyAuthoritativeUnshare : ( ) => {
126+ authoritativeCanUnshare = false ;
127+ } ,
128+ clearProjectCreator : ( ) => {
129+ projectCreatorMemberId = null ;
130+ } ,
109131 } ;
110132}
111133
@@ -264,7 +286,7 @@ describe('design-system team share linked backing project', () => {
264286 const f = fixture ( ) ;
265287 await f . service . share ( 'user:brand' , scope ( ) ) ;
266288 f . calls . length = 0 ;
267- f . service . sharedResources = async ( ) => [ { id : 'user:brand' , canUnshare : false } ] ;
289+ f . denyAuthoritativeUnshare ( ) ;
268290
269291 await expect ( f . service . unshare ( 'user:brand' , scope ( ) ) )
270292 . rejects . toBeInstanceOf ( TeamResourceShareForbiddenError ) ;
@@ -273,6 +295,74 @@ describe('design-system team share linked backing project', () => {
273295 expect ( f . calls ) . toEqual ( [ ] ) ;
274296 } ) ;
275297
298+ it ( 'treats an authoritative empty list as an idempotent unshare without touching the project' , async ( ) => {
299+ const f = fixture ( ) ;
300+ await f . service . share ( 'user:brand' , scope ( ) ) ;
301+ f . calls . length = 0 ;
302+ f . setAuthoritativeShared ( [ ] ) ;
303+
304+ await expect ( f . service . unshare ( 'user:brand' , scope ( ) ) ) . resolves . toBe ( false ) ;
305+
306+ expect ( f . projectVisibility . get ( 'project-brand' ) ) . toBe ( 'team' ) ;
307+ expect ( f . calls ) . toEqual ( [ ] ) ;
308+ } ) ;
309+
310+ it ( 'does not undo an independent project share on a repeated design-system DELETE' , async ( ) => {
311+ const f = fixture ( ) ;
312+ await f . service . share ( 'user:brand' , scope ( ) ) ;
313+ await expect ( f . service . unshare ( 'user:brand' , scope ( ) ) ) . resolves . toBe ( true ) ;
314+
315+ // The design system is already absent from the authoritative Team index,
316+ // while the backing project has since been shared independently.
317+ f . projectVisibility . set ( 'project-brand' , 'team' ) ;
318+ f . calls . length = 0 ;
319+
320+ await expect ( f . service . unshare ( 'user:brand' , scope ( ) ) ) . resolves . toBe ( false ) ;
321+
322+ expect ( f . projectVisibility . get ( 'project-brand' ) ) . toBe ( 'team' ) ;
323+ expect ( f . calls ) . toEqual ( [ ] ) ;
324+ } ) ;
325+
326+ it ( 'fails before touching the project when the authoritative Team index is unavailable' , async ( ) => {
327+ const f = fixture ( ) ;
328+ await f . service . share ( 'user:brand' , scope ( ) ) ;
329+ f . calls . length = 0 ;
330+ f . failAuthoritativeRead ( ) ;
331+
332+ await expect ( f . service . unshare ( 'user:brand' , scope ( ) ) )
333+ . rejects . toBeInstanceOf ( TeamResourceAuthorityUnavailableError ) ;
334+
335+ expect ( f . projectVisibility . get ( 'project-brand' ) ) . toBe ( 'team' ) ;
336+ expect ( f . calls ) . toEqual ( [ ] ) ;
337+ } ) ;
338+
339+ it ( 'downgrades hub owner/admin permission when the linked project creator gate denies mutation' , async ( ) => {
340+ const f = fixture ( ) ;
341+ await f . service . share ( 'user:brand' , scope ( ) ) ;
342+ const adminScope : TeamResourceRequestScope = {
343+ principal : {
344+ ...scope ( ) . principal ,
345+ memberId : 'member-admin' ,
346+ role : 'admin' ,
347+ } ,
348+ canShare : true ,
349+ } ;
350+
351+ await expect ( f . service . sharedResources ( adminScope ) ) . resolves . toEqual ( [
352+ { id : 'user:brand' , canUnshare : false } ,
353+ ] ) ;
354+ } ) ;
355+
356+ it ( 'fails closed when no exact linked-project creator can be proven' , async ( ) => {
357+ const f = fixture ( ) ;
358+ await f . service . share ( 'user:brand' , scope ( ) ) ;
359+ f . clearProjectCreator ( ) ;
360+
361+ await expect ( f . service . sharedResources ( scope ( ) ) ) . resolves . toEqual ( [
362+ { id : 'user:brand' , canUnshare : false } ,
363+ ] ) ;
364+ } ) ;
365+
276366 it ( 'fails closed across Workspace A→B before either hub mutation runs' , async ( ) => {
277367 const f = fixture ( ) ;
278368
0 commit comments