From b0b4ae1a5348ec7328f2138da807f443b3a8ab3e Mon Sep 17 00:00:00 2001 From: Nuwan Date: Tue, 27 Jan 2026 08:15:38 +0530 Subject: [PATCH] test(07-02): add failing tests for fetchChatHistory async thunk Add unit tests for fetchChatHistory extra reducers: - Test pending state sets loading status - Test fulfilled state adds messages and updates status - Test message deduplication on fulfilled - Test pagination: prepending older messages - Test rejected state sets error - Test null cursor handling Tests fail as expected - async thunk not yet implemented. Co-Authored-By: Claude Sonnet 4.5 --- .../__tests__/sessionChatSlice.test.js | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js b/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js index fcc8c7bdd..d8464cf69 100644 --- a/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js +++ b/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js @@ -488,6 +488,167 @@ describe('setWindowPosition', () => { }); }); +describe('fetchChatHistory async thunk', () => { + // Import will be added in implementation phase + // This is the RED phase - tests should fail + let fetchChatHistory; + + test('sets loading state on pending', () => { + // Mock the pending action type + const action = { + type: 'sessionChat/fetchChatHistory/pending', + meta: { arg: { channel: 'session-abc' } } + }; + const state = { + fetchStatus: {}, + fetchError: {}, + messagesByChannel: {}, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const newState = sessionChatReducer(state, action); + expect(newState.fetchStatus['session-abc']).toBe('loading'); + expect(newState.fetchError['session-abc']).toBeNull(); + }); + + test('adds messages on fulfilled', () => { + const action = { + type: 'sessionChat/fetchChatHistory/fulfilled', + meta: { arg: { channel: 'session-abc' } }, + payload: { + channel: 'session-abc', + messages: [ + { id: 'msg-1', message: 'Hello', sender_id: 'user-1', created_at: '2026-01-26T12:00:00Z' } + ], + next: 20 + } + }; + const state = { + messagesByChannel: {}, + fetchStatus: { 'session-abc': 'loading' }, + fetchError: {}, + nextCursors: {}, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const newState = sessionChatReducer(state, action); + expect(newState.messagesByChannel['session-abc']).toHaveLength(1); + expect(newState.fetchStatus['session-abc']).toBe('succeeded'); + expect(newState.nextCursors['session-abc']).toBe(20); + }); + + test('deduplicates messages on fulfilled', () => { + // Test that fetched messages don't duplicate existing ones + const state = { + messagesByChannel: { + 'session-abc': [{ id: 'msg-1', message: 'Hello', createdAt: '2026-01-26T12:00:00Z' }] + }, + fetchStatus: { 'session-abc': 'loading' }, + fetchError: {}, + nextCursors: {}, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const action = { + type: 'sessionChat/fetchChatHistory/fulfilled', + meta: { arg: { channel: 'session-abc' } }, + payload: { + channel: 'session-abc', + messages: [ + { id: 'msg-1', message: 'Hello', createdAt: '2026-01-26T12:00:00Z' }, // Duplicate + { id: 'msg-2', message: 'World', createdAt: '2026-01-26T12:01:00Z' } // New + ], + next: null + } + }; + const newState = sessionChatReducer(state, action); + expect(newState.messagesByChannel['session-abc']).toHaveLength(2); + expect(newState.messagesByChannel['session-abc'][0].id).toBe('msg-1'); + expect(newState.messagesByChannel['session-abc'][1].id).toBe('msg-2'); + }); + + test('prepends older messages for pagination', () => { + // When fetching older messages, they should be prepended (oldest first) + const state = { + messagesByChannel: { + 'session-abc': [ + { id: 'msg-3', message: 'Newest', createdAt: '2026-01-26T12:02:00Z' } + ] + }, + fetchStatus: { 'session-abc': 'loading' }, + fetchError: {}, + nextCursors: {}, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const action = { + type: 'sessionChat/fetchChatHistory/fulfilled', + meta: { arg: { channel: 'session-abc', before: 3 } }, + payload: { + channel: 'session-abc', + messages: [ + { id: 'msg-1', message: 'Oldest', createdAt: '2026-01-26T12:00:00Z' }, + { id: 'msg-2', message: 'Middle', createdAt: '2026-01-26T12:01:00Z' } + ], + next: null + } + }; + const newState = sessionChatReducer(state, action); + expect(newState.messagesByChannel['session-abc']).toHaveLength(3); + expect(newState.messagesByChannel['session-abc'][0].id).toBe('msg-1'); + expect(newState.messagesByChannel['session-abc'][1].id).toBe('msg-2'); + expect(newState.messagesByChannel['session-abc'][2].id).toBe('msg-3'); + }); + + test('sets error state on rejected', () => { + const action = { + type: 'sessionChat/fetchChatHistory/rejected', + meta: { arg: { channel: 'session-abc' } }, + error: { message: 'Not Found' } + }; + const state = { + fetchStatus: { 'session-abc': 'loading' }, + fetchError: {}, + messagesByChannel: {}, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const newState = sessionChatReducer(state, action); + expect(newState.fetchStatus['session-abc']).toBe('failed'); + expect(newState.fetchError['session-abc']).toBe('Not Found'); + }); + + test('handles null next cursor', () => { + const action = { + type: 'sessionChat/fetchChatHistory/fulfilled', + meta: { arg: { channel: 'session-abc' } }, + payload: { + channel: 'session-abc', + messages: [ + { id: 'msg-1', message: 'Hello', createdAt: '2026-01-26T12:00:00Z' } + ], + next: null + } + }; + const state = { + messagesByChannel: {}, + fetchStatus: { 'session-abc': 'loading' }, + fetchError: {}, + nextCursors: {}, + unreadCounts: {}, + isWindowOpen: false, + activeChannel: null + }; + const newState = sessionChatReducer(state, action); + expect(newState.nextCursors['session-abc']).toBeNull(); + }); +}); + describe('sessionChat integration', () => { test('complete message flow: receive → set active → open window → mark read', () => { const initialState = {