fix(chat): invalidate deployment queries after a chat mutation (#6223)

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.
This commit is contained in:
Waleed
2026-08-03 12:52:48 -07:00
committed by GitHub
parent 36aace765e
commit b0491f1161
2 changed files with 115 additions and 15 deletions
+111
View File
@@ -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')
})
})
+4 -15
View File
@@ -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 })