mirror of https://github.com/buster-so/buster.git
additional lint fixes
This commit is contained in:
parent
7e7aeb3480
commit
5d532a2957
|
@ -25,7 +25,8 @@ export type GenerateAssetMessagesInput = z.infer<typeof GenerateAssetMessagesInp
|
|||
interface AssetDetails {
|
||||
id: string;
|
||||
name: string;
|
||||
content?: any;
|
||||
//TODO: Dallin let's make a type for this. It should not just be a jsonb object.
|
||||
content?: unknown;
|
||||
createdBy: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -94,155 +94,6 @@ export async function getAllRawLlmMessagesForChat(chatId: string) {
|
|||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently update the responseMessages JSONB field for a specific message
|
||||
* Optimized for frequent streaming updates - replaces entire JSONB content
|
||||
* @param messageId - The ID of the message to update
|
||||
* @param responseMessages - The new response messages content (will completely replace existing)
|
||||
* @returns Success status
|
||||
*/
|
||||
export async function updateMessageResponseMessages(
|
||||
messageId: string,
|
||||
responseMessages: any
|
||||
): Promise<{ success: boolean }> {
|
||||
try {
|
||||
// First verify the message exists and is not deleted
|
||||
const existingMessage = await db
|
||||
.select({ id: messages.id })
|
||||
.from(messages)
|
||||
.where(and(eq(messages.id, messageId), isNull(messages.deletedAt)))
|
||||
.limit(1);
|
||||
|
||||
if (existingMessage.length === 0) {
|
||||
throw new Error(`Message not found or has been deleted: ${messageId}`);
|
||||
}
|
||||
|
||||
await db
|
||||
.update(messages)
|
||||
.set({
|
||||
responseMessages,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(and(eq(messages.id, messageId), isNull(messages.deletedAt)));
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to update message responseMessages:', error);
|
||||
// Re-throw our specific validation errors
|
||||
if (error instanceof Error && error.message.includes('Message not found')) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Failed to update response messages for message ${messageId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently update the reasoning JSONB field for a specific message
|
||||
* Optimized for frequent streaming updates - replaces entire JSONB content
|
||||
* Note: reasoning field has NOT NULL constraint, so null values are not allowed
|
||||
* @param messageId - The ID of the message to update
|
||||
* @param reasoning - The new reasoning content (will completely replace existing)
|
||||
* @returns Success status
|
||||
*/
|
||||
export async function updateMessageReasoning(
|
||||
messageId: string,
|
||||
reasoning: any
|
||||
): Promise<{ success: boolean }> {
|
||||
try {
|
||||
// First verify the message exists and is not deleted
|
||||
const existingMessage = await db
|
||||
.select({ id: messages.id })
|
||||
.from(messages)
|
||||
.where(and(eq(messages.id, messageId), isNull(messages.deletedAt)))
|
||||
.limit(1);
|
||||
|
||||
if (existingMessage.length === 0) {
|
||||
throw new Error(`Message not found or has been deleted: ${messageId}`);
|
||||
}
|
||||
|
||||
// Validate reasoning is not null (database constraint)
|
||||
if (reasoning === null || reasoning === undefined) {
|
||||
throw new Error('Reasoning cannot be null - database constraint violation');
|
||||
}
|
||||
|
||||
await db
|
||||
.update(messages)
|
||||
.set({
|
||||
reasoning,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(and(eq(messages.id, messageId), isNull(messages.deletedAt)));
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to update message reasoning:', error);
|
||||
// Re-throw our specific validation errors
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(error.message.includes('Message not found') ||
|
||||
error.message.includes('Reasoning cannot be null'))
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Failed to update reasoning for message ${messageId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Efficiently update both responseMessages and reasoning JSONB fields in a single query
|
||||
* Most efficient option when both fields need updating during streaming
|
||||
* Note: reasoning field has NOT NULL constraint, so null values are not allowed
|
||||
* @param messageId - The ID of the message to update
|
||||
* @param responseMessages - The new response messages content
|
||||
* @param reasoning - The new reasoning content (cannot be null)
|
||||
* @returns Success status
|
||||
*/
|
||||
export async function updateMessageStreamingFields(
|
||||
messageId: string,
|
||||
responseMessages: any,
|
||||
reasoning: any
|
||||
): Promise<{ success: boolean }> {
|
||||
try {
|
||||
// First verify the message exists and is not deleted
|
||||
const existingMessage = await db
|
||||
.select({ id: messages.id })
|
||||
.from(messages)
|
||||
.where(and(eq(messages.id, messageId), isNull(messages.deletedAt)))
|
||||
.limit(1);
|
||||
|
||||
if (existingMessage.length === 0) {
|
||||
throw new Error(`Message not found or has been deleted: ${messageId}`);
|
||||
}
|
||||
|
||||
// Validate reasoning is not null (database constraint)
|
||||
if (reasoning === null || reasoning === undefined) {
|
||||
throw new Error('Reasoning cannot be null - database constraint violation');
|
||||
}
|
||||
|
||||
await db
|
||||
.update(messages)
|
||||
.set({
|
||||
responseMessages,
|
||||
reasoning,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(and(eq(messages.id, messageId), isNull(messages.deletedAt)));
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to update message streaming fields:', error);
|
||||
// Re-throw our specific validation errors
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(error.message.includes('Message not found') ||
|
||||
error.message.includes('Reasoning cannot be null'))
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Failed to update streaming fields for message ${messageId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flexibly update message fields - only updates fields that are provided
|
||||
* Allows updating responseMessages, reasoning, and/or rawLlmMessages in a single query
|
||||
|
@ -254,9 +105,10 @@ export async function updateMessageStreamingFields(
|
|||
export async function updateMessageFields(
|
||||
messageId: string,
|
||||
fields: {
|
||||
responseMessages?: any;
|
||||
reasoning?: any;
|
||||
rawLlmMessages?: any;
|
||||
//TODO: Dallin let's make a type for this. It should not just be a jsonb object.
|
||||
responseMessages?: unknown;
|
||||
reasoning?: unknown;
|
||||
rawLlmMessages?: unknown;
|
||||
finalReasoningMessage?: string;
|
||||
}
|
||||
): Promise<{ success: boolean }> {
|
||||
|
@ -267,7 +119,13 @@ export async function updateMessageFields(
|
|||
}
|
||||
|
||||
// Build update object with only provided fields
|
||||
const updateData: any = {
|
||||
const updateData: {
|
||||
updatedAt: string;
|
||||
responseMessages?: unknown;
|
||||
reasoning?: unknown;
|
||||
rawLlmMessages?: unknown;
|
||||
finalReasoningMessage?: string;
|
||||
} = {
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
@ -335,18 +193,16 @@ export async function updateMessage(
|
|||
throw new Error('Reasoning cannot be null - database constraint violation');
|
||||
}
|
||||
|
||||
// Remove undefined fields and build update object
|
||||
const updateData: any = {
|
||||
const updateData = {
|
||||
updatedAt: new Date().toISOString(),
|
||||
...Object.fromEntries(
|
||||
Object.entries(fields).filter(
|
||||
([key, value]) =>
|
||||
value !== undefined && key !== 'id' && key !== 'createdAt' && key !== 'deletedAt'
|
||||
)
|
||||
),
|
||||
};
|
||||
|
||||
// Only add fields that are actually provided (not undefined)
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
if (value !== undefined && key !== 'id' && key !== 'createdAt' && key !== 'deletedAt') {
|
||||
updateData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// If updatedAt was explicitly provided, use that instead
|
||||
if ('updatedAt' in fields && fields.updatedAt !== undefined) {
|
||||
updateData.updatedAt = fields.updatedAt;
|
||||
|
|
|
@ -9,13 +9,13 @@ export const BarAndLineAxisSchema = z
|
|||
// the column ids to use for the category axis. If multiple column ids are provided, they will be grouped together. THE LLM SHOULD NEVER SET MULTIPLE CATEGORY COLUMNS. ONLY THE USER CAN SET THIS.
|
||||
category: z.array(z.string()).default([]),
|
||||
// if null the y axis will automatically be used, the y axis will be used for the tooltip.
|
||||
tooltip: z.nullable(z.array(z.string())).default(null).optional()
|
||||
tooltip: z.nullable(z.array(z.string())).default(null).optional(),
|
||||
})
|
||||
.default({
|
||||
x: [],
|
||||
y: [],
|
||||
category: [],
|
||||
tooltip: null
|
||||
tooltip: null,
|
||||
});
|
||||
|
||||
export const ScatterAxisSchema = z
|
||||
|
@ -29,14 +29,14 @@ export const ScatterAxisSchema = z
|
|||
// the column id to use for the size range of the dots. ONLY one column id should be provided.
|
||||
size: z.tuple([z.string()]).or(z.array(z.string()).length(0)).default([]),
|
||||
// if null the y axis will automatically be used, the y axis will be used for the tooltip.
|
||||
tooltip: z.nullable(z.array(z.string())).default(null)
|
||||
tooltip: z.nullable(z.array(z.string())).default(null),
|
||||
})
|
||||
.default({
|
||||
x: [],
|
||||
y: [],
|
||||
size: [],
|
||||
category: [],
|
||||
tooltip: null
|
||||
tooltip: null,
|
||||
});
|
||||
|
||||
export const ComboChartAxisSchema = z
|
||||
|
@ -50,14 +50,14 @@ export const ComboChartAxisSchema = z
|
|||
// the column ids to use for the category axis. If multiple column ids are provided, they will be grouped together. THE LLM SHOULD NEVER SET MULTIPLE CATEGORY COLUMNS. ONLY THE USER CAN SET THIS.
|
||||
category: z.array(z.string()).default([]),
|
||||
// if null the y axis will automatically be used, the y axis will be used for the tooltip.
|
||||
tooltip: z.nullable(z.array(z.string())).default(null).optional()
|
||||
tooltip: z.nullable(z.array(z.string())).default(null).optional(),
|
||||
})
|
||||
.default({
|
||||
x: [],
|
||||
y: [],
|
||||
y2: [],
|
||||
category: [],
|
||||
tooltip: null
|
||||
tooltip: null,
|
||||
});
|
||||
|
||||
export const PieChartAxisSchema = z
|
||||
|
@ -67,19 +67,19 @@ export const PieChartAxisSchema = z
|
|||
// the column ids to use for the y axis. If multiple column ids are provided, they will appear as rings. The LLM should NEVER set multiple y axis columns. Only the user can set this.
|
||||
y: z.array(z.string()).default([]),
|
||||
// if null the y axis will automatically be used, the y axis will be used for the tooltip.
|
||||
tooltip: z.nullable(z.array(z.string())).default(null)
|
||||
tooltip: z.nullable(z.array(z.string())).default(null),
|
||||
})
|
||||
.default({
|
||||
x: [],
|
||||
y: [],
|
||||
tooltip: null
|
||||
tooltip: null,
|
||||
});
|
||||
|
||||
export const ChartEncodesSchema = z.union([
|
||||
BarAndLineAxisSchema,
|
||||
ScatterAxisSchema,
|
||||
PieChartAxisSchema,
|
||||
ComboChartAxisSchema
|
||||
ComboChartAxisSchema,
|
||||
]);
|
||||
|
||||
// Export inferred types
|
||||
|
|
|
@ -12,7 +12,7 @@ export const BarChartPropsSchema = z.object({
|
|||
// OPTIONAL: default is group. This will only apply if the columnVisualization is set to 'bar'.
|
||||
barGroupType: z.nullable(z.enum(['stack', 'group', 'percentage-stack'])).default('group'),
|
||||
// OPTIONAL: default is false. This will only apply if is is stacked and there is either a category or multiple y axis applie to the series.
|
||||
barShowTotalAtTop: z.boolean().default(false)
|
||||
barShowTotalAtTop: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type BarChartProps = z.infer<typeof BarChartPropsSchema>;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { ChartConfigPropsSchema } from "./chartConfigProps";
|
||||
import { DEFAULT_CHART_THEME } from "./configColors";
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ChartConfigPropsSchema } from './chartConfigProps';
|
||||
import { DEFAULT_CHART_THEME } from './configColors';
|
||||
|
||||
describe("DEFAULT_CHART_CONFIG", () => {
|
||||
it("should conform to BusterChartConfigPropsSchema and have expected default values", () => {
|
||||
describe('DEFAULT_CHART_CONFIG', () => {
|
||||
it('should conform to BusterChartConfigPropsSchema and have expected default values', () => {
|
||||
// Verify that DEFAULT_CHART_CONFIG is valid according to the schema
|
||||
const parseResult = ChartConfigPropsSchema.safeParse({});
|
||||
expect(parseResult.success).toBe(true);
|
||||
|
@ -21,9 +21,9 @@ describe("DEFAULT_CHART_CONFIG", () => {
|
|||
expect(config.columnSettings).toEqual({});
|
||||
expect(config.columnLabelFormats).toEqual({});
|
||||
expect(config.showLegend).toBeNull();
|
||||
expect(config.barLayout).toBe("vertical");
|
||||
expect(config.barLayout).toBe('vertical');
|
||||
expect(config.barSortBy).toEqual([]);
|
||||
expect(config.barGroupType).toBe("group");
|
||||
expect(config.barGroupType).toBe('group');
|
||||
expect(config.barShowTotalAtTop).toBe(false);
|
||||
expect(config.lineGroupType).toBeNull();
|
||||
expect(config.scatterAxis).toEqual({
|
||||
|
@ -40,7 +40,7 @@ describe("DEFAULT_CHART_CONFIG", () => {
|
|||
});
|
||||
|
||||
// Verify the config is a complete object with all required properties
|
||||
expect(typeof config).toBe("object");
|
||||
expect(typeof config).toBe('object');
|
||||
expect(config).not.toBeNull();
|
||||
|
||||
// Verify it has a selectedChartType (required field)
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ComboChartAxisSchema } from './axisInterfaces';
|
|||
|
||||
export const ComboChartPropsSchema = z.object({
|
||||
// Required for Combo
|
||||
comboChartAxis: ComboChartAxisSchema
|
||||
comboChartAxis: ComboChartAxisSchema,
|
||||
});
|
||||
|
||||
export type ComboChartProps = z.infer<typeof ComboChartPropsSchema>;
|
||||
|
|
|
@ -8,5 +8,5 @@ export const DEFAULT_CHART_THEME = [
|
|||
'#F3864F',
|
||||
'#C82184',
|
||||
'#31FCB4',
|
||||
'#E83562'
|
||||
'#E83562',
|
||||
];
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { LineChartPropsSchema, type LineChartProps } from './lineChartProps';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { type LineChartProps, LineChartPropsSchema } from './lineChartProps';
|
||||
|
||||
describe('LineChartPropsSchema', () => {
|
||||
describe('valid inputs', () => {
|
||||
|
@ -81,17 +81,17 @@ describe('LineChartPropsSchema', () => {
|
|||
describe('type inference', () => {
|
||||
it('should correctly infer LineChartProps type', () => {
|
||||
const validData: LineChartProps = {
|
||||
lineGroupType: 'stack'
|
||||
lineGroupType: 'stack',
|
||||
};
|
||||
expect(LineChartPropsSchema.parse(validData)).toEqual(validData);
|
||||
|
||||
const validDataWithNull: LineChartProps = {
|
||||
lineGroupType: null
|
||||
lineGroupType: null,
|
||||
};
|
||||
expect(LineChartPropsSchema.parse(validDataWithNull)).toEqual(validDataWithNull);
|
||||
|
||||
const validDataWithPercentage: LineChartProps = {
|
||||
lineGroupType: 'percentage-stack'
|
||||
lineGroupType: 'percentage-stack',
|
||||
};
|
||||
expect(LineChartPropsSchema.parse(validDataWithPercentage)).toEqual(validDataWithPercentage);
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import { z } from 'zod/v4';
|
|||
|
||||
export const LineChartPropsSchema = z.object({
|
||||
// OPTIONAL: default is null. This will only apply if the columnVisualization is set to 'line'. If this is set to stack it will stack the lines on top of each other. The UI has this labeled as "Show as %"
|
||||
lineGroupType: z.enum(['stack', 'percentage-stack']).nullable().default(null)
|
||||
lineGroupType: z.enum(['stack', 'percentage-stack']).nullable().default(null),
|
||||
});
|
||||
|
||||
export type LineChartProps = z.infer<typeof LineChartPropsSchema>;
|
||||
|
|
|
@ -6,7 +6,7 @@ export const DerivedMetricTitleSchema = z.object({
|
|||
// whether to display to use the key or the value in the chart
|
||||
useValue: z.boolean(),
|
||||
// OPTIONAL: default is sum
|
||||
aggregate: z.enum(['sum', 'average', 'median', 'max', 'min', 'count', 'first']).default('sum')
|
||||
aggregate: z.enum(['sum', 'average', 'median', 'max', 'min', 'count', 'first']).default('sum'),
|
||||
});
|
||||
|
||||
export const MetricChartPropsSchema = z.object({
|
||||
|
@ -21,7 +21,7 @@ export const MetricChartPropsSchema = z.object({
|
|||
// OPTIONAL: default is ''
|
||||
metricSubHeader: z.nullable(z.union([z.string(), DerivedMetricTitleSchema])).default(null),
|
||||
// OPTIONAL: default is null. If null then the metricColumnId will be used in conjunction with the metricValueAggregate. If not null, then the metricValueLabel will be used.
|
||||
metricValueLabel: z.nullable(z.string()).default(null)
|
||||
metricValueLabel: z.nullable(z.string()).default(null),
|
||||
});
|
||||
|
||||
export type DerivedMetricTitle = z.infer<typeof DerivedMetricTitleSchema>;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from "zod/v4";
|
||||
import { PieChartAxisSchema } from "./axisInterfaces";
|
||||
import { PieSortBySchema } from "./etcInterfaces";
|
||||
import { z } from 'zod/v4';
|
||||
import { PieChartAxisSchema } from './axisInterfaces';
|
||||
import { PieSortBySchema } from './etcInterfaces';
|
||||
|
||||
export const PieChartPropsSchema = z.object({
|
||||
// OPTIONAL: default: value
|
||||
|
@ -8,30 +8,28 @@ export const PieChartPropsSchema = z.object({
|
|||
// Required for Pie
|
||||
pieChartAxis: PieChartAxisSchema,
|
||||
// OPTIONAL: default: number
|
||||
pieDisplayLabelAs: z.enum(["percent", "number"]).default("number"),
|
||||
pieDisplayLabelAs: z.enum(['percent', 'number']).default('number'),
|
||||
// OPTIONAL: default true if donut width is set. If the data contains a percentage, set this as false.
|
||||
pieShowInnerLabel: z.boolean().default(true),
|
||||
// OPTIONAL: default: sum
|
||||
pieInnerLabelAggregate: z
|
||||
.enum(["sum", "average", "median", "max", "min", "count"])
|
||||
.default("sum"),
|
||||
.enum(['sum', 'average', 'median', 'max', 'min', 'count'])
|
||||
.default('sum'),
|
||||
// OPTIONAL: default is null and will be the name of the pieInnerLabelAggregate
|
||||
pieInnerLabelTitle: z.string().nullable().default(null),
|
||||
// OPTIONAL: default: none
|
||||
pieLabelPosition: z
|
||||
.nullable(z.enum(["inside", "outside", "none"]))
|
||||
.default("none"),
|
||||
pieLabelPosition: z.nullable(z.enum(['inside', 'outside', 'none'])).default('none'),
|
||||
// OPTIONAL: default: 55 | range 0-65 | range represents percent size of the donut hole. If user asks for a pie this should be 0
|
||||
pieDonutWidth: z
|
||||
.number()
|
||||
.min(0, "Donut width must be at least 0")
|
||||
.max(65, "Donut width must be at most 65")
|
||||
.min(0, 'Donut width must be at least 0')
|
||||
.max(65, 'Donut width must be at most 65')
|
||||
.default(40),
|
||||
// OPTIONAL: default: 2.5 | range 0-100 | If there are items that are less than this percentage of the pie, they combine to form a single slice.
|
||||
pieMinimumSlicePercentage: z
|
||||
.number()
|
||||
.min(0, "Minimum slice percentage must be at least 0")
|
||||
.max(100, "Minimum slice percentage must be at most 100")
|
||||
.min(0, 'Minimum slice percentage must be at least 0')
|
||||
.max(100, 'Minimum slice percentage must be at most 100')
|
||||
.default(0),
|
||||
});
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { ScatterAxisSchema } from './axisInterfaces';
|
|||
export const ScatterChartPropsSchema = z.object({
|
||||
// Required for Scatter
|
||||
scatterAxis: ScatterAxisSchema,
|
||||
scatterDotSize: z.tuple([z.number(), z.number()]).default([3, 15])
|
||||
scatterDotSize: z.tuple([z.number(), z.number()]).default([3, 15]),
|
||||
});
|
||||
|
||||
export type ScatterChartProps = z.infer<typeof ScatterChartPropsSchema>;
|
||||
|
|
|
@ -5,7 +5,7 @@ export const TableChartPropsSchema = z.object({
|
|||
tableColumnWidths: z.nullable(z.record(z.string(), z.number())).default(null),
|
||||
tableHeaderBackgroundColor: z.nullable(z.string()).default(null),
|
||||
tableHeaderFontColor: z.nullable(z.string()).default(null),
|
||||
tableColumnFontColor: z.nullable(z.string()).default(null)
|
||||
tableColumnFontColor: z.nullable(z.string()).default(null),
|
||||
});
|
||||
|
||||
export type TableChartProps = z.infer<typeof TableChartPropsSchema>;
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import {
|
||||
getLatestMessageForChat,
|
||||
updateMessageReasoning,
|
||||
updateMessageResponseMessages,
|
||||
updateMessageStreamingFields,
|
||||
} from '@buster/database/src/helpers/messages';
|
||||
} from '@buster/database';
|
||||
import { createTestMessageWithContext } from '@buster/test-utils';
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
||||
import { cleanupTestEnvironment, setupTestEnvironment } from './helpers';
|
||||
import { cleanupTestEnvironment, setupTestEnvironment } from '../../src/envHelpers/env-helpers';
|
||||
|
||||
describe('Message Update Helpers', () => {
|
||||
beforeEach(async () => {
|
||||
|
@ -17,84 +16,6 @@ describe('Message Update Helpers', () => {
|
|||
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();
|
||||
|
@ -266,43 +187,4 @@ describe('Message Update Helpers', () => {
|
|||
).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()
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import {
|
||||
getLatestMessageForChat,
|
||||
updateMessageFields,
|
||||
} from '@buster/database/src/helpers/messages';
|
||||
import { getLatestMessageForChat, updateMessageFields } from '@buster/database';
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
||||
import { createTestMessageWithContext } from '../../src';
|
||||
import { cleanupTestEnvironment, setupTestEnvironment } from './helpers';
|
||||
import { cleanupTestEnvironment, setupTestEnvironment } from '../../src/envHelpers/env-helpers';
|
||||
|
||||
describe('updateMessageFields', () => {
|
||||
beforeEach(async () => {
|
||||
|
|
Loading…
Reference in New Issue