additional lint fixes

This commit is contained in:
Nate Kelley 2025-07-07 10:12:31 -06:00
parent 7e7aeb3480
commit 5d532a2957
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
15 changed files with 65 additions and 331 deletions

View File

@ -25,7 +25,8 @@ export type GenerateAssetMessagesInput = z.infer<typeof GenerateAssetMessagesInp
interface AssetDetails { interface AssetDetails {
id: string; id: string;
name: 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; createdBy: string;
} }

View File

@ -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 * Flexibly update message fields - only updates fields that are provided
* Allows updating responseMessages, reasoning, and/or rawLlmMessages in a single query * Allows updating responseMessages, reasoning, and/or rawLlmMessages in a single query
@ -254,9 +105,10 @@ export async function updateMessageStreamingFields(
export async function updateMessageFields( export async function updateMessageFields(
messageId: string, messageId: string,
fields: { fields: {
responseMessages?: any; //TODO: Dallin let's make a type for this. It should not just be a jsonb object.
reasoning?: any; responseMessages?: unknown;
rawLlmMessages?: any; reasoning?: unknown;
rawLlmMessages?: unknown;
finalReasoningMessage?: string; finalReasoningMessage?: string;
} }
): Promise<{ success: boolean }> { ): Promise<{ success: boolean }> {
@ -267,7 +119,13 @@ export async function updateMessageFields(
} }
// Build update object with only provided fields // 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(), updatedAt: new Date().toISOString(),
}; };
@ -335,18 +193,16 @@ export async function updateMessage(
throw new Error('Reasoning cannot be null - database constraint violation'); throw new Error('Reasoning cannot be null - database constraint violation');
} }
// Remove undefined fields and build update object const updateData = {
const updateData: any = {
updatedAt: new Date().toISOString(), 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 was explicitly provided, use that instead
if ('updatedAt' in fields && fields.updatedAt !== undefined) { if ('updatedAt' in fields && fields.updatedAt !== undefined) {
updateData.updatedAt = fields.updatedAt; updateData.updatedAt = fields.updatedAt;

View File

@ -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. // 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([]), category: z.array(z.string()).default([]),
// if null the y axis will automatically be used, the y axis will be used for the tooltip. // 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({ .default({
x: [], x: [],
y: [], y: [],
category: [], category: [],
tooltip: null tooltip: null,
}); });
export const ScatterAxisSchema = z 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. // 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([]), 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. // 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({ .default({
x: [], x: [],
y: [], y: [],
size: [], size: [],
category: [], category: [],
tooltip: null tooltip: null,
}); });
export const ComboChartAxisSchema = z 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. // 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([]), category: z.array(z.string()).default([]),
// if null the y axis will automatically be used, the y axis will be used for the tooltip. // 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({ .default({
x: [], x: [],
y: [], y: [],
y2: [], y2: [],
category: [], category: [],
tooltip: null tooltip: null,
}); });
export const PieChartAxisSchema = z 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. // 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([]), y: z.array(z.string()).default([]),
// if null the y axis will automatically be used, the y axis will be used for the tooltip. // 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({ .default({
x: [], x: [],
y: [], y: [],
tooltip: null tooltip: null,
}); });
export const ChartEncodesSchema = z.union([ export const ChartEncodesSchema = z.union([
BarAndLineAxisSchema, BarAndLineAxisSchema,
ScatterAxisSchema, ScatterAxisSchema,
PieChartAxisSchema, PieChartAxisSchema,
ComboChartAxisSchema ComboChartAxisSchema,
]); ]);
// Export inferred types // Export inferred types

View File

@ -12,7 +12,7 @@ export const BarChartPropsSchema = z.object({
// OPTIONAL: default is group. This will only apply if the columnVisualization is set to 'bar'. // 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'), 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. // 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>; export type BarChartProps = z.infer<typeof BarChartPropsSchema>;

View File

@ -1,9 +1,9 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from 'vitest';
import { ChartConfigPropsSchema } from "./chartConfigProps"; import { ChartConfigPropsSchema } from './chartConfigProps';
import { DEFAULT_CHART_THEME } from "./configColors"; import { DEFAULT_CHART_THEME } from './configColors';
describe("DEFAULT_CHART_CONFIG", () => { describe('DEFAULT_CHART_CONFIG', () => {
it("should conform to BusterChartConfigPropsSchema and have expected default values", () => { it('should conform to BusterChartConfigPropsSchema and have expected default values', () => {
// Verify that DEFAULT_CHART_CONFIG is valid according to the schema // Verify that DEFAULT_CHART_CONFIG is valid according to the schema
const parseResult = ChartConfigPropsSchema.safeParse({}); const parseResult = ChartConfigPropsSchema.safeParse({});
expect(parseResult.success).toBe(true); expect(parseResult.success).toBe(true);
@ -21,9 +21,9 @@ describe("DEFAULT_CHART_CONFIG", () => {
expect(config.columnSettings).toEqual({}); expect(config.columnSettings).toEqual({});
expect(config.columnLabelFormats).toEqual({}); expect(config.columnLabelFormats).toEqual({});
expect(config.showLegend).toBeNull(); expect(config.showLegend).toBeNull();
expect(config.barLayout).toBe("vertical"); expect(config.barLayout).toBe('vertical');
expect(config.barSortBy).toEqual([]); expect(config.barSortBy).toEqual([]);
expect(config.barGroupType).toBe("group"); expect(config.barGroupType).toBe('group');
expect(config.barShowTotalAtTop).toBe(false); expect(config.barShowTotalAtTop).toBe(false);
expect(config.lineGroupType).toBeNull(); expect(config.lineGroupType).toBeNull();
expect(config.scatterAxis).toEqual({ expect(config.scatterAxis).toEqual({
@ -40,7 +40,7 @@ describe("DEFAULT_CHART_CONFIG", () => {
}); });
// Verify the config is a complete object with all required properties // 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(); expect(config).not.toBeNull();
// Verify it has a selectedChartType (required field) // Verify it has a selectedChartType (required field)

View File

@ -3,7 +3,7 @@ import { ComboChartAxisSchema } from './axisInterfaces';
export const ComboChartPropsSchema = z.object({ export const ComboChartPropsSchema = z.object({
// Required for Combo // Required for Combo
comboChartAxis: ComboChartAxisSchema comboChartAxis: ComboChartAxisSchema,
}); });
export type ComboChartProps = z.infer<typeof ComboChartPropsSchema>; export type ComboChartProps = z.infer<typeof ComboChartPropsSchema>;

View File

@ -8,5 +8,5 @@ export const DEFAULT_CHART_THEME = [
'#F3864F', '#F3864F',
'#C82184', '#C82184',
'#31FCB4', '#31FCB4',
'#E83562' '#E83562',
]; ];

View File

@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'; import { describe, expect, it } from 'vitest';
import { LineChartPropsSchema, type LineChartProps } from './lineChartProps'; import { type LineChartProps, LineChartPropsSchema } from './lineChartProps';
describe('LineChartPropsSchema', () => { describe('LineChartPropsSchema', () => {
describe('valid inputs', () => { describe('valid inputs', () => {
@ -81,17 +81,17 @@ describe('LineChartPropsSchema', () => {
describe('type inference', () => { describe('type inference', () => {
it('should correctly infer LineChartProps type', () => { it('should correctly infer LineChartProps type', () => {
const validData: LineChartProps = { const validData: LineChartProps = {
lineGroupType: 'stack' lineGroupType: 'stack',
}; };
expect(LineChartPropsSchema.parse(validData)).toEqual(validData); expect(LineChartPropsSchema.parse(validData)).toEqual(validData);
const validDataWithNull: LineChartProps = { const validDataWithNull: LineChartProps = {
lineGroupType: null lineGroupType: null,
}; };
expect(LineChartPropsSchema.parse(validDataWithNull)).toEqual(validDataWithNull); expect(LineChartPropsSchema.parse(validDataWithNull)).toEqual(validDataWithNull);
const validDataWithPercentage: LineChartProps = { const validDataWithPercentage: LineChartProps = {
lineGroupType: 'percentage-stack' lineGroupType: 'percentage-stack',
}; };
expect(LineChartPropsSchema.parse(validDataWithPercentage)).toEqual(validDataWithPercentage); expect(LineChartPropsSchema.parse(validDataWithPercentage)).toEqual(validDataWithPercentage);
}); });

View File

@ -2,7 +2,7 @@ import { z } from 'zod/v4';
export const LineChartPropsSchema = z.object({ 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 %" // 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>; export type LineChartProps = z.infer<typeof LineChartPropsSchema>;

View File

@ -6,7 +6,7 @@ export const DerivedMetricTitleSchema = z.object({
// whether to display to use the key or the value in the chart // whether to display to use the key or the value in the chart
useValue: z.boolean(), useValue: z.boolean(),
// OPTIONAL: default is sum // 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({ export const MetricChartPropsSchema = z.object({
@ -21,7 +21,7 @@ export const MetricChartPropsSchema = z.object({
// OPTIONAL: default is '' // OPTIONAL: default is ''
metricSubHeader: z.nullable(z.union([z.string(), DerivedMetricTitleSchema])).default(null), 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. // 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>; export type DerivedMetricTitle = z.infer<typeof DerivedMetricTitleSchema>;

View File

@ -1,6 +1,6 @@
import { z } from "zod/v4"; import { z } from 'zod/v4';
import { PieChartAxisSchema } from "./axisInterfaces"; import { PieChartAxisSchema } from './axisInterfaces';
import { PieSortBySchema } from "./etcInterfaces"; import { PieSortBySchema } from './etcInterfaces';
export const PieChartPropsSchema = z.object({ export const PieChartPropsSchema = z.object({
// OPTIONAL: default: value // OPTIONAL: default: value
@ -8,30 +8,28 @@ export const PieChartPropsSchema = z.object({
// Required for Pie // Required for Pie
pieChartAxis: PieChartAxisSchema, pieChartAxis: PieChartAxisSchema,
// OPTIONAL: default: number // 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. // OPTIONAL: default true if donut width is set. If the data contains a percentage, set this as false.
pieShowInnerLabel: z.boolean().default(true), pieShowInnerLabel: z.boolean().default(true),
// OPTIONAL: default: sum // OPTIONAL: default: sum
pieInnerLabelAggregate: z pieInnerLabelAggregate: z
.enum(["sum", "average", "median", "max", "min", "count"]) .enum(['sum', 'average', 'median', 'max', 'min', 'count'])
.default("sum"), .default('sum'),
// OPTIONAL: default is null and will be the name of the pieInnerLabelAggregate // OPTIONAL: default is null and will be the name of the pieInnerLabelAggregate
pieInnerLabelTitle: z.string().nullable().default(null), pieInnerLabelTitle: z.string().nullable().default(null),
// OPTIONAL: default: none // OPTIONAL: default: none
pieLabelPosition: z pieLabelPosition: z.nullable(z.enum(['inside', 'outside', 'none'])).default('none'),
.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 // 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 pieDonutWidth: z
.number() .number()
.min(0, "Donut width must be at least 0") .min(0, 'Donut width must be at least 0')
.max(65, "Donut width must be at most 65") .max(65, 'Donut width must be at most 65')
.default(40), .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. // 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 pieMinimumSlicePercentage: z
.number() .number()
.min(0, "Minimum slice percentage must be at least 0") .min(0, 'Minimum slice percentage must be at least 0')
.max(100, "Minimum slice percentage must be at most 100") .max(100, 'Minimum slice percentage must be at most 100')
.default(0), .default(0),
}); });

View File

@ -4,7 +4,7 @@ import { ScatterAxisSchema } from './axisInterfaces';
export const ScatterChartPropsSchema = z.object({ export const ScatterChartPropsSchema = z.object({
// Required for Scatter // Required for Scatter
scatterAxis: ScatterAxisSchema, 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>; export type ScatterChartProps = z.infer<typeof ScatterChartPropsSchema>;

View File

@ -5,7 +5,7 @@ export const TableChartPropsSchema = z.object({
tableColumnWidths: z.nullable(z.record(z.string(), z.number())).default(null), tableColumnWidths: z.nullable(z.record(z.string(), z.number())).default(null),
tableHeaderBackgroundColor: z.nullable(z.string()).default(null), tableHeaderBackgroundColor: z.nullable(z.string()).default(null),
tableHeaderFontColor: 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>; export type TableChartProps = z.infer<typeof TableChartPropsSchema>;

View File

@ -1,12 +1,11 @@
import { import {
getLatestMessageForChat, getLatestMessageForChat,
updateMessageReasoning, updateMessageReasoning,
updateMessageResponseMessages,
updateMessageStreamingFields, updateMessageStreamingFields,
} from '@buster/database/src/helpers/messages'; } from '@buster/database';
import { createTestMessageWithContext } from '@buster/test-utils'; import { createTestMessageWithContext } from '@buster/test-utils';
import { afterEach, beforeEach, describe, expect, test } from 'vitest'; 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', () => { describe('Message Update Helpers', () => {
beforeEach(async () => { beforeEach(async () => {
@ -17,84 +16,6 @@ describe('Message Update Helpers', () => {
await cleanupTestEnvironment(); 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', () => { describe('updateMessageReasoning', () => {
test('successfully updates reasoning JSONB field', async () => { test('successfully updates reasoning JSONB field', async () => {
const { messageId, chatId } = await createTestMessageWithContext(); const { messageId, chatId } = await createTestMessageWithContext();
@ -266,43 +187,4 @@ describe('Message Update Helpers', () => {
).rejects.toThrow(`Message not found or has been deleted: ${nonExistentId}`); ).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()
);
});
});
}); });

View File

@ -1,10 +1,7 @@
import { import { getLatestMessageForChat, updateMessageFields } from '@buster/database';
getLatestMessageForChat,
updateMessageFields,
} from '@buster/database/src/helpers/messages';
import { afterEach, beforeEach, describe, expect, test } from 'vitest'; import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { createTestMessageWithContext } from '../../src'; import { createTestMessageWithContext } from '../../src';
import { cleanupTestEnvironment, setupTestEnvironment } from './helpers'; import { cleanupTestEnvironment, setupTestEnvironment } from '../../src/envHelpers/env-helpers';
describe('updateMessageFields', () => { describe('updateMessageFields', () => {
beforeEach(async () => { beforeEach(async () => {