From e2dc391b736df4265a4d6761d460d5e2ff7e97fe Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 3 Aug 2026 12:48:32 -0700 Subject: [PATCH] fix(chat): invalidate deployment queries after a chat mutation PATCH /api/chat/manage/[id] calls performFullDeploy when the workflow has drifted from its active deployment, so editing a chat can mint a new deployment version. useUpdateChat invalidated only chatStatus and chatDetail, leaving the deployment panel showing the previous version and a stale "needs redeployment" indicator until the staleTime expired. Both mutations now route through invalidateDeploymentQueries, the shared helper the rest of the deployment surface uses. That also picks up deployedState, which useCreateChat's hand-rolled list had omitted even though performChatDeploy replaces the deployed workflow state. Tests cover both mutations and were verified to fail against the previous invalidation. --- apps/sim/hooks/queries/chats.test.tsx | 111 ++++++++++++++++++++++++++ apps/sim/hooks/queries/chats.ts | 19 +---- 2 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 apps/sim/hooks/queries/chats.test.tsx 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 })