@@ -28,6 +28,7 @@ import { generateId } from '@sim/utils/id'
2828import { toRecord } from '@sim/utils/object'
2929import { PASTE_RENDER_THRESHOLDS } from '@sim/utils/paste'
3030import { generateRandomHex } from '@sim/utils/random'
31+ import { compareStrings } from '@sim/utils/string'
3132import postgres from 'postgres'
3233import { chatContextsSchema } from '@/lib/api/contracts/mothership-chat-context'
3334import { createOrganizationWithOwner } from '@/lib/billing/organizations/create-organization'
@@ -714,6 +715,25 @@ try {
714715 await expect ( shell . locator ( 'body' ) ) . toHaveAttribute ( 'data-capture-receipts' , String ( before + 1 ) )
715716 }
716717 const attachments = ( ) => frame ( ) . getByRole ( 'group' , { name : 'Page attached to next message' } )
718+ /**
719+ * The organization's chat access as the panel next reads it. Call it after a policy change: a
720+ * read the panel starts from then on is answered under the change.
721+ */
722+ const organizationAccessReread = async ( timeout : number ) => {
723+ const request = await shell . waitForRequest (
724+ ( candidate ) => {
725+ const url = new URL ( candidate . url ( ) )
726+ return (
727+ url . pathname === `/api/organizations/${ organizationId } ` &&
728+ url . searchParams . get ( 'include' ) === 'chat-access'
729+ )
730+ } ,
731+ { timeout }
732+ )
733+ const response = await request . response ( )
734+ assert ( response ?. ok ( ) , 'The organization chat access re-read failed' )
735+ return toRecord ( toRecord ( await response . json ( ) ) . chatAccess )
736+ }
717737 const isUploadCompletion = ( url : URL ) =>
718738 url . pathname . startsWith ( '/api/files/uploads/' ) && url . pathname . endsWith ( '/complete' )
719739 const uploadFile = async ( name : string , add : ( ) => Promise < void > ) : Promise < string > => {
@@ -832,7 +852,7 @@ try {
832852 window . addEventListener ( 'sim-qa-release-capture' , release )
833853 window . addEventListener ( 'sim-qa-dispose-capture' , dispose )
834854 } , `chrome-extension://${ extensionId } ` )
835- const verifyPendingCaptureDrop = async ( scope : 'workspace' | 'organization' | 'assistant' ) => {
855+ const verifyPendingCaptureDrop = async ( scope : 'workspace' | 'organization' ) => {
836856 const app = frame ( )
837857 const input = app . getByRole ( 'textbox' , {
838858 name : scope === 'workspace' ? 'Message' : 'Ask Sim' ,
@@ -937,7 +957,7 @@ try {
937957 body [ scope === 'workspace' ? 'workspaceId' : 'organizationId' ] ,
938958 scope === 'workspace' ? alphaId : organizationId
939959 )
940- if ( scope === 'assistant ' ) assert . equal ( body . mode , 'assistant ' )
960+ if ( scope === 'organization ' ) assert . equal ( body . mode , 'agent ' )
941961 const pages = submittedPages ( body )
942962 assert . equal ( pages . length , 1 )
943963 assert ( pages [ 0 ] . text . includes ( 'ORCHID-482' ) )
@@ -2055,26 +2075,21 @@ try {
20552075 const permissionGroupId = generateId ( )
20562076 await shell . bringToFront ( )
20572077 await sql `insert into permission_group (id, organization_id, name, config, created_by, is_default)
2058- values (${ permissionGroupId } , ${ scope . organizationId } , 'Revoked Build access ',
2078+ values (${ permissionGroupId } , ${ scope . organizationId } , 'No workspace creation ',
20592079 ${ sql . json ( { disableWorkspaceCreation : true } ) } , ${ userId } , true)`
20602080 try {
2061- await expect ( frame ( ) . getByText ( 'This chat is unavailable.' ) ) . toBeVisible ( {
2062- timeout : ACCESS_REVALIDATION_TIMEOUT_MS ,
2063- } )
2081+ // Chat follows `copilot.use` and the deployment, so an admitted chat stays open.
2082+ assert . equal (
2083+ ( await organizationAccessReread ( ACCESS_REVALIDATION_TIMEOUT_MS ) ) . mothershipAvailable ,
2084+ true
2085+ )
2086+ await expect ( frame ( ) . getByText ( 'This chat is unavailable.' ) ) . toHaveCount ( 0 )
20642087 await expect (
20652088 frame ( ) . getByRole ( 'textbox' , { name : scope . inputLabel , exact : true } )
2066- ) . toHaveCount ( 0 )
2067- await expect ( attachments ( ) ) . toHaveCount ( 0 )
2068- await captureScreenshot ( 'admitted-build-access-revoked' )
2089+ ) . toBeVisible ( )
2090+ await captureScreenshot ( 'admitted-chat-kept-without-workspace-creation' )
20692091 const [ savedChat ] = await sql `select config from copilot_chats where id=${ chatId } `
20702092 assert . equal ( toRecord ( savedChat . config ) . conversationMode , 'agent' )
2071- await frame ( ) . getByRole ( 'button' , { name : 'Back to chat' , exact : true } ) . click ( )
2072- await expect (
2073- frame ( ) . getByRole ( 'button' , { name : 'Attach image' , exact : true } )
2074- ) . toBeVisible ( )
2075- await expect . poll ( ( ) => new URL ( frame ( ) . url ( ) ) . searchParams . get ( 'chat' ) ) . toBeNull ( )
2076- await expect ( attachments ( ) ) . toHaveCount ( 0 )
2077- await captureScreenshot ( 'assistant-after-build-revocation' )
20782093 } finally {
20792094 await sql `delete from permission_group where id=${ permissionGroupId } `
20802095 }
@@ -2214,12 +2229,35 @@ try {
22142229 `${ base . origin } /extension/o/${ otherOrganizationId } ?chat=${ otherOrganizationChatId } `
22152230 )
22162231 await expect ( frame ( ) . getByText ( 'This organization is unavailable.' ) ) . toBeVisible ( )
2217- for ( const chatId of [ historyId , otherOrganizationChatId ] ) {
2218- await navigate ( `${ base . origin } /extension/o/${ organizationId } ?chat=${ chatId } ` )
2219- await expect ( frame ( ) . getByText ( 'This chat is unavailable.' ) ) . toBeVisible ( )
2220- await expect ( frame ( ) . getByRole ( 'textbox' , { name : 'Ask Sim' , exact : true } ) ) . toHaveCount ( 0 )
2221- }
2232+ await navigate ( `${ base . origin } /extension/o/${ organizationId } ?chat=${ otherOrganizationChatId } ` )
2233+ await expect ( frame ( ) . getByText ( 'This chat is unavailable.' ) ) . toBeVisible ( )
2234+ await expect ( frame ( ) . getByRole ( 'textbox' , { name : 'Ask Sim' , exact : true } ) ) . toHaveCount ( 0 )
22222235 await screenshot ( 'organization-wrong-scope-history' )
2236+ // The organization's chat list holds the viewer's workspace chats; one opens here and sends
2237+ // under its own workspace, never the organization.
2238+ await navigate ( `${ base . origin } /extension/o/${ organizationId } ?chat=${ historyId } ` )
2239+ await expect (
2240+ frame ( ) . getByText ( 'QA history response.' , { exact : false } ) . first ( )
2241+ ) . toBeVisible ( )
2242+ let workspaceChatSend : unknown
2243+ await shell . route ( '**/api/mothership/chat' , async ( route ) => {
2244+ if ( route . request ( ) . method ( ) !== 'POST' ) return route . continue ( )
2245+ workspaceChatSend = route . request ( ) . postDataJSON ( )
2246+ injectedRequests . add ( route . request ( ) )
2247+ await route . fulfill ( { status : 403 , json : { error : 'QA admission rejected' } } )
2248+ } )
2249+ try {
2250+ const composer = frame ( ) . getByRole ( 'textbox' , { name : 'Ask Sim' , exact : true } )
2251+ await composer . fill ( 'QA workspace chat follow-up' )
2252+ await composer . press ( 'Enter' )
2253+ await expect . poll ( ( ) => workspaceChatSend ) . toBeTruthy ( )
2254+ const payload = toRecord ( workspaceChatSend )
2255+ assert . equal ( payload . chatId , historyId )
2256+ assert . equal ( payload . workspaceId , alphaId )
2257+ assert . equal ( payload . organizationId , undefined )
2258+ } finally {
2259+ await shell . unroute ( '**/api/mothership/chat' )
2260+ }
22232261 }
22242262 )
22252263 const setActiveOrganization = async ( ) => {
@@ -2973,45 +3011,44 @@ try {
29733011 )
29743012 }
29753013 await checkAccess (
2976- 'Organization permission changes switch a new chat to Assistant without losing its draft' ,
3014+ 'A workspace-creation restriction keeps a new organization chat in Build with its draft' ,
29773015 async ( ) => {
29783016 assert ( userId )
29793017 await navigate ( `${ base . origin } /extension/o/${ organizationId } ` )
29803018 const input = frame ( ) . getByRole ( 'textbox' , { name : 'Ask Sim' , exact : true } )
3019+ const attachFileButton = frame ( ) . getByRole ( 'button' , { name : 'Attach file' , exact : true } )
29813020 await expect ( input ) . toBeVisible ( )
2982- await expect ( frame ( ) . getByRole ( 'button' , { name : 'Attach file' , exact : true } ) ) . toBeVisible ( )
3021+ await expect ( attachFileButton ) . toBeVisible ( )
29833022 await capture ( )
2984- await input . fill ( 'Keep this draft when Build access changes.' )
2985- await attachFile ( )
3023+ await input . fill ( 'Keep this draft when workspace creation changes.' )
3024+ const notesKey = await attachFile ( )
29863025 await shell . bringToFront ( )
29873026 await sql `insert into permission_group (id, organization_id, name, config, created_by, is_default)
2988- values (${ generateId ( ) } , ${ organizationId } , 'Search only ',
3027+ values (${ generateId ( ) } , ${ organizationId } , 'No workspace creation ',
29893028 ${ sql . json ( { disableWorkspaceCreation : true } ) } , ${ userId } , true)`
3029+ // Chat follows `copilot.use` and the deployment, never workspace creation.
3030+ const reread = organizationAccessReread ( revalidationTimeout )
29903031 await advanceAccessRevalidation ( )
2991- await expect ( frame ( ) . getByRole ( 'button' , { name : 'Attach image' , exact : true } ) ) . toBeVisible ( {
2992- timeout : revalidationTimeout ,
2993- } )
2994- await expect ( input ) . toHaveValue ( 'Keep this draft when Build access changes.' )
2995- await expect ( attachments ( ) ) . toHaveCount ( 1 )
2996- await input . press ( 'Enter' )
2997- await expect ( frame ( ) . getByText ( / R e m o v e u n s u p p o r t e d f i l e s / ) ) . toBeVisible ( )
2998- await expect ( input ) . toHaveValue ( 'Keep this draft when Build access changes.' )
2999- await expect ( attachments ( ) ) . toHaveCount ( 1 )
3000- await screenshot ( 'organization-mode-file-recovery' )
3001- await frame ( ) . getByRole ( 'button' , { name : 'Remove extension-notes.txt' , exact : true } ) . click ( )
3002- await expect ( frame ( ) . getByText ( / R e m o v e u n s u p p o r t e d f i l e s / ) ) . not . toBeVisible ( )
3003- await attachments ( ) . first ( ) . getByRole ( 'button' , { name : 'Remove page context' } ) . click ( )
3004- await capture ( 'shortcut' )
3032+ assert . equal ( ( await reread ) . mothershipAvailable , true )
3033+ await expect ( frame ( ) . getByText ( 'This chat is unavailable.' ) ) . toHaveCount ( 0 )
3034+ await expect ( attachFileButton ) . toBeVisible ( )
3035+ await expect ( frame ( ) . getByRole ( 'button' , { name : 'Attach image' , exact : true } ) ) . toHaveCount (
3036+ 0
3037+ )
3038+ await expect ( input ) . toHaveValue ( 'Keep this draft when workspace creation changes.' )
30053039 await expect ( attachments ( ) ) . toHaveCount ( 1 )
3040+ await expect (
3041+ frame ( ) . getByRole ( 'button' , { name : 'Remove extension-notes.txt' , exact : true } )
3042+ ) . toBeVisible ( )
30063043 const droppedImage = ! liveChat
30073044 ? await attachScreenshot (
30083045 frame ( ) . getByRole ( 'heading' , { name : 'How can I help?' , exact : true } )
30093046 )
30103047 : undefined
30113048 await expect ( frame ( ) . getByRole ( 'button' , { name : 'Voice input' , exact : true } ) ) . toBeVisible ( )
3012- await screenshot ( 'organization-assistant -page' )
3049+ await screenshot ( 'organization-build -page' )
30133050 await setPanelViewport ( 320 , 844 )
3014- await screenshot ( 'assistant -microphone-320' )
3051+ await screenshot ( 'build -microphone-320' )
30153052 await setPanelViewport ( 390 , 844 )
30163053 if ( ! liveChat )
30173054 await shell . route ( '**/api/mothership/chat' , ( route ) =>
@@ -3026,11 +3063,12 @@ try {
30263063 )
30273064 await input . press ( 'Enter' )
30283065 const body = toRecord ( ( await submitted ) . postDataJSON ( ) )
3029- if ( droppedImage ) {
3030- assert ( Array . isArray ( body . fileAttachments ) && body . fileAttachments . length === 1 )
3031- assert . equal ( toRecord ( body . fileAttachments [ 0 ] ) . key , droppedImage )
3032- }
3033- assert . equal ( body . mode , 'assistant' )
3066+ assert ( Array . isArray ( body . fileAttachments ) )
3067+ assert . deepEqual (
3068+ body . fileAttachments . map ( ( file ) => String ( toRecord ( file ) . key ) ) . sort ( compareStrings ) ,
3069+ [ notesKey , ...( droppedImage ? [ droppedImage ] : [ ] ) ] . sort ( compareStrings )
3070+ )
3071+ assert . equal ( body . mode , 'agent' )
30343072 assert . equal ( body . organizationId , organizationId )
30353073 assert . equal ( body . workspaceId , undefined )
30363074 assert ( Array . isArray ( body . contexts ) && body . contexts . length === 1 )
@@ -3057,16 +3095,16 @@ try {
30573095 await sql `select organization_id, workspace_id, config from copilot_chats where id=${ chatId } `
30583096 assert . equal ( chat . organization_id , organizationId )
30593097 assert . equal ( chat . workspace_id , null )
3060- assert . equal ( toRecord ( chat . config ) . conversationMode , 'assistant ' )
3098+ assert . equal ( toRecord ( chat . config ) . conversationMode , 'agent ' )
30613099 await expect (
30623100 frame ( ) . getByRole ( 'button' , { name : 'Copy message' , exact : true } )
30633101 ) . toBeVisible ( { timeout : 120_000 } )
3064- await screenshot ( 'organization-assistant -response' )
3102+ await screenshot ( 'organization-build -response' )
30653103 } else await shell . unroute ( '**/api/mothership/chat' )
30663104 }
30673105 )
30683106 await checkAccess (
3069- 'Restricted Assistant freezes body file drops while a page reference is pending' ,
3107+ 'A workspace-creation restriction still freezes body file drops while a page reference is pending' ,
30703108 async ( ) => {
30713109 await shell . close ( )
30723110 panel = await openPanel ( )
@@ -3078,9 +3116,9 @@ try {
30783116 await observeCaptureReceipts ( )
30793117 await navigate ( `${ base . origin } /extension/o/${ organizationId } ` )
30803118 await expect (
3081- frame ( ) . getByRole ( 'button' , { name : 'Attach image ' , exact : true } )
3119+ frame ( ) . getByRole ( 'button' , { name : 'Attach file ' , exact : true } )
30823120 ) . toBeVisible ( )
3083- await verifyPendingCaptureDrop ( 'assistant ' )
3121+ await verifyPendingCaptureDrop ( 'organization ' )
30843122 } finally {
30853123 await captureGate . dispose ( )
30863124 }
0 commit comments