diff --git a/apps/sim/hooks/queries/chats.test.tsx b/apps/sim/hooks/queries/chats.test.tsx new file mode 100644 index 00000000000..03cdf31dd18 --- /dev/null +++ b/apps/sim/hooks/queries/chats.test.tsx @@ -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()), + invalidateDeploymentQueries: mockInvalidateDeploymentQueries, +})) + +import { useCreateChat, useUpdateChat } from '@/hooks/queries/chats' + +function renderHookWithClient(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( + {() as ReactNode} + ) + }) + + 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') + }) +}) diff --git a/apps/sim/hooks/queries/chats.ts b/apps/sim/hooks/queries/chats.ts index ec69f65f270..a3bf3e99103 100644 --- a/apps/sim/hooks/queries/chats.ts +++ b/apps/sim/hooks/queries/chats.ts @@ -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') @@ -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 }) }, @@ -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 })