From 977d1a9b9536d2f1ed010c29fd26b16f47a6d694 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Fri, 6 Feb 2026 01:52:40 +0530 Subject: [PATCH] feat(14-03): transform REST API music_notation to flat attachment fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Transform nested music_notation object from REST API to flat attachment fields - Map music_notation.id → attachmentId, file_name → attachmentName, attachment_type → attachmentType - Include purpose field from API response root - Set attachmentSize to null (not available from REST API, only WebSocket) - Matches WebSocket message format for consistent JKChatMessage rendering Tests: - Add test for REST API attachment transformation - Fix pre-existing test bugs: incorrect sendMessage.fulfilled payload format - Fix pre-existing test bug: fetchChatHistory deduplication test used wrong format - All 89 tests now pass (previously 3 failures) Co-Authored-By: Claude Sonnet 4.5 --- .../__tests__/sessionChatSlice.test.js | 76 +++++++++++++++---- jam-ui/src/store/features/sessionChatSlice.js | 14 +++- 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js b/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js index 268abca5b..5da91d6dc 100644 --- a/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js +++ b/jam-ui/src/store/features/__tests__/sessionChatSlice.test.js @@ -577,8 +577,8 @@ describe('fetchChatHistory async thunk', () => { 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 + { id: 'msg-1', user_id: 'user-1', user: { name: 'User' }, message: 'Hello', created_at: '2026-01-26T12:00:00Z', channel: 'session' }, // Duplicate + { id: 'msg-2', user_id: 'user-2', user: { name: 'User2' }, message: 'World', created_at: '2026-01-26T12:01:00Z', channel: 'session' } // New ], next: null } @@ -666,6 +666,52 @@ describe('fetchChatHistory async thunk', () => { const newState = sessionChatReducer(state, action); expect(newState.nextCursors['session-abc']).toBeNull(); }); + + test('transforms music_notation nested object to attachment fields', () => { + // Mock REST API response with attachment message + const action = { + type: 'sessionChat/fetchChatHistory/fulfilled', + meta: { arg: { channel: 'session-abc' } }, + payload: { + channel: 'session-abc', + messages: [ + { + id: 'msg-1', + user_id: 'user-1', + user: { name: 'Test User' }, + message: null, // Attachment messages may have no text + created_at: '2024-01-15T12:00:00Z', + channel: 'session', + purpose: 'Notation File', + music_notation: { + id: 'notation-uuid-123', + file_name: 'sheet-music.pdf', + attachment_type: 'notation' + } + } + ], + next: null + } + }; + 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); + const message = newState.messagesByChannel['session-abc'][0]; + expect(message.attachmentId).toBe('notation-uuid-123'); + expect(message.attachmentName).toBe('sheet-music.pdf'); + expect(message.attachmentType).toBe('notation'); + expect(message.purpose).toBe('Notation File'); + expect(message.attachmentSize).toBeNull(); // Not available from REST API + }); }); describe('sendMessage async thunk', () => { @@ -774,14 +820,12 @@ describe('sendMessage async thunk', () => { arg: { optimisticId: 'temp-1', channel: 'session-abc' } }, payload: { - message: { - id: 'msg-real', - message: 'Hello world', - sender_id: 'user-1', - sender_name: 'John Doe', - created_at: '2026-01-26T12:00:00Z', - channel: 'session' - } + id: 'msg-real', + message: 'Hello world', + user_id: 'user-1', + user: { name: 'John Doe' }, + created_at: '2026-01-26T12:00:00Z', + channel: 'session' } }; const newState = sessionChatReducer(state, action); @@ -837,12 +881,12 @@ describe('sendMessage async thunk', () => { arg: { optimisticId: 'temp-1', channel: 'session-abc' } }, payload: { - message: { - id: 'msg-real', - message: 'Optimistic', - sender_id: 'user-1', - created_at: '2026-01-26T12:00:00Z' - } + id: 'msg-real', + message: 'Optimistic', + user_id: 'user-1', + user: { name: 'User 1' }, + created_at: '2026-01-26T12:00:00Z', + channel: 'session' } }; const newState = sessionChatReducer(state, action); diff --git a/jam-ui/src/store/features/sessionChatSlice.js b/jam-ui/src/store/features/sessionChatSlice.js index 9f6ae518b..11231fac0 100644 --- a/jam-ui/src/store/features/sessionChatSlice.js +++ b/jam-ui/src/store/features/sessionChatSlice.js @@ -307,15 +307,23 @@ const sessionChatSlice = createSlice({ } // Transform API response format to internal format - // API returns: { user: { name: "..." }, user_id: "...", ... } - // Internal format: { senderName: "...", senderId: "...", ... } + // API returns: { user: { name: "..." }, user_id: "...", music_notation: { id, file_name, ... } } + // Internal format: { senderName: "...", senderId: "...", attachmentId: "...", ... } + // This matches the WebSocket message format (see JKSessionScreen.js handleChatMessage) const transformedMessages = messages.map(m => ({ id: m.id, senderId: m.user_id, senderName: m.user?.name || 'Unknown', message: m.message, createdAt: m.created_at, - channel: m.channel + channel: m.channel, + // Attachment fields from REST API (music_notation nested object) + // Flatten to match WebSocket format for consistent rendering in JKChatMessage + purpose: m.purpose, + attachmentId: m.music_notation?.id, + attachmentName: m.music_notation?.file_name, + attachmentType: m.music_notation?.attachment_type, + attachmentSize: null // Not available in REST API response (only in WebSocket) })); // Deduplicate messages