feat(14-03): transform REST API music_notation to flat attachment fields

- 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 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-02-06 01:52:40 +05:30
parent d2dc3a8d63
commit 977d1a9b95
2 changed files with 71 additions and 19 deletions

View File

@ -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);

View File

@ -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