mirror of https://github.com/buster-so/buster.git
309 lines
11 KiB
TypeScript
309 lines
11 KiB
TypeScript
|
import {
|
||
|
getLatestMessageForChat,
|
||
|
updateMessageReasoning,
|
||
|
updateMessageResponseMessages,
|
||
|
updateMessageStreamingFields,
|
||
|
} from '@buster/database/src/helpers/messages';
|
||
|
import { createTestMessageWithContext } from '@buster/test-utils';
|
||
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
||
|
import { cleanupTestEnvironment, setupTestEnvironment } from './helpers';
|
||
|
|
||
|
describe('Message Update Helpers', () => {
|
||
|
beforeEach(async () => {
|
||
|
await setupTestEnvironment();
|
||
|
});
|
||
|
|
||
|
afterEach(async () => {
|
||
|
await cleanupTestEnvironment();
|
||
|
});
|
||
|
|
||
|
describe('updateMessageResponseMessages', () => {
|
||
|
test('successfully updates responseMessages JSONB field', async () => {
|
||
|
const { messageId, chatId } = await createTestMessageWithContext();
|
||
|
|
||
|
const newResponseMessages = {
|
||
|
content: 'Updated response content',
|
||
|
metadata: { tokens: 150, model: 'gpt-4' },
|
||
|
timestamp: new Date().toISOString(),
|
||
|
};
|
||
|
|
||
|
const result = await updateMessageResponseMessages(messageId, newResponseMessages);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
|
||
|
// Verify the update was persisted
|
||
|
const updatedMessage = await getLatestMessageForChat(chatId);
|
||
|
expect(updatedMessage?.responseMessages).toEqual(newResponseMessages);
|
||
|
});
|
||
|
|
||
|
test('handles empty responseMessages object', async () => {
|
||
|
const { messageId, chatId } = await createTestMessageWithContext();
|
||
|
|
||
|
const emptyResponse = {};
|
||
|
|
||
|
const result = await updateMessageResponseMessages(messageId, emptyResponse);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
|
||
|
const updatedMessage = await getLatestMessageForChat(chatId);
|
||
|
expect(updatedMessage?.responseMessages).toEqual(emptyResponse);
|
||
|
});
|
||
|
|
||
|
test('handles complex nested JSONB structure', async () => {
|
||
|
const { messageId, chatId } = await createTestMessageWithContext();
|
||
|
|
||
|
const complexResponse = {
|
||
|
messages: [
|
||
|
{ role: 'assistant', content: 'Hello' },
|
||
|
{ role: 'user', content: 'Hi there' },
|
||
|
],
|
||
|
metadata: {
|
||
|
tokens: 250,
|
||
|
reasoning: {
|
||
|
steps: ['analyze', 'respond'],
|
||
|
confidence: 0.95,
|
||
|
},
|
||
|
},
|
||
|
charts: {
|
||
|
type: 'bar',
|
||
|
data: [1, 2, 3, 4],
|
||
|
},
|
||
|
};
|
||
|
|
||
|
const result = await updateMessageResponseMessages(messageId, complexResponse);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
|
||
|
const updatedMessage = await getLatestMessageForChat(chatId);
|
||
|
expect(updatedMessage?.responseMessages).toEqual(complexResponse);
|
||
|
});
|
||
|
|
||
|
test('throws error for non-existent message ID', async () => {
|
||
|
const nonExistentId = '00000000-0000-0000-0000-000000000000';
|
||
|
|
||
|
await expect(
|
||
|
updateMessageResponseMessages(nonExistentId, { content: 'test' })
|
||
|
).rejects.toThrow(`Message not found or has been deleted: ${nonExistentId}`);
|
||
|
});
|
||
|
|
||
|
test('throws error for invalid UUID format', async () => {
|
||
|
const invalidId = 'invalid-uuid';
|
||
|
|
||
|
await expect(updateMessageResponseMessages(invalidId, { content: 'test' })).rejects.toThrow(
|
||
|
'Failed to update response messages for message invalid-uuid'
|
||
|
);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('updateMessageReasoning', () => {
|
||
|
test('successfully updates reasoning JSONB field', async () => {
|
||
|
const { messageId, chatId } = await createTestMessageWithContext();
|
||
|
|
||
|
const newReasoning = {
|
||
|
steps: ['analyze data', 'identify patterns', 'generate insights'],
|
||
|
conclusion: 'Analysis complete',
|
||
|
confidence: 0.89,
|
||
|
metadata: { duration: '45s' },
|
||
|
};
|
||
|
|
||
|
const result = await updateMessageReasoning(messageId, newReasoning);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
|
||
|
// Verify the update was persisted
|
||
|
const updatedMessage = await getLatestMessageForChat(chatId);
|
||
|
expect(updatedMessage?.reasoning).toEqual(newReasoning);
|
||
|
});
|
||
|
|
||
|
test('handles simple reasoning object', async () => {
|
||
|
const { messageId, chatId } = await createTestMessageWithContext();
|
||
|
|
||
|
const simpleReasoning = {
|
||
|
thought: 'This is a simple reasoning step',
|
||
|
};
|
||
|
|
||
|
const result = await updateMessageReasoning(messageId, simpleReasoning);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
|
||
|
const updatedMessage = await getLatestMessageForChat(chatId);
|
||
|
expect(updatedMessage?.reasoning).toEqual(simpleReasoning);
|
||
|
});
|
||
|
|
||
|
test('throws error when trying to set reasoning to null', async () => {
|
||
|
const { messageId } = await createTestMessageWithContext();
|
||
|
|
||
|
await expect(updateMessageReasoning(messageId, null)).rejects.toThrow(
|
||
|
'Reasoning cannot be null - database constraint violation'
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('throws error when trying to set reasoning to undefined', async () => {
|
||
|
const { messageId } = await createTestMessageWithContext();
|
||
|
|
||
|
await expect(updateMessageReasoning(messageId, undefined)).rejects.toThrow(
|
||
|
'Reasoning cannot be null - database constraint violation'
|
||
|
);
|
||
|
});
|
||
|
|
||
|
test('throws error for non-existent message ID', async () => {
|
||
|
const nonExistentId = '00000000-0000-0000-0000-000000000000';
|
||
|
|
||
|
await expect(updateMessageReasoning(nonExistentId, { thought: 'test' })).rejects.toThrow(
|
||
|
`Message not found or has been deleted: ${nonExistentId}`
|
||
|
);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('updateMessageStreamingFields', () => {
|
||
|
test('successfully updates both responseMessages and reasoning in single query', async () => {
|
||
|
const { messageId, chatId } = await createTestMessageWithContext();
|
||
|
|
||
|
const newResponseMessages = {
|
||
|
content: 'Streaming response',
|
||
|
tokens: 100,
|
||
|
};
|
||
|
|
||
|
const newReasoning = {
|
||
|
step: 'processing',
|
||
|
progress: 0.75,
|
||
|
};
|
||
|
|
||
|
const result = await updateMessageStreamingFields(
|
||
|
messageId,
|
||
|
newResponseMessages,
|
||
|
newReasoning
|
||
|
);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
|
||
|
// Verify both fields were updated
|
||
|
const updatedMessage = await getLatestMessageForChat(chatId);
|
||
|
expect(updatedMessage?.responseMessages).toEqual(newResponseMessages);
|
||
|
expect(updatedMessage?.reasoning).toEqual(newReasoning);
|
||
|
});
|
||
|
|
||
|
test('handles large streaming data efficiently', async () => {
|
||
|
const { messageId, chatId } = await createTestMessageWithContext();
|
||
|
|
||
|
// Simulate large streaming data
|
||
|
const largeResponseMessages = {
|
||
|
content: 'A'.repeat(10000), // Large content
|
||
|
chunks: Array.from({ length: 1000 }, (_, i) => ({ id: i, data: `chunk-${i}` })),
|
||
|
};
|
||
|
|
||
|
const largeReasoning = {
|
||
|
steps: Array.from({ length: 500 }, (_, i) => `reasoning-step-${i}`),
|
||
|
detailed_analysis: 'B'.repeat(5000),
|
||
|
};
|
||
|
|
||
|
const result = await updateMessageStreamingFields(
|
||
|
messageId,
|
||
|
largeResponseMessages,
|
||
|
largeReasoning
|
||
|
);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
|
||
|
const updatedMessage = await getLatestMessageForChat(chatId);
|
||
|
expect(updatedMessage?.responseMessages).toEqual(largeResponseMessages);
|
||
|
expect(updatedMessage?.reasoning).toEqual(largeReasoning);
|
||
|
});
|
||
|
|
||
|
test('throws error when reasoning is null', async () => {
|
||
|
const { messageId } = await createTestMessageWithContext();
|
||
|
|
||
|
const responseMessages = { content: 'valid response' };
|
||
|
const reasoning = null;
|
||
|
|
||
|
await expect(
|
||
|
updateMessageStreamingFields(messageId, responseMessages, reasoning)
|
||
|
).rejects.toThrow('Reasoning cannot be null - database constraint violation');
|
||
|
});
|
||
|
|
||
|
test('throws error when reasoning is undefined', async () => {
|
||
|
const { messageId } = await createTestMessageWithContext();
|
||
|
|
||
|
const responseMessages = { content: 'valid response' };
|
||
|
const reasoning = undefined;
|
||
|
|
||
|
await expect(
|
||
|
updateMessageStreamingFields(messageId, responseMessages, reasoning)
|
||
|
).rejects.toThrow('Reasoning cannot be null - database constraint violation');
|
||
|
});
|
||
|
|
||
|
test('overwrites previous data completely', async () => {
|
||
|
const { messageId, chatId } = await createTestMessageWithContext();
|
||
|
|
||
|
// First update
|
||
|
const initialResponse = { content: 'initial', version: 1 };
|
||
|
const initialReasoning = { step: 'initial', data: 'old' };
|
||
|
|
||
|
await updateMessageStreamingFields(messageId, initialResponse, initialReasoning);
|
||
|
|
||
|
// Second update should completely replace
|
||
|
const newResponse = { content: 'updated', version: 2, newField: 'added' };
|
||
|
const newReasoning = { step: 'updated', different: 'structure' };
|
||
|
|
||
|
const result = await updateMessageStreamingFields(messageId, newResponse, newReasoning);
|
||
|
|
||
|
expect(result.success).toBe(true);
|
||
|
|
||
|
const updatedMessage = await getLatestMessageForChat(chatId);
|
||
|
expect(updatedMessage?.responseMessages).toEqual(newResponse);
|
||
|
expect(updatedMessage?.reasoning).toEqual(newReasoning);
|
||
|
|
||
|
// Verify old fields are gone
|
||
|
expect(updatedMessage?.responseMessages).not.toHaveProperty('version', 1);
|
||
|
expect(updatedMessage?.reasoning).not.toHaveProperty('data');
|
||
|
});
|
||
|
|
||
|
test('throws error for non-existent message ID', async () => {
|
||
|
const nonExistentId = '00000000-0000-0000-0000-000000000000';
|
||
|
|
||
|
await expect(
|
||
|
updateMessageStreamingFields(nonExistentId, { content: 'test' }, { step: 'test' })
|
||
|
).rejects.toThrow(`Message not found or has been deleted: ${nonExistentId}`);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
describe('Performance and Concurrency', () => {
|
||
|
test('handles rapid sequential updates without data corruption', async () => {
|
||
|
const { messageId, chatId } = await createTestMessageWithContext();
|
||
|
|
||
|
// Simulate streaming updates sequentially to avoid race conditions
|
||
|
for (let i = 0; i < 5; i++) {
|
||
|
const result = await updateMessageStreamingFields(
|
||
|
messageId,
|
||
|
{ content: `update-${i}`, iteration: i },
|
||
|
{ step: i, timestamp: Date.now() }
|
||
|
);
|
||
|
expect(result.success).toBe(true);
|
||
|
}
|
||
|
|
||
|
// Final state should be consistent (last update applied)
|
||
|
const finalMessage = await getLatestMessageForChat(chatId);
|
||
|
expect((finalMessage?.responseMessages as any)?.iteration).toBe(4);
|
||
|
expect((finalMessage?.reasoning as any)?.step).toBe(4);
|
||
|
});
|
||
|
|
||
|
test('updates timestamp on every change', async () => {
|
||
|
const { messageId, chatId } = await createTestMessageWithContext();
|
||
|
|
||
|
const originalMessage = await getLatestMessageForChat(chatId);
|
||
|
const originalTimestamp = originalMessage?.updatedAt;
|
||
|
|
||
|
// Wait a moment to ensure timestamp difference
|
||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||
|
|
||
|
await updateMessageResponseMessages(messageId, { content: 'updated' });
|
||
|
|
||
|
const updatedMessage = await getLatestMessageForChat(chatId);
|
||
|
expect(updatedMessage?.updatedAt).not.toBe(originalTimestamp);
|
||
|
expect(new Date(updatedMessage?.updatedAt || '').getTime()).toBeGreaterThan(
|
||
|
new Date(originalTimestamp || '').getTime()
|
||
|
);
|
||
|
});
|
||
|
});
|
||
|
});
|