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:
parent
d2dc3a8d63
commit
977d1a9b95
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue