Skip to content

Commit b34c8cf

Browse files
committed
test(desktop): observe what the chat view starts and announces instead of asserting mock calls
The background-chat tests now record the calls a view starts and the notifications it asks the desktop app to show, and read whether the desktop activity query went stale from a real query client, instead of asserting that mocks were called.
1 parent c278450 commit b34c8cf

3 files changed

Lines changed: 90 additions & 75 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.test.ts‎

Lines changed: 48 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -108,74 +108,71 @@ describe('tool events (dispatch → model + side effects)', () => {
108108
)
109109
})
110110

111+
/** Starters that record which calls this view started, by tool call id. */
112+
function recordingStarters() {
113+
const started: string[] = []
114+
const record = (toolCallId: string) => {
115+
started.push(toolCallId)
116+
}
117+
return {
118+
started,
119+
starters: {
120+
startClientBrowserTool: record,
121+
startClientTerminalTool: record,
122+
startClientLocalFilesystemTool: record,
123+
startClientWorkflowTool: record,
124+
},
125+
}
126+
}
127+
128+
function dispatchCall(
129+
ctx: StreamLoopContext,
130+
toolCallId: string,
131+
toolName: string,
132+
args: Record<string, unknown>
133+
) {
134+
dispatchStreamEvent(
135+
ctx,
136+
toolEnv({
137+
phase: 'call',
138+
executor: 'client',
139+
mode: 'async',
140+
toolCallId,
141+
toolName,
142+
arguments: args,
143+
status: 'executing',
144+
})
145+
)
146+
}
147+
111148
it("only shows desktop calls when the desktop's background executor runs the turn", () => {
112-
const startClientBrowserTool = vi.fn()
113-
const startClientTerminalTool = vi.fn()
114-
const startClientLocalFilesystemTool = vi.fn()
115-
const startClientWorkflowTool = vi.fn()
149+
const { started, starters } = recordingStarters()
116150
const ctx = createStreamLoopContext(
117151
makeStreamLoopDeps({
118-
startClientBrowserTool,
119-
startClientTerminalTool,
120-
startClientLocalFilesystemTool,
121-
startClientWorkflowTool,
152+
...starters,
122153
chatIdRef: ref('chat-1'),
123154
options: { desktopToolsOnDevice: true },
124155
})
125156
)
126-
const call = (toolCallId: string, toolName: string, args: Record<string, unknown>) =>
127-
dispatchStreamEvent(
128-
ctx,
129-
toolEnv({
130-
phase: 'call',
131-
executor: 'client',
132-
mode: 'async',
133-
toolCallId,
134-
toolName,
135-
arguments: args,
136-
status: 'executing',
137-
})
138-
)
139157

140-
call('click-1', 'browser_click', { ref: 'e1' })
141-
call('run-1', 'terminal', { operation: 'run', args: { command: 'ls' } })
142-
call('read-1', 'read_local_file', { path: '~/notes.txt' })
143-
call('workflow-1', 'run_workflow', { workflowId: 'wf-1' })
158+
dispatchCall(ctx, 'click-1', 'browser_click', { ref: 'e1' })
159+
dispatchCall(ctx, 'run-1', 'terminal', { operation: 'run', args: { command: 'ls' } })
160+
dispatchCall(ctx, 'read-1', 'read_local_file', { path: '~/notes.txt' })
161+
dispatchCall(ctx, 'workflow-1', 'run_workflow', { workflowId: 'wf-1' })
144162

145163
expect(toolNode(ctx, 'click-1').status).toBe('running')
146-
expect(startClientBrowserTool).not.toHaveBeenCalled()
147-
expect(startClientTerminalTool).not.toHaveBeenCalled()
148-
expect(startClientLocalFilesystemTool).not.toHaveBeenCalled()
149-
expect(startClientWorkflowTool).toHaveBeenCalledWith('workflow-1', 'run_workflow', {
150-
workflowId: 'wf-1',
151-
})
164+
expect(started).toEqual(['workflow-1'])
152165
})
153166

154167
it('runs a desktop call in the view when no desktop runs the chat in the background', () => {
155-
const startClientBrowserTool = vi.fn()
168+
const { started, starters } = recordingStarters()
156169
const ctx = createStreamLoopContext(
157-
makeStreamLoopDeps({ startClientBrowserTool, chatIdRef: ref('chat-1') })
170+
makeStreamLoopDeps({ ...starters, chatIdRef: ref('chat-1') })
158171
)
159172

160-
dispatchStreamEvent(
161-
ctx,
162-
toolEnv({
163-
phase: 'call',
164-
executor: 'client',
165-
mode: 'async',
166-
toolCallId: 'click-2',
167-
toolName: 'browser_click',
168-
arguments: { ref: 'e1' },
169-
status: 'executing',
170-
})
171-
)
173+
dispatchCall(ctx, 'click-2', 'browser_click', { ref: 'e1' })
172174

173-
expect(startClientBrowserTool).toHaveBeenCalledWith(
174-
'click-2',
175-
'browser_click',
176-
{ ref: 'e1' },
177-
expect.anything()
178-
)
175+
expect(started).toEqual(['click-2'])
179176
})
180177

181178
it('never starts a skipped terminal command', () => {

‎apps/sim/hooks/use-mothership-chat-events.test.ts‎

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { DesktopNotificationPayload } from '@sim/desktop-bridge'
12
import { sleep } from '@sim/utils/helpers'
23
import { QueryClient, QueryObserver } from '@tanstack/react-query'
34
import { beforeEach, describe, expect, it, vi } from 'vitest'
@@ -298,65 +299,79 @@ describe('resyncMothershipChatCaches', () => {
298299
})
299300

300301
describe('reflectBackgroundChatStatus', () => {
301-
const notify = vi.fn(async () => true)
302-
const queryClient = {
303-
getQueryData: vi.fn(),
304-
invalidateQueries: vi.fn().mockResolvedValue(undefined),
305-
} satisfies Pick<QueryClient, 'getQueryData' | 'invalidateQueries'>
302+
/** What the desktop app was asked to show. */
303+
let shown: DesktopNotificationPayload[] = []
304+
let queryClient: QueryClient
306305

307306
function showing(pathname: string, desktop = true) {
307+
shown = []
308308
vi.stubGlobal('window', {
309309
location: { pathname },
310-
...(desktop ? { simDesktop: { settings: { notify } } } : {}),
310+
...(desktop
311+
? {
312+
simDesktop: {
313+
settings: {
314+
notify: async (payload: DesktopNotificationPayload) => {
315+
shown.push(payload)
316+
return true
317+
},
318+
},
319+
},
320+
}
321+
: {}),
311322
})
312-
queryClient.getQueryData.mockReturnValue([{ id: 'chat-b', name: 'Fix CI' }])
323+
queryClient = new QueryClient()
324+
queryClient.setQueryData(mothershipChatKeys.ownerList('ws-1'), [
325+
{ id: 'chat-b', name: 'Fix CI' },
326+
])
327+
queryClient.setQueryData(desktopActivityKeys.list('ws-1'), [])
313328
}
314329

330+
const activityStale = () =>
331+
queryClient.getQueryState(desktopActivityKeys.list('ws-1'))?.isInvalidated ?? false
332+
315333
const completed = JSON.stringify({ chatId: 'chat-b', type: 'completed', streamId: 's-1' })
316334

317335
it('announces a chat that finished in the background, and opens it from the notification', () => {
318336
showing('/workspace/ws-1/chat/chat-c')
319337

320338
reflectBackgroundChatStatus(queryClient, 'ws-1', completed, true)
321339

322-
expect(notify).toHaveBeenCalledWith({
323-
title: 'Fix CI',
324-
body: 'Sim finished responding.',
325-
route: '/workspace/ws-1/chat/chat-b',
326-
})
340+
expect(shown).toEqual([
341+
{ title: 'Fix CI', body: 'Sim finished responding.', route: '/workspace/ws-1/chat/chat-b' },
342+
])
327343
})
328344

329345
it('announces nothing while the background executor is off', () => {
330346
showing('/workspace/ws-1/chat/chat-c')
331347

332348
reflectBackgroundChatStatus(queryClient, 'ws-1', completed, false)
333349

334-
expect(notify).not.toHaveBeenCalled()
350+
expect(shown).toEqual([])
335351
})
336352

337353
it('leaves the chat on screen to announce itself', () => {
338354
showing('/workspace/ws-1/chat/chat-b')
339355

340356
reflectBackgroundChatStatus(queryClient, 'ws-1', completed, true)
341357

342-
expect(notify).not.toHaveBeenCalled()
358+
expect(shown).toEqual([])
343359
})
344360

345-
it('stays silent outside the desktop app and for a turn that only started', () => {
346-
showing('/workspace/ws-1/chat/chat-c', false)
347-
reflectBackgroundChatStatus(queryClient, 'ws-1', completed, true)
361+
it('stays silent for a turn that only started', () => {
348362
showing('/workspace/ws-1/chat/chat-c')
363+
349364
reflectBackgroundChatStatus(
350365
queryClient,
351366
'ws-1',
352367
JSON.stringify({ chatId: 'chat-b', type: 'started', streamId: 's-2' }),
353368
true
354369
)
355370

356-
expect(notify).not.toHaveBeenCalled()
371+
expect(shown).toEqual([])
357372
})
358373

359-
it('refreshes which chats run on a desktop whenever a turn starts or ends', () => {
374+
it('refreshes which chats run on a desktop when a turn starts', () => {
360375
showing('/workspace/ws-1/home', false)
361376

362377
reflectBackgroundChatStatus(
@@ -365,16 +380,20 @@ describe('reflectBackgroundChatStatus', () => {
365380
JSON.stringify({ chatId: 'chat-b', type: 'started', streamId: 's-3' }),
366381
true
367382
)
383+
384+
expect(activityStale()).toBe(true)
385+
})
386+
387+
it('leaves the desktop activity alone for a rename', () => {
388+
showing('/workspace/ws-1/home', false)
389+
368390
reflectBackgroundChatStatus(
369391
queryClient,
370392
'ws-1',
371393
JSON.stringify({ chatId: 'chat-b', type: 'renamed' }),
372394
true
373395
)
374396

375-
expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(1)
376-
expect(queryClient.invalidateQueries).toHaveBeenCalledWith({
377-
queryKey: desktopActivityKeys.lists(),
378-
})
397+
expect(activityStale()).toBe(false)
379398
})
380399
})

‎apps/sim/lib/mothership/chat/application/admit-turn.test.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,6 @@ describe('organization turn admission through current private-chat authorization
137137
input: { ...input(), desktopDeviceId: '33333333-3333-4333-8333-333333333333' },
138138
})
139139

140-
expect(hoisted.resolveDesktop).not.toHaveBeenCalled()
141140
expect(dbChainMockFns.values).toHaveBeenCalledWith(
142141
expect.objectContaining({ organizationId: 'org-1', desktopDeviceId: null })
143142
)

0 commit comments

Comments
 (0)