diff --git a/packages/frontend/@n8n/chat/src/__tests__/plugins/chat.spec.ts b/packages/frontend/@n8n/chat/src/__tests__/plugins/chat.spec.ts index 329c38bd268..81d2731a795 100644 --- a/packages/frontend/@n8n/chat/src/__tests__/plugins/chat.spec.ts +++ b/packages/frontend/@n8n/chat/src/__tests__/plugins/chat.spec.ts @@ -311,7 +311,53 @@ describe('ChatPlugin', () => { expect(sessionId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); expect(chatStore.messages.value).toHaveLength(0); - expect(chatStore.currentSessionId.value).toBeNull(); + expect(chatStore.currentSessionId.value).toBe(sessionId); + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(window.localStorage.setItem).toHaveBeenCalledWith(localStorageSessionIdKey, sessionId); + }); + + it('should preserve manually set sessionId when no messages exist', async () => { + const manualSessionId = '5123f177-df4b-4c0b-b2a1-645432140313'; + (window.localStorage.getItem as ReturnType).mockReturnValueOnce( + manualSessionId, + ); + vi.mocked(api.loadPreviousSession).mockResolvedValueOnce({ data: [] }); + + const sessionId = await chatStore.loadPreviousSession?.(); + + expect(sessionId).toBe(manualSessionId); + expect(chatStore.currentSessionId.value).toBe(manualSessionId); + expect(chatStore.messages.value).toHaveLength(0); + // localStorage.setItem should not be called since sessionId already existed + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(window.localStorage.setItem).not.toHaveBeenCalled(); + }); + + it('should preserve manually set sessionId when messages exist', async () => { + const manualSessionId = '5123f177-df4b-4c0b-b2a1-645432140313'; + (window.localStorage.getItem as ReturnType).mockReturnValueOnce( + manualSessionId, + ); + const mockMessages: LoadPreviousSessionResponse = { + data: [ + { + id: ['user', 'uuid-1'], + kwargs: { content: 'Hello', additional_kwargs: {} }, + lc: 1, + type: 'HumanMessage', + }, + ], + }; + vi.mocked(api.loadPreviousSession).mockResolvedValueOnce(mockMessages); + + const sessionId = await chatStore.loadPreviousSession?.(); + + expect(sessionId).toBe(manualSessionId); + expect(chatStore.currentSessionId.value).toBe(manualSessionId); + expect(chatStore.messages.value).toHaveLength(1); + // localStorage.setItem should not be called since sessionId already existed + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(window.localStorage.setItem).not.toHaveBeenCalled(); }); it('should skip loading if loadPreviousSession is false', async () => { @@ -324,7 +370,9 @@ describe('ChatPlugin', () => { expect(api.loadPreviousSession).not.toHaveBeenCalled(); }); - it('should start a new session', async () => { + it('should start a new session when localStorage is empty', async () => { + (window.localStorage.getItem as ReturnType).mockReturnValueOnce(null); + await chatStore.startNewSession?.(); expect(chatStore.currentSessionId.value).toMatch( @@ -336,6 +384,45 @@ describe('ChatPlugin', () => { chatStore.currentSessionId.value, ); }); + + it('should preserve existing sessionId when starting new session with loadPreviousSession enabled', async () => { + const existingSessionId = '5123f177-df4b-4c0b-b2a1-645432140313'; + mockOptions.loadPreviousSession = true; + chatStore = setupChatStore(mockOptions); + (window.localStorage.getItem as ReturnType).mockReturnValueOnce( + existingSessionId, + ); + + await chatStore.startNewSession?.(); + + expect(chatStore.currentSessionId.value).toBe(existingSessionId); + // localStorage.setItem should not be called since sessionId already exists + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(window.localStorage.setItem).not.toHaveBeenCalled(); + }); + + it('should generate new sessionId when loadPreviousSession is disabled', async () => { + const existingSessionId = '5123f177-df4b-4c0b-b2a1-645432140313'; + mockOptions.loadPreviousSession = false; + chatStore = setupChatStore(mockOptions); + (window.localStorage.getItem as ReturnType).mockReturnValueOnce( + existingSessionId, + ); + + await chatStore.startNewSession?.(); + + // Should generate new UUID, not preserve existing one + expect(chatStore.currentSessionId.value).not.toBe(existingSessionId); + expect(chatStore.currentSessionId.value).toMatch( + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, + ); + // localStorage.setItem should be called with new sessionId + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(window.localStorage.setItem).toHaveBeenCalledWith( + localStorageSessionIdKey, + chatStore.currentSessionId.value, + ); + }); }); describe('initial messages', () => { diff --git a/packages/frontend/@n8n/chat/src/plugins/chat.ts b/packages/frontend/@n8n/chat/src/plugins/chat.ts index f4cea34a7dc..3a4b8bb59f7 100644 --- a/packages/frontend/@n8n/chat/src/plugins/chat.ts +++ b/packages/frontend/@n8n/chat/src/plugins/chat.ts @@ -259,7 +259,14 @@ export const ChatPlugin: Plugin = { return; } - const sessionId = localStorage.getItem(localStorageSessionIdKey) ?? uuidv4(); + const existingSessionId = localStorage.getItem(localStorageSessionIdKey); + const sessionId = existingSessionId ?? uuidv4(); + + // Save to localStorage if it was newly generated + if (!existingSessionId) { + localStorage.setItem(localStorageSessionIdKey, sessionId); + } + const previousMessagesResponse = await api.loadPreviousSession(sessionId, options); messages.value = (previousMessagesResponse?.data || []).map((message, index) => ({ @@ -268,18 +275,26 @@ export const ChatPlugin: Plugin = { sender: message.id.includes('HumanMessage') ? 'user' : 'bot', })); - if (messages.value.length) { - currentSessionId.value = sessionId; - } + // Always set currentSessionId to preserve manually set sessionIds + currentSessionId.value = sessionId; return sessionId; } // eslint-disable-next-line @typescript-eslint/require-await async function startNewSession() { - currentSessionId.value = uuidv4(); + const existingSessionId = localStorage.getItem(localStorageSessionIdKey); - localStorage.setItem(localStorageSessionIdKey, currentSessionId.value); + // Only preserve existing sessionId if loadPreviousSession is enabled + // When loadPreviousSession is false, always generate a new session + if (existingSessionId && options.loadPreviousSession) { + // Preserve existing sessionId (e.g., manually set by user) + currentSessionId.value = existingSessionId; + } else { + // Generate new UUID and save to localStorage + currentSessionId.value = uuidv4(); + localStorage.setItem(localStorageSessionIdKey, currentSessionId.value); + } } const chatStore = {