Skip to content

Commit 416a691

Browse files
committed
test(extension): assert chat access by copilot.use and workspace chats on the organization surface
fb3f895 removed canBuild: a permission group that only disables workspace creation no longer moves a new organization chat to Assistant or closes an admitted one. The access checks now prove, after a re-read of the organization's chat access, that the chat keeps Build, its draft, its file and page attachments, and sends mode agent; the pending-capture drop runs on that Build chat. 299a1ef opens a viewer's workspace chat from the organization's list: the composer check now expects it to open on the organization surface and send under its workspace, while another organization's chat stays unavailable.
1 parent 03cf868 commit 416a691

1 file changed

Lines changed: 90 additions & 52 deletions

File tree

‎apps/sim/scripts/test-browser-extension-e2e.ts‎

Lines changed: 90 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { generateId } from '@sim/utils/id'
2828
import { toRecord } from '@sim/utils/object'
2929
import { PASTE_RENDER_THRESHOLDS } from '@sim/utils/paste'
3030
import { generateRandomHex } from '@sim/utils/random'
31+
import { compareStrings } from '@sim/utils/string'
3132
import postgres from 'postgres'
3233
import { chatContextsSchema } from '@/lib/api/contracts/mothership-chat-context'
3334
import { 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(/Remove unsupported files/)).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(/Remove unsupported files/)).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

Comments
 (0)