From ceb6111c5e4940a7b97608709ab707fcc9d62c0c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 17 Oct 2026 13:59:01 -0700 Subject: [PATCH 1/2] improvement(computer-use): roll out computer use per organization --- .../desktop/computer/availability/route.ts | 4 +- apps/sim/app/o/[organizationId]/layout.tsx | 2 +- .../[workspaceId]/home/hooks/use-chat.ts | 11 +++- .../app/workspace/[workspaceId]/layout.tsx | 2 +- apps/sim/lib/api/contracts/computer-use.ts | 10 ++- .../application/authorize.test.ts | 6 ++ .../lib/computer-use/application/authorize.ts | 2 +- .../computer-use/application/availability.ts | 61 +++++++++++++++++-- .../lib/computer-use/availability.server.ts | 6 +- apps/sim/lib/core/config/feature-flags.ts | 3 +- apps/sim/lib/desktop/index.test.ts | 23 ++++--- apps/sim/lib/desktop/index.ts | 14 +++-- apps/sim/lib/mothership/chat/payload.test.ts | 20 +++--- apps/sim/lib/mothership/chat/payload.ts | 13 +++- 14 files changed, 140 insertions(+), 37 deletions(-) diff --git a/apps/sim/app/api/desktop/computer/availability/route.ts b/apps/sim/app/api/desktop/computer/availability/route.ts index a6a2310afff..884fe581ade 100644 --- a/apps/sim/app/api/desktop/computer/availability/route.ts +++ b/apps/sim/app/api/desktop/computer/availability/route.ts @@ -12,10 +12,10 @@ export const GET = defineInternalJsonRoute({ auth: internalSessionAuth, operation: readComputerUseAvailability.operation, rateLimit: internalRateLimits.none({ - reason: 'Read-only rollout switch used on each desktop chat admission.', + reason: 'Read-only rollout check used on each desktop chat admission.', }), errorPolicy: internalOrchestrationErrorPolicy, - mapInput: () => undefined, + mapInput: ({ query }) => query, useCase: readComputerUseAvailability, present: (result) => result, }) diff --git a/apps/sim/app/o/[organizationId]/layout.tsx b/apps/sim/app/o/[organizationId]/layout.tsx index 9b11599685c..ef1ab7fa5a9 100644 --- a/apps/sim/app/o/[organizationId]/layout.tsx +++ b/apps/sim/app/o/[organizationId]/layout.tsx @@ -88,7 +88,7 @@ export default async function OrganizationLayout({ logger.warn('Could not resolve benchmark navigation access') return false }), - isComputerUseAvailable(), + isComputerUseAvailable(organizationId), isWorkflowTestsEnabled(organizationId), isChangelogEnabled(organizationId), isProjectsEnabled(), diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index e8c28ad699d..8cb1740eaae 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -3929,7 +3929,16 @@ export function useChat( const desktopChatCapabilities = options?.requestMode === 'assistant' ? {} - : await getDesktopChatCapabilities(desktopScopeIdRef.current, computerUseEnabled) + : await getDesktopChatCapabilities( + desktopScopeIdRef.current, + !computerUseEnabled + ? null + : organizationId + ? { organizationId } + : workspaceId + ? { workspaceId } + : null + ) admissionUnknown = true /** A reload from here on may find the server holding this id. */ diff --git a/apps/sim/app/workspace/[workspaceId]/layout.tsx b/apps/sim/app/workspace/[workspaceId]/layout.tsx index f98e57ff015..df091963571 100644 --- a/apps/sim/app/workspace/[workspaceId]/layout.tsx +++ b/apps/sim/app/workspace/[workspaceId]/layout.tsx @@ -150,7 +150,7 @@ async function WorkspaceContent({ isWorkflowTestsEnabled(hostContext.hostOrganizationId), isChangelogEnabled(hostContext.hostOrganizationId), hasDesktopBackgroundExecutor(session.user.id), - isComputerUseAvailable(), + isComputerUseAvailable(hostContext.hostOrganizationId), isProjectsEnabled(), prefetchWorkspaceAccess(queryClient, workspaceId, principal), prefetchWorkspaceForkAvailability(queryClient, workspaceId, principal, hostContext), diff --git a/apps/sim/lib/api/contracts/computer-use.ts b/apps/sim/lib/api/contracts/computer-use.ts index 70b0c657b12..3c4737e6493 100644 --- a/apps/sim/lib/api/contracts/computer-use.ts +++ b/apps/sim/lib/api/contracts/computer-use.ts @@ -1,14 +1,22 @@ import { ComputerUseSchema } from '@sim/desktop-bridge/computer-use' import { z } from 'zod' import { desktopToolCallIdSchema } from '@/lib/api/contracts/desktop-tool-authorization' -import { defineRouteContract } from '@/lib/api/contracts/types' +import { organizationIdSchema, workspaceIdSchema } from '@/lib/api/contracts/primitives' +import { type ContractQuery, defineRouteContract } from '@/lib/api/contracts/types' +/** The chat owner whose rollout applies: exactly one of a workspace or an organization. */ +const computerUseAvailabilityQuerySchema = z.union([ + z.object({ workspaceId: workspaceIdSchema }).strict(), + z.object({ organizationId: organizationIdSchema }).strict(), +]) const computerUseAvailabilityResponseSchema = z.object({ enabled: z.boolean() }).strict() export const computerUseAvailabilityContract = defineRouteContract({ method: 'GET', path: '/api/desktop/computer/availability', + query: computerUseAvailabilityQuerySchema, response: { mode: 'json', schema: computerUseAvailabilityResponseSchema }, }) +export type ComputerUseOwner = ContractQuery const authorizeComputerUseBodySchema = z.object({ toolCallId: desktopToolCallIdSchema }).strict() const authorizeComputerUseResponseSchema = z diff --git a/apps/sim/lib/computer-use/application/authorize.test.ts b/apps/sim/lib/computer-use/application/authorize.test.ts index 1716040894a..0cbdb8bf756 100644 --- a/apps/sim/lib/computer-use/application/authorize.test.ts +++ b/apps/sim/lib/computer-use/application/authorize.test.ts @@ -86,6 +86,11 @@ describe('native computer authorization', () => { await expect(authorizeComputerUse.execute({ principal, input })).rejects.toThrow('unavailable') expect(mocks.claim).not.toHaveBeenCalled() }) + it("checks the rollout of the workspace's organization", async () => { + mocks.ownedChat.mockResolvedValue({ ...context, workspaceOrganizationId: 'org-2' }) + await authorizeComputerUse.execute({ principal, input }) + expect(mocks.available).toHaveBeenCalledWith('org-2') + }) it('rechecks current workspace membership', async () => { workspaceAuthzMockFns.mockResolveEffectiveWorkspacePermission.mockResolvedValue(null) await expect(authorizeComputerUse.execute({ principal, input })).rejects.toThrow() @@ -126,6 +131,7 @@ describe('native computer authorization', () => { mode: 'agent', }) await authorizeComputerUse.execute({ principal, input }) + expect(mocks.available).toHaveBeenCalledWith('org-1') expect( organizationAuthorizationMockFns.mockAuthorizeOrganizationOperation ).toHaveBeenCalledWith( diff --git a/apps/sim/lib/computer-use/application/authorize.ts b/apps/sim/lib/computer-use/application/authorize.ts index 74706fd6987..5ba96e12911 100644 --- a/apps/sim/lib/computer-use/application/authorize.ts +++ b/apps/sim/lib/computer-use/application/authorize.ts @@ -60,7 +60,7 @@ export const authorizeComputerUse = defineAuthorizedChatUseCase({ }, authorizationOptions: {}, async execute({ principal, context, input }) { - if (!(await isComputerUseAvailable())) + if (!(await isComputerUseAvailable(context.organizationId ?? context.workspaceOrganizationId))) throw new OrchestrationError('forbidden', 'Computer use is unavailable on this deployment') const claimed = await claimComputerUseTool({ toolCallId: input.toolCallId, diff --git a/apps/sim/lib/computer-use/application/availability.ts b/apps/sim/lib/computer-use/application/availability.ts index 4e81a28dde1..36e84c9c3a4 100644 --- a/apps/sim/lib/computer-use/application/availability.ts +++ b/apps/sim/lib/computer-use/application/availability.ts @@ -1,26 +1,77 @@ +import type { Principal } from '@sim/auth/principal' +import type { ComputerUseOwner } from '@/lib/api/contracts/computer-use' import { isComputerUseAvailable } from '@/lib/computer-use/availability.server' +import { defineAuthorizedWorkspaceUseCase, defineWorkspaceOperation } from '@/lib/core/application' +import { defineAuthorizedOrganizationUseCase } from '@/lib/core/application/authorized-organization-use-case' import { assertOperationPrincipal, defineOperation, type OperationUseCase, } from '@/lib/core/application/operation' +import { defineOrganizationOperation } from '@/lib/core/application/organization-operation' +import { resolveActiveWorkspaceApplicationContext } from '@/lib/workspaces/application/context' -/** permission-group-exempt: reports only the deployment-wide rollout switch; actions require chat authorization. */ +/** permission-group-exempt: reports the rollout for a workspace the caller can read; actions require chat authorization. */ +const workspaceAvailabilityOperation = defineWorkspaceOperation({ + id: 'desktop.computer.availability', + minimumRole: 'read', + workspaceApiKey: 'deny', + capability: 'none', + principalKinds: ['session'], +}) + +/** permission-group-exempt: reports the rollout for an organization the caller belongs to; actions require chat authorization. */ +const organizationAvailabilityOperation = defineOrganizationOperation({ + id: 'desktop.computer.availability', + minimumRole: 'member', + capability: 'none', + principalKinds: ['session'], +}) + +const workspaceAvailability = defineAuthorizedWorkspaceUseCase({ + operation: workspaceAvailabilityOperation, + resolveContext: ({ input }: { input: { workspaceId: string } }) => + resolveActiveWorkspaceApplicationContext(input.workspaceId), + authorizationOptions: {}, + execute: ({ context }) => isComputerUseAvailable(context.workspaceOrganizationId), +}) + +const organizationAvailability = defineAuthorizedOrganizationUseCase({ + operation: organizationAvailabilityOperation, + execute: ({ input }: { input: { organizationId: string } }) => + isComputerUseAvailable(input.organizationId), +}) + +/** + * Whether computer use is rolled out for a chat's owner: an organization chat follows its + * organization, a workspace chat its workspace's organization, and a personal workspace the + * global switch. + */ +export function isComputerUseAvailableFor( + principal: Principal, + owner: ComputerUseOwner +): Promise { + return 'organizationId' in owner + ? organizationAvailability.execute({ principal, input: owner }) + : workspaceAvailability.execute({ principal, input: owner }) +} + +/** permission-group-exempt: dispatches to the owner's authorized rollout read above. */ const availabilityOperation = defineOperation({ id: 'desktop.computer.availability', capability: 'none', principalKinds: ['session'], }) -/** The authenticated settings and capability read; no device or app data leaves the desktop. */ +/** The desktop's rollout read before it touches the native helper; no device or app data leaves the desktop. */ export const readComputerUseAvailability: OperationUseCase< typeof availabilityOperation, - undefined, + ComputerUseOwner, { enabled: boolean } > = { operation: availabilityOperation, - async execute({ principal }) { + async execute({ principal, input }) { assertOperationPrincipal(principal, availabilityOperation) - return { enabled: await isComputerUseAvailable() } + return { enabled: await isComputerUseAvailableFor(principal, input) } }, } diff --git a/apps/sim/lib/computer-use/availability.server.ts b/apps/sim/lib/computer-use/availability.server.ts index 4a2d5d10dd3..c42a84fb23d 100644 --- a/apps/sim/lib/computer-use/availability.server.ts +++ b/apps/sim/lib/computer-use/availability.server.ts @@ -1,6 +1,6 @@ import { isFeatureEnabled } from '@/lib/core/config/feature-flags' -/** Re-evaluates the global rollout at every server admission boundary. */ -export async function isComputerUseAvailable(): Promise { - return isFeatureEnabled('mothership-computer-use') +/** Computer use rolls out by organization; personal workspaces follow the global switch. */ +export function isComputerUseAvailable(orgId?: string | null): Promise { + return isFeatureEnabled('mothership-computer-use', orgId ? { orgId } : {}) } diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index 9a58894564e..cedbf9d8c42 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -53,7 +53,8 @@ const FEATURE_FLAGS = { }, 'mothership-computer-use': { description: - 'Enable native macOS computer use in Mothership. Global on/off only; each device must also opt in.', + 'Enable native macOS computer use in Mothership. Supports global and organization rollout; ' + + 'personal workspaces follow the global switch. Each device must also opt in.', fallback: 'MSHIP_COMPUTER_USE', }, 'mothership-search-integration-tools': { diff --git a/apps/sim/lib/desktop/index.test.ts b/apps/sim/lib/desktop/index.test.ts index 3ea510434be..a6ee234302b 100644 --- a/apps/sim/lib/desktop/index.test.ts +++ b/apps/sim/lib/desktop/index.test.ts @@ -62,8 +62,12 @@ describe('desktop surface availability', () => { terminalEnabled: false, }) requestAvailability.mockResolvedValueOnce({ enabled: rollout }) - const result = await getDesktopChatCapabilities('chat-1', true) + const result = await getDesktopChatCapabilities('chat-1', { workspaceId: 'ws-1' }) expect(result.desktopCapabilities?.computerUse ?? false).toBe(expected) + expect(requestAvailability).toHaveBeenCalledWith( + expect.objectContaining({ path: '/api/desktop/computer/availability' }), + expect.objectContaining({ query: { workspaceId: 'ws-1' } }) + ) } ) it('leaves ordinary chat capabilities independent of computer use while rollout is off', async () => { @@ -75,7 +79,7 @@ describe('desktop surface availability', () => { browserAgent: { getKnownSessions: vi.fn(async () => ({ sessions: [] })) }, }) requestAvailability.mockResolvedValue({ enabled: false }) - const result = await getDesktopChatCapabilities('chat-1', false) + const result = await getDesktopChatCapabilities('chat-1', null) expect(requestAvailability).not.toHaveBeenCalled() expect(getStatus).not.toHaveBeenCalled() expect(result.desktopCapabilities).toMatchObject({ @@ -95,7 +99,7 @@ describe('desktop surface availability', () => { terminalEnabled: false, }) requestAvailability.mockReturnValueOnce(rollout.promise) - const result = getDesktopChatCapabilities('chat-1', true) + const result = getDesktopChatCapabilities('chat-1', { workspaceId: 'ws-1' }) await vi.waitFor(() => expect(requestAvailability).toHaveBeenCalledOnce()) expect(getStatus).not.toHaveBeenCalled() rollout.resolve({ enabled: false }) @@ -113,7 +117,8 @@ describe('desktop surface availability', () => { }) requestAvailability.mockRejectedValueOnce(new Error('offline')) expect( - (await getDesktopChatCapabilities('chat-1', true)).desktopCapabilities?.computerUse + (await getDesktopChatCapabilities('chat-1', { workspaceId: 'ws-1' })).desktopCapabilities + ?.computerUse ).toBeUndefined() expect(getStatus).not.toHaveBeenCalled() }) @@ -143,12 +148,12 @@ describe('desktop surface availability', () => { browserEnabled: false, terminalEnabled: false, }) - expect(await getDesktopChatCapabilities('org-chat', false)).toMatchObject({ + expect(await getDesktopChatCapabilities('org-chat', null)).toMatchObject({ desktopCapabilities: { localFiles: true }, }) installBridge({}) expect( - (await getDesktopChatCapabilities('org-chat', false)).desktopCapabilities?.localFiles + (await getDesktopChatCapabilities('org-chat', null)).desktopCapabilities?.localFiles ).toBeUndefined() }) @@ -160,7 +165,9 @@ describe('desktop surface availability', () => { }) const offerFrom = async (bridge: Record) => { installBridge({ localFiles: vi.fn(), ...bridge }) - const { desktopCapabilities } = await getDesktopChatCapabilities('chat-1', true) + const { desktopCapabilities } = await getDesktopChatCapabilities('chat-1', { + workspaceId: 'ws-1', + }) return { deviceId: desktopCapabilities?.deviceId, executor: desktopCapabilities?.executor } } const unset = { deviceId: undefined, executor: undefined } @@ -208,7 +215,7 @@ describe('desktop surface availability', () => { browserEnabled: false, }) - const capabilities = await getDesktopChatCapabilities('chat-1', true) + const capabilities = await getDesktopChatCapabilities('chat-1', { workspaceId: 'ws-1' }) expect(capabilities.desktopCapabilities?.terminals).toEqual([ { diff --git a/apps/sim/lib/desktop/index.ts b/apps/sim/lib/desktop/index.ts index ab41a042be5..44ce2c97b81 100644 --- a/apps/sim/lib/desktop/index.ts +++ b/apps/sim/lib/desktop/index.ts @@ -25,7 +25,10 @@ import type { BrowserKnownSession } from '@sim/browser-protocol' import type { DesktopPreferences, SimDesktopApi } from '@sim/desktop-bridge' import { truncate } from '@sim/utils/string' import { requestJson } from '@/lib/api/client/request' -import { computerUseAvailabilityContract } from '@/lib/api/contracts/computer-use' +import { + type ComputerUseOwner, + computerUseAvailabilityContract, +} from '@/lib/api/contracts/computer-use' import { DESKTOP_TERMINAL_HINT_ID_MAX_LENGTH, DESKTOP_TERMINAL_HINT_TEXT_MAX_LENGTH, @@ -177,7 +180,7 @@ export interface DesktopChatCapabilities { */ export async function getDesktopChatCapabilities( scopeId: string, - computerUseEnabled: boolean + computerUseOwner: ComputerUseOwner | null ): Promise { const bridge = getDesktopBridge() // Never advertise a surface the user switched off, even on the first @@ -188,8 +191,11 @@ export async function getDesktopChatCapabilities( const browser = isBrowserAgentEnabled() const terminal = isTerminalEnabled() const computerUse = - computerUseEnabled && bridge?.computerUse - ? await requestJson(computerUseAvailabilityContract, { signal: AbortSignal.timeout(5000) }) + computerUseOwner && bridge?.computerUse + ? await requestJson(computerUseAvailabilityContract, { + query: computerUseOwner, + signal: AbortSignal.timeout(5000), + }) .then(async (rollout) => { if (!rollout.enabled) return false const status = await bridge.computerUse?.getStatus() diff --git a/apps/sim/lib/mothership/chat/payload.test.ts b/apps/sim/lib/mothership/chat/payload.test.ts index 9042d708079..af821f5746e 100644 --- a/apps/sim/lib/mothership/chat/payload.test.ts +++ b/apps/sim/lib/mothership/chat/payload.test.ts @@ -43,8 +43,8 @@ const { mockSearchIntegrationToolsEnabled: vi.fn(async () => true), })) -vi.mock('@/lib/computer-use/availability.server', () => ({ - isComputerUseAvailable: mockComputerUseAvailable, +vi.mock('@/lib/computer-use/application/availability', () => ({ + isComputerUseAvailableFor: mockComputerUseAvailable, })) vi.mock('@/lib/mothership/feature-flags', () => ({ @@ -811,21 +811,27 @@ describe('Assistant payload', () => { }) describe('desktop request capabilities', () => { - it.each([false, true])( - 'rechecks the server rollout for a forged computer capability (enabled=%s)', - async (enabled) => { + it.each([ + [false, { workspaceId: 'workspace' }], + [true, { workspaceId: 'workspace' }], + [true, { organizationId: 'organization' }], + ])( + "rechecks the chat owner's rollout for a forged computer capability (enabled=%s, %j)", + async (enabled, owner) => { + const principal = { kind: 'session', userId: 'user', sessionId: 'session' } as const mockComputerUseAvailable.mockResolvedValueOnce(enabled) const payload = await buildCopilotRequestPayload({ message: 'Use Notes', - workspaceId: 'workspace', + ...owner, userId: 'user', + principal, userMessageId: 'message', mode: 'agent', model: 'gpt-6-astra', computerUse: true, }) expect(payload.desktop?.computerUse ?? false).toBe(enabled) - expect(mockComputerUseAvailable).toHaveBeenCalledWith() + expect(mockComputerUseAvailable).toHaveBeenCalledWith(principal, owner) } ) diff --git a/apps/sim/lib/mothership/chat/payload.ts b/apps/sim/lib/mothership/chat/payload.ts index 5054add81d7..3db1f6fe0ee 100644 --- a/apps/sim/lib/mothership/chat/payload.ts +++ b/apps/sim/lib/mothership/chat/payload.ts @@ -6,7 +6,7 @@ import { toError } from '@sim/utils/errors' import { LRUCache } from 'lru-cache' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import { isPaid } from '@/lib/billing/plan-helpers' -import { isComputerUseAvailable } from '@/lib/computer-use/availability.server' +import { isComputerUseAvailableFor } from '@/lib/computer-use/application/availability' import type { BlockVisibilityState } from '@/lib/core/config/block-visibility' import { isHosted } from '@/lib/core/config/env-flags' import { isOAuthServiceDeploymentAvailable } from '@/lib/integrations/availability.server' @@ -323,8 +323,17 @@ export async function buildCopilotRequestPayload(params: BuildPayloadParams): Pr params const effectiveMode = mode === 'agent' ? 'build' : mode const isAssistant = effectiveMode === 'assistant' + const computerUseOwner = params.organizationId + ? { organizationId: params.organizationId } + : params.workspaceId + ? { workspaceId: params.workspaceId } + : null const computerUse = - !isAssistant && params.computerUse === true && (await isComputerUseAvailable()) + !isAssistant && + params.computerUse === true && + params.principal !== undefined && + computerUseOwner !== null && + (await isComputerUseAvailableFor(params.principal, computerUseOwner)) const integrationGateway = !isAssistant || (await isSearchIntegrationToolsEnabled()) // Track uploaded files in the DB and build context tags instead of base64 inlining. From 008f5b426f85615c34f78637070d13a107b6c83a Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 17 Oct 2026 14:15:11 -0700 Subject: [PATCH 2/2] test(computer-use): drop assertions that restate mocked rollout calls --- .../computer-use/application/authorize.test.ts | 6 ------ apps/sim/lib/desktop/index.test.ts | 4 ---- apps/sim/lib/mothership/chat/payload.test.ts | 16 +++++----------- 3 files changed, 5 insertions(+), 21 deletions(-) diff --git a/apps/sim/lib/computer-use/application/authorize.test.ts b/apps/sim/lib/computer-use/application/authorize.test.ts index 0cbdb8bf756..1716040894a 100644 --- a/apps/sim/lib/computer-use/application/authorize.test.ts +++ b/apps/sim/lib/computer-use/application/authorize.test.ts @@ -86,11 +86,6 @@ describe('native computer authorization', () => { await expect(authorizeComputerUse.execute({ principal, input })).rejects.toThrow('unavailable') expect(mocks.claim).not.toHaveBeenCalled() }) - it("checks the rollout of the workspace's organization", async () => { - mocks.ownedChat.mockResolvedValue({ ...context, workspaceOrganizationId: 'org-2' }) - await authorizeComputerUse.execute({ principal, input }) - expect(mocks.available).toHaveBeenCalledWith('org-2') - }) it('rechecks current workspace membership', async () => { workspaceAuthzMockFns.mockResolveEffectiveWorkspacePermission.mockResolvedValue(null) await expect(authorizeComputerUse.execute({ principal, input })).rejects.toThrow() @@ -131,7 +126,6 @@ describe('native computer authorization', () => { mode: 'agent', }) await authorizeComputerUse.execute({ principal, input }) - expect(mocks.available).toHaveBeenCalledWith('org-1') expect( organizationAuthorizationMockFns.mockAuthorizeOrganizationOperation ).toHaveBeenCalledWith( diff --git a/apps/sim/lib/desktop/index.test.ts b/apps/sim/lib/desktop/index.test.ts index a6ee234302b..1a7436c7e83 100644 --- a/apps/sim/lib/desktop/index.test.ts +++ b/apps/sim/lib/desktop/index.test.ts @@ -64,10 +64,6 @@ describe('desktop surface availability', () => { requestAvailability.mockResolvedValueOnce({ enabled: rollout }) const result = await getDesktopChatCapabilities('chat-1', { workspaceId: 'ws-1' }) expect(result.desktopCapabilities?.computerUse ?? false).toBe(expected) - expect(requestAvailability).toHaveBeenCalledWith( - expect.objectContaining({ path: '/api/desktop/computer/availability' }), - expect.objectContaining({ query: { workspaceId: 'ws-1' } }) - ) } ) it('leaves ordinary chat capabilities independent of computer use while rollout is off', async () => { diff --git a/apps/sim/lib/mothership/chat/payload.test.ts b/apps/sim/lib/mothership/chat/payload.test.ts index af821f5746e..1151409d15d 100644 --- a/apps/sim/lib/mothership/chat/payload.test.ts +++ b/apps/sim/lib/mothership/chat/payload.test.ts @@ -811,27 +811,21 @@ describe('Assistant payload', () => { }) describe('desktop request capabilities', () => { - it.each([ - [false, { workspaceId: 'workspace' }], - [true, { workspaceId: 'workspace' }], - [true, { organizationId: 'organization' }], - ])( - "rechecks the chat owner's rollout for a forged computer capability (enabled=%s, %j)", - async (enabled, owner) => { - const principal = { kind: 'session', userId: 'user', sessionId: 'session' } as const + it.each([false, true])( + 'rechecks the server rollout for a forged computer capability (enabled=%s)', + async (enabled) => { mockComputerUseAvailable.mockResolvedValueOnce(enabled) const payload = await buildCopilotRequestPayload({ message: 'Use Notes', - ...owner, + workspaceId: 'workspace', userId: 'user', - principal, + principal: { kind: 'session', userId: 'user', sessionId: 'session' }, userMessageId: 'message', mode: 'agent', model: 'gpt-6-astra', computerUse: true, }) expect(payload.desktop?.computerUse ?? false).toBe(enabled) - expect(mockComputerUseAvailable).toHaveBeenCalledWith(principal, owner) } )