-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🌿 fix: forking a long conversation breaks chat structure (#4778)
* fix: branching and forking sometimes break conversation structure * fix test for forking. * chore: message type issues * test: add conversation structure tests for message handling --------- Co-authored-by: xyqyear <[email protected]>
- Loading branch information
1 parent
7d5be68
commit c87a51e
Showing
8 changed files
with
248 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
const mongoose = require('mongoose'); | ||
const { MongoMemoryServer } = require('mongodb-memory-server'); | ||
const { Message, getMessages, bulkSaveMessages } = require('./Message'); | ||
|
||
// Original version of buildTree function | ||
function buildTree({ messages, fileMap }) { | ||
if (messages === null) { | ||
return null; | ||
} | ||
|
||
const messageMap = {}; | ||
const rootMessages = []; | ||
const childrenCount = {}; | ||
|
||
messages.forEach((message) => { | ||
const parentId = message.parentMessageId ?? ''; | ||
childrenCount[parentId] = (childrenCount[parentId] || 0) + 1; | ||
|
||
const extendedMessage = { | ||
...message, | ||
children: [], | ||
depth: 0, | ||
siblingIndex: childrenCount[parentId] - 1, | ||
}; | ||
|
||
if (message.files && fileMap) { | ||
extendedMessage.files = message.files.map((file) => fileMap[file.file_id ?? ''] ?? file); | ||
} | ||
|
||
messageMap[message.messageId] = extendedMessage; | ||
|
||
const parentMessage = messageMap[parentId]; | ||
if (parentMessage) { | ||
parentMessage.children.push(extendedMessage); | ||
extendedMessage.depth = parentMessage.depth + 1; | ||
} else { | ||
rootMessages.push(extendedMessage); | ||
} | ||
}); | ||
|
||
return rootMessages; | ||
} | ||
|
||
let mongod; | ||
|
||
beforeAll(async () => { | ||
mongod = await MongoMemoryServer.create(); | ||
const uri = mongod.getUri(); | ||
await mongoose.connect(uri); | ||
}); | ||
|
||
afterAll(async () => { | ||
await mongoose.disconnect(); | ||
await mongod.stop(); | ||
}); | ||
|
||
beforeEach(async () => { | ||
await Message.deleteMany({}); | ||
}); | ||
|
||
describe('Conversation Structure Tests', () => { | ||
test('Conversation folding/corrupting with inconsistent timestamps', async () => { | ||
const userId = 'testUser'; | ||
const conversationId = 'testConversation'; | ||
|
||
// Create messages with inconsistent timestamps | ||
const messages = [ | ||
{ | ||
messageId: 'message0', | ||
parentMessageId: null, | ||
text: 'Message 0', | ||
createdAt: new Date('2023-01-01T00:00:00Z'), | ||
}, | ||
{ | ||
messageId: 'message1', | ||
parentMessageId: 'message0', | ||
text: 'Message 1', | ||
createdAt: new Date('2023-01-01T00:02:00Z'), | ||
}, | ||
{ | ||
messageId: 'message2', | ||
parentMessageId: 'message1', | ||
text: 'Message 2', | ||
createdAt: new Date('2023-01-01T00:01:00Z'), | ||
}, // Note: Earlier than its parent | ||
{ | ||
messageId: 'message3', | ||
parentMessageId: 'message1', | ||
text: 'Message 3', | ||
createdAt: new Date('2023-01-01T00:03:00Z'), | ||
}, | ||
{ | ||
messageId: 'message4', | ||
parentMessageId: 'message2', | ||
text: 'Message 4', | ||
createdAt: new Date('2023-01-01T00:04:00Z'), | ||
}, | ||
]; | ||
|
||
// Add common properties to all messages | ||
messages.forEach((msg) => { | ||
msg.conversationId = conversationId; | ||
msg.user = userId; | ||
msg.isCreatedByUser = false; | ||
msg.error = false; | ||
msg.unfinished = false; | ||
}); | ||
|
||
// Save messages with overrideTimestamp omitted (default is false) | ||
await bulkSaveMessages(messages, true); | ||
|
||
// Retrieve messages (this will sort by createdAt) | ||
const retrievedMessages = await getMessages({ conversationId, user: userId }); | ||
|
||
// Build tree | ||
const tree = buildTree({ messages: retrievedMessages }); | ||
|
||
// Check if the tree is incorrect (folded/corrupted) | ||
expect(tree.length).toBeGreaterThan(1); // Should have multiple root messages, indicating corruption | ||
}); | ||
|
||
test('Fix: Conversation structure maintained with more than 16 messages', async () => { | ||
const userId = 'testUser'; | ||
const conversationId = 'testConversation'; | ||
|
||
// Create more than 16 messages | ||
const messages = Array.from({ length: 20 }, (_, i) => ({ | ||
messageId: `message${i}`, | ||
parentMessageId: i === 0 ? null : `message${i - 1}`, | ||
conversationId, | ||
user: userId, | ||
text: `Message ${i}`, | ||
createdAt: new Date(Date.now() + (i % 2 === 0 ? i * 500000 : -i * 500000)), | ||
})); | ||
|
||
// Save messages with new timestamps being generated (message objects ignored) | ||
await bulkSaveMessages(messages); | ||
|
||
// Retrieve messages (this will sort by createdAt, but it shouldn't matter now) | ||
const retrievedMessages = await getMessages({ conversationId, user: userId }); | ||
|
||
// Build tree | ||
const tree = buildTree({ messages: retrievedMessages }); | ||
|
||
// Check if the tree is correct | ||
expect(tree.length).toBe(1); // Should have only one root message | ||
let currentNode = tree[0]; | ||
for (let i = 1; i < 20; i++) { | ||
expect(currentNode.children.length).toBe(1); | ||
currentNode = currentNode.children[0]; | ||
expect(currentNode.text).toBe(`Message ${i}`); | ||
} | ||
expect(currentNode.children.length).toBe(0); // Last message should have no children | ||
}); | ||
|
||
test('Simulate MongoDB ordering issue with more than 16 messages and close timestamps', async () => { | ||
const userId = 'testUser'; | ||
const conversationId = 'testConversation'; | ||
|
||
// Create more than 16 messages with very close timestamps | ||
const messages = Array.from({ length: 20 }, (_, i) => ({ | ||
messageId: `message${i}`, | ||
parentMessageId: i === 0 ? null : `message${i - 1}`, | ||
conversationId, | ||
user: userId, | ||
text: `Message ${i}`, | ||
createdAt: new Date(Date.now() + (i % 2 === 0 ? i * 1 : -i * 1)), | ||
})); | ||
|
||
// Add common properties to all messages | ||
messages.forEach((msg) => { | ||
msg.isCreatedByUser = false; | ||
msg.error = false; | ||
msg.unfinished = false; | ||
}); | ||
|
||
await bulkSaveMessages(messages, true); | ||
const retrievedMessages = await getMessages({ conversationId, user: userId }); | ||
const tree = buildTree({ messages: retrievedMessages }); | ||
expect(tree.length).toBeGreaterThan(1); | ||
}); | ||
|
||
test('Fix: Preserve order with more than 16 messages by maintaining original timestamps', async () => { | ||
const userId = 'testUser'; | ||
const conversationId = 'testConversation'; | ||
|
||
// Create more than 16 messages with distinct timestamps | ||
const messages = Array.from({ length: 20 }, (_, i) => ({ | ||
messageId: `message${i}`, | ||
parentMessageId: i === 0 ? null : `message${i - 1}`, | ||
conversationId, | ||
user: userId, | ||
text: `Message ${i}`, | ||
createdAt: new Date(Date.now() + i * 1000), // Ensure each message has a distinct timestamp | ||
})); | ||
|
||
// Add common properties to all messages | ||
messages.forEach((msg) => { | ||
msg.isCreatedByUser = false; | ||
msg.error = false; | ||
msg.unfinished = false; | ||
}); | ||
|
||
// Save messages with overriding timestamps (preserve original timestamps) | ||
await bulkSaveMessages(messages, true); | ||
|
||
// Retrieve messages (this will sort by createdAt) | ||
const retrievedMessages = await getMessages({ conversationId, user: userId }); | ||
|
||
// Build tree | ||
const tree = buildTree({ messages: retrievedMessages }); | ||
|
||
// Check if the tree is correct | ||
expect(tree.length).toBe(1); // Should have only one root message | ||
let currentNode = tree[0]; | ||
for (let i = 1; i < 20; i++) { | ||
expect(currentNode.children.length).toBe(1); | ||
currentNode = currentNode.children[0]; | ||
expect(currentNode.text).toBe(`Message ${i}`); | ||
} | ||
expect(currentNode.children.length).toBe(0); // Last message should have no children | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters