LibreChat/api/models/convoStructure.spec.js
Danny Avila a2fc7d312a
🏗️ refactor: Extract DB layers to data-schemas for shared use (#7650)
* refactor: move model definitions and database-related methods to packages/data-schemas

* ci: update tests due to new DB structure

fix: disable mocking `librechat-data-provider`

feat: Add schema exports to data-schemas package

- Introduced a new schema module that exports various schemas including action, agent, and user schemas.
- Updated index.ts to include the new schema exports for better modularity and organization.

ci: fix appleStrategy tests

fix: Agent.spec.js

ci: refactor handleTools tests to use MongoMemoryServer for in-memory database

fix: getLogStores imports

ci: update banViolation tests to use MongoMemoryServer and improve session mocking

test: refactor samlStrategy tests to improve mock configurations and user handling

ci: fix crypto mock in handleText tests for improved accuracy

ci: refactor spendTokens tests to improve model imports and setup

ci: refactor Message model tests to use MongoMemoryServer and improve database interactions

* refactor: streamline IMessage interface and move feedback properties to types/message.ts

* refactor: use exported initializeRoles from `data-schemas`, remove api workspace version (this serves as an example of future migrations that still need to happen)

* refactor: update model imports to use destructuring from `~/db/models` for consistency and clarity

* refactor: remove unused mongoose imports from model files for cleaner code

* refactor: remove unused mongoose imports from Share, Prompt, and Transaction model files for cleaner code

* refactor: remove unused import in Transaction model for cleaner code

* ci: update deploy workflow to reference new Docker Dev Branch Images Build and add new workflow for building Docker images on dev branch

* chore: cleanup imports
2025-05-30 22:18:13 -04:00

313 lines
9.8 KiB
JavaScript

const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const { getMessages, bulkSaveMessages } = require('./Message');
const { Message } = require('~/db/models');
// 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
});
test('Random order dates between parent and children messages', async () => {
const userId = 'testUser';
const conversationId = 'testConversation';
// Create messages with deliberately out-of-order timestamps but sequential creation
const messages = [
{
messageId: 'parent',
parentMessageId: null,
text: 'Parent Message',
createdAt: new Date('2023-01-01T00:00:00Z'), // Make parent earliest
},
{
messageId: 'child1',
parentMessageId: 'parent',
text: 'Child Message 1',
createdAt: new Date('2023-01-01T00:01:00Z'),
},
{
messageId: 'child2',
parentMessageId: 'parent',
text: 'Child Message 2',
createdAt: new Date('2023-01-01T00:02:00Z'),
},
{
messageId: 'grandchild1',
parentMessageId: 'child1',
text: 'Grandchild Message 1',
createdAt: new Date('2023-01-01T00:03: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 set to true
await bulkSaveMessages(messages, true);
// Retrieve messages
const retrievedMessages = await getMessages({ conversationId, user: userId });
// Debug log to see what's being returned
console.log(
'Retrieved Messages:',
retrievedMessages.map((msg) => ({
messageId: msg.messageId,
parentMessageId: msg.parentMessageId,
createdAt: msg.createdAt,
})),
);
// Build tree
const tree = buildTree({ messages: retrievedMessages });
// Debug log to see the tree structure
console.log(
'Tree structure:',
tree.map((root) => ({
messageId: root.messageId,
children: root.children.map((child) => ({
messageId: child.messageId,
children: child.children.map((grandchild) => ({
messageId: grandchild.messageId,
})),
})),
})),
);
// Verify the structure before making assertions
expect(retrievedMessages.length).toBe(4); // Should have all 4 messages
// Check if messages are properly linked
const parentMsg = retrievedMessages.find((msg) => msg.messageId === 'parent');
expect(parentMsg.parentMessageId).toBeNull(); // Parent should have null parentMessageId
const childMsg1 = retrievedMessages.find((msg) => msg.messageId === 'child1');
expect(childMsg1.parentMessageId).toBe('parent');
// Then check tree structure
expect(tree.length).toBe(1); // Should have only one root message
expect(tree[0].messageId).toBe('parent');
expect(tree[0].children.length).toBe(2); // Should have two children
});
});