diff --git a/web/app/components/base/chat/chat-with-history/hooks.spec.tsx b/web/app/components/base/chat/chat-with-history/hooks.spec.tsx
index eaa743bfea..32ef133453 100644
--- a/web/app/components/base/chat/chat-with-history/hooks.spec.tsx
+++ b/web/app/components/base/chat/chat-with-history/hooks.spec.tsx
@@ -3,6 +3,7 @@ import type { ChatConfig } from '../types'
import type { AppConversationData, AppData, AppMeta, ConversationItem } from '@/models/share'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, renderHook, waitFor } from '@testing-library/react'
+import { ToastProvider } from '@/app/components/base/toast'
import {
fetchChatList,
fetchConversations,
@@ -12,14 +13,6 @@ import { shareQueryKeys } from '@/service/use-share'
import { CONVERSATION_ID_INFO } from '../constants'
import { useChatWithHistory } from './hooks'
-const notifyMock = vi.fn()
-
-vi.mock('@/app/components/base/toast', () => ({
- useToastContext: () => ({
- notify: notifyMock,
- }),
-}))
-
vi.mock('@/hooks/use-app-favicon', () => ({
useAppFavicon: vi.fn(),
}))
@@ -85,7 +78,9 @@ const createQueryClient = () => new QueryClient({
const createWrapper = (queryClient: QueryClient) => {
return ({ children }: { children: ReactNode }) => (
- {children}
+
+ {children}
+
)
}
@@ -127,6 +122,7 @@ const setConversationIdInfo = (appId: string, conversationId: string) => {
describe('useChatWithHistory', () => {
beforeEach(() => {
vi.clearAllMocks()
+ localStorage.removeItem(CONVERSATION_ID_INFO)
mockStoreState.appInfo = {
app_id: 'app-1',
custom_config: null,
@@ -142,6 +138,10 @@ describe('useChatWithHistory', () => {
setConversationIdInfo('app-1', 'conversation-1')
})
+ afterEach(() => {
+ localStorage.removeItem(CONVERSATION_ID_INFO)
+ })
+
// Scenario: share query results populate conversation lists and trigger chat list fetch.
describe('Share queries', () => {
it('should load pinned, unpinned, and chat list data from share queries', async () => {
@@ -208,4 +208,63 @@ describe('useChatWithHistory', () => {
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: shareQueryKeys.conversations })
})
})
+
+ // Scenario: chat list queries stop when reload key is cleared.
+ describe('Chat list gating', () => {
+ it('should not refetch chat list when newConversationId matches current conversation', async () => {
+ // Arrange
+ const listData = createConversationData({
+ data: [createConversationItem({ id: 'conversation-1', name: 'First' })],
+ })
+ mockFetchConversations.mockResolvedValue(listData)
+ mockFetchChatList.mockResolvedValue({ data: [] })
+ mockGenerationConversationName.mockResolvedValue(createConversationItem({ id: 'conversation-1' }))
+
+ const { result } = renderWithClient(() => useChatWithHistory())
+
+ await waitFor(() => {
+ expect(mockFetchChatList).toHaveBeenCalledTimes(1)
+ })
+
+ // Act
+ act(() => {
+ result.current.handleNewConversationCompleted('conversation-1')
+ })
+
+ // Assert
+ await waitFor(() => {
+ expect(result.current.chatShouldReloadKey).toBe('')
+ })
+ expect(mockFetchChatList).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ // Scenario: conversation id updates persist to localStorage.
+ describe('Conversation id persistence', () => {
+ it('should store new conversation id in localStorage after completion', async () => {
+ // Arrange
+ const listData = createConversationData({
+ data: [createConversationItem({ id: 'conversation-1', name: 'First' })],
+ })
+ mockFetchConversations.mockResolvedValue(listData)
+ mockFetchChatList.mockResolvedValue({ data: [] })
+ mockGenerationConversationName.mockResolvedValue(createConversationItem({ id: 'conversation-new' }))
+
+ const { result } = renderWithClient(() => useChatWithHistory())
+
+ // Act
+ act(() => {
+ result.current.handleNewConversationCompleted('conversation-new')
+ })
+
+ // Assert
+ await waitFor(() => {
+ const storedValue = localStorage.getItem(CONVERSATION_ID_INFO)
+ const parsed = storedValue ? JSON.parse(storedValue) : {}
+ const storedUserId = parsed['app-1']?.['user-1']
+ const storedDefaultId = parsed['app-1']?.DEFAULT
+ expect([storedUserId, storedDefaultId]).toContain('conversation-new')
+ })
+ })
+ })
})
diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.spec.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.spec.tsx
index e4001c2464..ca6a90c4d8 100644
--- a/web/app/components/base/chat/embedded-chatbot/hooks.spec.tsx
+++ b/web/app/components/base/chat/embedded-chatbot/hooks.spec.tsx
@@ -3,22 +3,16 @@ import type { ChatConfig } from '../types'
import type { AppConversationData, AppData, AppMeta, ConversationItem } from '@/models/share'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, renderHook, waitFor } from '@testing-library/react'
+import { ToastProvider } from '@/app/components/base/toast'
import {
fetchChatList,
fetchConversations,
generationConversationName,
} from '@/service/share'
import { shareQueryKeys } from '@/service/use-share'
+import { CONVERSATION_ID_INFO } from '../constants'
import { useEmbeddedChatbot } from './hooks'
-const notifyMock = vi.fn()
-
-vi.mock('@/app/components/base/toast', () => ({
- useToastContext: () => ({
- notify: notifyMock,
- }),
-}))
-
vi.mock('@/i18n-config/i18next-config', () => ({
changeLanguage: vi.fn().mockResolvedValue(undefined),
}))
@@ -80,7 +74,9 @@ const createQueryClient = () => new QueryClient({
const createWrapper = (queryClient: QueryClient) => {
return ({ children }: { children: ReactNode }) => (
- {children}
+
+ {children}
+
)
}
@@ -112,6 +108,7 @@ const createConversationData = (overrides: Partial = {}): A
describe('useEmbeddedChatbot', () => {
beforeEach(() => {
vi.clearAllMocks()
+ localStorage.removeItem(CONVERSATION_ID_INFO)
mockStoreState.appInfo = {
app_id: 'app-1',
custom_config: null,
@@ -128,6 +125,10 @@ describe('useEmbeddedChatbot', () => {
mockStoreState.embeddedUserId = 'embedded-user-1'
})
+ afterEach(() => {
+ localStorage.removeItem(CONVERSATION_ID_INFO)
+ })
+
// Scenario: share query results populate conversation lists and trigger chat list fetch.
describe('Share queries', () => {
it('should load pinned, unpinned, and chat list data from share queries', async () => {
@@ -194,4 +195,63 @@ describe('useEmbeddedChatbot', () => {
expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: shareQueryKeys.conversations })
})
})
+
+ // Scenario: chat list queries stop when reload key is cleared.
+ describe('Chat list gating', () => {
+ it('should not refetch chat list when newConversationId matches current conversation', async () => {
+ // Arrange
+ const listData = createConversationData({
+ data: [createConversationItem({ id: 'conversation-1', name: 'First' })],
+ })
+ mockFetchConversations.mockResolvedValue(listData)
+ mockFetchChatList.mockResolvedValue({ data: [] })
+ mockGenerationConversationName.mockResolvedValue(createConversationItem({ id: 'conversation-1' }))
+
+ const { result } = renderWithClient(() => useEmbeddedChatbot())
+
+ await waitFor(() => {
+ expect(mockFetchChatList).toHaveBeenCalledTimes(1)
+ })
+
+ // Act
+ act(() => {
+ result.current.handleNewConversationCompleted('conversation-1')
+ })
+
+ // Assert
+ await waitFor(() => {
+ expect(result.current.chatShouldReloadKey).toBe('')
+ })
+ expect(mockFetchChatList).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ // Scenario: conversation id updates persist to localStorage.
+ describe('Conversation id persistence', () => {
+ it('should store new conversation id in localStorage after completion', async () => {
+ // Arrange
+ const listData = createConversationData({
+ data: [createConversationItem({ id: 'conversation-1', name: 'First' })],
+ })
+ mockFetchConversations.mockResolvedValue(listData)
+ mockFetchChatList.mockResolvedValue({ data: [] })
+ mockGenerationConversationName.mockResolvedValue(createConversationItem({ id: 'conversation-new' }))
+
+ const { result } = renderWithClient(() => useEmbeddedChatbot())
+
+ // Act
+ act(() => {
+ result.current.handleNewConversationCompleted('conversation-new')
+ })
+
+ // Assert
+ await waitFor(() => {
+ const storedValue = localStorage.getItem(CONVERSATION_ID_INFO)
+ const parsed = storedValue ? JSON.parse(storedValue) : {}
+ const storedUserId = parsed['app-1']?.['embedded-user-1']
+ const storedDefaultId = parsed['app-1']?.DEFAULT
+ expect([storedUserId, storedDefaultId]).toContain('conversation-new')
+ })
+ })
+ })
})
diff --git a/web/service/use-share.spec.tsx b/web/service/use-share.spec.tsx
index 4f04392315..d0202ed140 100644
--- a/web/service/use-share.spec.tsx
+++ b/web/service/use-share.spec.tsx
@@ -105,12 +105,13 @@ describe('useShareConversations', () => {
}
// Act
- renderShareHook(() => useShareConversations(params))
+ const { result } = renderShareHook(() => useShareConversations(params))
// Assert
await waitFor(() => {
- expect(mockFetchConversations).not.toHaveBeenCalled()
+ expect(result.current.fetchStatus).toBe('idle')
})
+ expect(mockFetchConversations).not.toHaveBeenCalled()
})
})
@@ -151,12 +152,13 @@ describe('useShareChatList', () => {
}
// Act
- renderShareHook(() => useShareChatList(params))
+ const { result } = renderShareHook(() => useShareChatList(params))
// Assert
await waitFor(() => {
- expect(mockFetchChatList).not.toHaveBeenCalled()
+ expect(result.current.fetchStatus).toBe('idle')
})
+ expect(mockFetchChatList).not.toHaveBeenCalled()
})
})
@@ -197,12 +199,13 @@ describe('useShareConversationName', () => {
}
// Act
- renderShareHook(() => useShareConversationName(params, { enabled: false }))
+ const { result } = renderShareHook(() => useShareConversationName(params, { enabled: false }))
// Assert
await waitFor(() => {
- expect(mockGenerationConversationName).not.toHaveBeenCalled()
+ expect(result.current.fetchStatus).toBe('idle')
})
+ expect(mockGenerationConversationName).not.toHaveBeenCalled()
})
})