Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions apps/sim/hooks/queries/chats.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* @vitest-environment jsdom
*/
import { act, type ReactNode } from 'react'
import { sleep } from '@sim/utils/helpers'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createRoot, type Root } from 'react-dom/client'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockRequestJson, mockInvalidateDeploymentQueries } = vi.hoisted(() => ({
mockRequestJson: vi.fn(),
mockInvalidateDeploymentQueries: vi.fn(),
}))

vi.mock('@/lib/api/client/request', () => ({
requestJson: mockRequestJson,
}))

vi.mock('@/hooks/queries/deployments', async (importOriginal) => ({
...(await importOriginal<typeof import('@/hooks/queries/deployments')>()),
invalidateDeploymentQueries: mockInvalidateDeploymentQueries,
}))

import { useCreateChat, useUpdateChat } from '@/hooks/queries/chats'

function renderHookWithClient<T>(useHook: () => T): { getResult: () => T } {
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
const container = document.createElement('div')
const root: Root = createRoot(container)
let result: T | undefined

function Probe() {
result = useHook()
return null
}

act(() => {
root.render(
<QueryClientProvider client={queryClient}>{(<Probe />) as ReactNode}</QueryClientProvider>
)
})

return {
getResult: () => {
if (result === undefined) throw new Error('Hook result is not ready')
return result
},
}
}

async function flush() {
await act(async () => {
for (let i = 0; i < 5; i++) {
await Promise.resolve()
await sleep(1)
}
})
}

const FORM_DATA = {
identifier: 'my-chat',
title: 'My chat',
description: '',
authType: 'public' as const,
password: '',
emails: [],
welcomeMessage: 'hi',
selectedOutputBlocks: [],
includeThinking: false,
includeToolCalls: false,
}

beforeEach(() => {
vi.clearAllMocks()
mockRequestJson.mockResolvedValue({ chatUrl: 'https://sim.ai/chat/my-chat', chatId: 'chat-1' })
mockInvalidateDeploymentQueries.mockResolvedValue(undefined)
})

describe('chat mutations invalidate the deployment boundary', () => {
/**
* PATCH /api/chat/manage/[id] calls performFullDeploy when the workflow has
* drifted, so a chat edit can mint a new deployment version. Invalidating only
* chatStatus/chatDetail left the deployment panel showing the previous version.
*/
it('useUpdateChat invalidates every deployment query for the workflow', async () => {
const { getResult } = renderHookWithClient(() => useUpdateChat())

await act(async () => {
await getResult().mutateAsync({
chatId: 'chat-1',
workflowId: 'wf-1',
formData: FORM_DATA,
})
})
await flush()

expect(mockInvalidateDeploymentQueries).toHaveBeenCalledWith(expect.anything(), 'wf-1')
})

it('useCreateChat invalidates every deployment query for the workflow', async () => {
const { getResult } = renderHookWithClient(() => useCreateChat())

await act(async () => {
await getResult().mutateAsync({ workflowId: 'wf-1', formData: FORM_DATA })
})
await flush()

expect(mockInvalidateDeploymentQueries).toHaveBeenCalledWith(expect.anything(), 'wf-1')
})
})
19 changes: 4 additions & 15 deletions apps/sim/hooks/queries/chats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
verifyChatEmailOtpContract,
} from '@/lib/api/contracts/chats'
import type { OutputConfig } from '@/stores/chat/types'
import { deploymentKeys } from './deployments'
import { deploymentKeys, invalidateDeploymentQueries } from './deployments'

const logger = createLogger('ChatMutations')

Expand Down Expand Up @@ -296,17 +296,8 @@ export function useCreateChat() {
throwUserFriendlyIdentifierError(error)
}
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({
queryKey: deploymentKeys.chatStatus(variables.workflowId),
})
queryClient.invalidateQueries({
queryKey: deploymentKeys.info(variables.workflowId),
})
queryClient.invalidateQueries({
queryKey: deploymentKeys.versions(variables.workflowId),
})
},
onSettled: (_data, _error, variables) =>
invalidateDeploymentQueries(queryClient, variables.workflowId),
onError: (error) => {
logger.error('Failed to create chat', { error })
},
Expand Down Expand Up @@ -341,12 +332,10 @@ export function useUpdateChat() {
}
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({
queryKey: deploymentKeys.chatStatus(variables.workflowId),
})
queryClient.invalidateQueries({
queryKey: deploymentKeys.chatDetail(variables.chatId),
})
return invalidateDeploymentQueries(queryClient, variables.workflowId)
},
onError: (error) => {
logger.error('Failed to update chat', { error })
Expand Down
Loading