@@ -2266,8 +2266,9 @@ try {
22662266 async ( ) => {
22672267 assert ( userId )
22682268 const viewer = userId
2269- // A workspace of its own, owned by someone else and shared with the viewer, so revoking the
2270- // share leaves the viewer no other way in.
2269+ // A workspace of the viewer's organization, owned by someone else and shared with the viewer.
2270+ // Organization owners inherit admin on its workspaces, so access is gone only once the share
2271+ // is revoked and the viewer is demoted to a plain member.
22712272 const scopedWorkspaceId = generateId ( )
22722273 const scopedChatId = generateId ( )
22732274 await sql . begin ( async ( tx ) => {
@@ -2288,10 +2289,13 @@ try {
22882289 response . request ( ) . method ( ) === 'POST' &&
22892290 new URL ( response . url ( ) ) . pathname === '/api/mothership/chat'
22902291 )
2291- const runs = ( ) =>
2292- sql < { workspaceId : string | null ; organizationId : string | null } [ ] > `
2293- select workspace_id as "workspaceId", organization_id as "organizationId"
2294- from copilot_runs where chat_id = ${ scopedChatId } `
2292+ const runs = async ( ) =>
2293+ (
2294+ await sql < { workspaceId : string | null ; organizationId : string | null } [ ] > `
2295+ select workspace_id as "workspaceId", organization_id as "organizationId"
2296+ from copilot_runs where chat_id = ${ scopedChatId } `
2297+ ) . map ( ( { workspaceId, organizationId } ) => ( { workspaceId, organizationId } ) )
2298+ let demoted = false
22952299 try {
22962300 await navigate ( `${ base . origin } /extension/o/${ organizationId } ?chat=${ scopedChatId } ` )
22972301 await expect ( frame ( ) . getByText ( 'QA scoped response.' , { exact : true } ) ) . toBeVisible ( )
@@ -2321,19 +2325,29 @@ try {
23212325 . toMatch ( / ^ (? ! a c t i v e $ ) \w + $ / )
23222326 assert . deepEqual ( await runs ( ) , [ { workspaceId : scopedWorkspaceId , organizationId : null } ] )
23232327
2324- await sql `delete from permissions where entity_type = 'workspace' and entity_id = ${ scopedWorkspaceId } and user_id = ${ viewer } `
2328+ await sql . begin ( async ( tx ) => {
2329+ await tx `delete from permissions where entity_type = 'workspace' and entity_id = ${ scopedWorkspaceId } and user_id = ${ viewer } `
2330+ await tx `update member set role = 'member' where organization_id = ${ organizationId } and user_id = ${ viewer } `
2331+ } )
2332+ demoted = true
23252333 const followUp = chatPost ( )
23262334 await composer . fill ( 'QA scoped follow-up' )
23272335 await composer . press ( 'Enter' )
23282336 const refusal = ( await followUp ) . status ( )
2329- assert ( refusal === 403 || refusal === 404 , `The follow-up was answered ${ refusal } ` )
2337+ // The chat route answers a workspace the sender cannot reach with 400, before any run.
2338+ assert (
2339+ refusal === 400 || refusal === 403 || refusal === 404 ,
2340+ `The follow-up was answered ${ refusal } `
2341+ )
23302342 // Refused, not re-scoped: no run under the organization, and the chat keeps its workspace.
23312343 assert . deepEqual ( await runs ( ) , [ { workspaceId : scopedWorkspaceId , organizationId : null } ] )
23322344 const [ chat ] = await sql < { workspaceId : string | null ; organizationId : string | null } [ ] > `
23332345 select workspace_id as "workspaceId", organization_id as "organizationId"
23342346 from copilot_chats where id = ${ scopedChatId } `
23352347 assert . deepEqual ( chat , { workspaceId : scopedWorkspaceId , organizationId : null } )
23362348 } finally {
2349+ if ( demoted )
2350+ await sql `update member set role = 'owner' where organization_id = ${ organizationId } and user_id = ${ viewer } `
23372351 await sql `delete from permissions where entity_type = 'workspace' and entity_id = ${ scopedWorkspaceId } `
23382352 await sql `delete from workspace where id = ${ scopedWorkspaceId } `
23392353 }
0 commit comments