Files
FastGPT/test/cases/service/core/chat/controller.test.ts
T
Archer c93c3937e1 S3 sdk (#6215)
* refactor: fastgpt object storage & global proxy (#6155)

* feat: migrate to fastgpt storage sdk

* chore: rename env variable

* chore: move to sdk dir

* docs: object storage

* CHORE

* chore: storage mocks

* chore: update docker-compose

* fix: global proxy agent

* fix: update COS proxy

* refactor: use fetch instead of http.request

* fix: axios request base url

* fix: axios proxy request behavior

* fix: bumps axios

* fix: patch axios for proxy

* fix: replace axios with proxied axios

* fix: upload txt file encoding

* clean code

* fix: use "minio" for minio adapter (#6205)

* fix: use minio client to delete files when using minio vendor (#6206)

* doc

* feat: filter citations and add response button control (#6170)

* feat: filter citations and add response button control

* i18n

* fix

* fix test

* perf: chat api code

* fix: workflow edge overlap and auto-align in folded loop nodes (#6204)

* fix: workflow edge overlap and auto-align in folded loop nodes

* sort

* fix

* fix edge

* fix icon

* perf: s3 file name

* perf: admin get app api

* perf: catch user error

* fix: refactor useOrg hook to use debounced search key (#6180)

* chore: comment minio adapter (#6207)

* chore: filename with suffix random id

* perf: s3 storage code

* fix: encode filename when copy object

---------

Co-authored-by: archer <545436317@qq.com>

* fix: node card link

* json

* perf: chat index;

* index

* chat item soft delete (#6216)

* chat item soft delete

* temp

* fix

* remove code

* perf: delete chat item

---------

Co-authored-by: archer <545436317@qq.com>

* feat: select wheather filter sensitive info when export apps (#6222)

* fix some bugs (#6210)

* fix v4.14.5 bugs

* type

* fix

* fix

* custom feedback

* fix

* code

* fix

* remove invalid function

---------

Co-authored-by: archer <545436317@qq.com>

* perf: test

* fix file default local upload (#6223)

* docs: improve object storage introduction (#6224)

* doc

---------

Co-authored-by: roy <whoeverimf5@gmail.com>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
2026-01-09 18:25:02 +08:00

1044 lines
31 KiB
TypeScript

import { describe, expect, it, beforeEach } from 'vitest';
import { getChatItems, updateChatFeedbackCount } from '@fastgpt/service/core/chat/controller';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { getUser } from '@test/datas/users';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import type { ChatItemSchema } from '@fastgpt/global/core/chat/type';
describe('getChatItems', () => {
let testUser: Awaited<ReturnType<typeof getUser>>;
let appId: string;
let chatId: string;
beforeEach(async () => {
testUser = await getUser('test-user');
// Create test app
const app = await MongoApp.create({
name: 'Test App',
type: AppTypeEnum.simple,
teamId: testUser.teamId,
tmbId: testUser.tmbId,
modules: []
});
appId = String(app._id);
chatId = getNanoid();
});
// Helper function to create chat items
const createChatItems = async (count: number): Promise<ChatItemSchema[]> => {
const items: ChatItemSchema[] = [];
for (let i = 0; i < count; i++) {
const item = await MongoChatItem.create({
teamId: testUser.teamId,
tmbId: testUser.tmbId,
userId: testUser.userId,
appId,
chatId,
dataId: getNanoid(),
obj: i % 2 === 0 ? ChatRoleEnum.Human : ChatRoleEnum.AI,
value: [
{
type: 'text',
text: {
content: `Message ${i + 1}`
}
}
]
});
items.push(item.toObject() as ChatItemSchema);
}
return items;
};
describe('Normal Pagination Mode', () => {
it('should return empty array when chatId is not provided', async () => {
const result = await getChatItems({
appId,
chatId: undefined,
offset: 0,
limit: 10,
field: 'obj value'
});
expect(result.histories).toEqual([]);
expect(result.total).toBe(0);
});
it('should return empty array when no chat items exist', async () => {
const result = await getChatItems({
appId,
chatId,
offset: 0,
limit: 10,
field: 'obj value'
});
expect(result.histories).toEqual([]);
expect(result.total).toBe(0);
});
it('should fetch chat items with pagination correctly', async () => {
await createChatItems(20);
const result = await getChatItems({
appId,
chatId,
offset: 0,
limit: 5,
field: 'obj value'
});
expect(result.histories).toHaveLength(5);
expect(result.total).toBe(20);
// Should be in chronological order (oldest first)
expect(result.histories[0].value[0].text?.content).toContain('Message 1');
});
it('should handle pagination offset correctly', async () => {
await createChatItems(20);
const result = await getChatItems({
appId,
chatId,
offset: 5,
limit: 5,
field: 'obj value'
});
expect(result.histories).toHaveLength(5);
expect(result.total).toBe(20);
// The function gets items in reverse order then reverses them back
// So offset 5 should skip the first 5 newest items and get items 6-10 (from newest)
// After reversing, these would be items 11-15 from oldest (Message 11-15)
expect(result.histories[0].value[0].text?.content).toContain('Message 11');
});
it('should return remaining items when limit exceeds available items', async () => {
await createChatItems(5);
const result = await getChatItems({
appId,
chatId,
offset: 0,
limit: 10,
field: 'obj value'
});
expect(result.histories).toHaveLength(5);
expect(result.total).toBe(5);
});
it('should only return specified fields', async () => {
await createChatItems(5);
const result = await getChatItems({
appId,
chatId,
offset: 0,
limit: 5,
field: 'obj'
});
expect(result.histories).toHaveLength(5);
// Should have dataId (always included) and obj
expect(result.histories[0].dataId).toBeDefined();
expect(result.histories[0].obj).toBeDefined();
// Should not have other optional fields
expect(result.histories[0].value).toBeUndefined();
});
});
describe('Field Selection', () => {
it('should always include dataId field even if not specified', async () => {
await createChatItems(3);
const result = await getChatItems({
appId,
chatId,
offset: 0,
limit: 3,
field: 'obj value'
});
expect(result.histories).toHaveLength(3);
result.histories.forEach((item) => {
expect(item.dataId).toBeDefined();
expect(typeof item.dataId).toBe('string');
});
});
it('should include custom fields when specified', async () => {
// Create AI items to support customFeedbacks
const aiItem = await MongoChatItem.create({
teamId: testUser.teamId,
tmbId: testUser.tmbId,
userId: testUser.userId,
appId,
chatId,
dataId: getNanoid(),
obj: ChatRoleEnum.AI,
value: [{ type: 'text', text: { content: 'AI response' } }],
customFeedbacks: ['good', 'helpful']
});
const result = await getChatItems({
appId,
chatId,
offset: 0,
limit: 3,
field: 'obj value customFeedbacks'
});
const aiHistory = result.histories.find((h) => h.obj === ChatRoleEnum.AI);
expect(aiHistory).toBeDefined();
// Type assertion to access customFeedbacks on AI item
if (aiHistory && aiHistory.obj === ChatRoleEnum.AI) {
expect(aiHistory.customFeedbacks).toEqual(['good', 'helpful']);
}
});
});
describe('Edge Cases', () => {
it('should handle single item correctly', async () => {
await createChatItems(1);
const result = await getChatItems({
appId,
chatId,
offset: 0,
limit: 10,
field: 'obj value'
});
expect(result.histories).toHaveLength(1);
expect(result.total).toBe(1);
});
it('should handle offset beyond total items', async () => {
await createChatItems(5);
const result = await getChatItems({
appId,
chatId,
offset: 10,
limit: 5,
field: 'obj value'
});
expect(result.histories).toHaveLength(0);
expect(result.total).toBe(5);
});
it('should handle zero limit gracefully', async () => {
await createChatItems(5);
const result = await getChatItems({
appId,
chatId,
offset: 0,
limit: 0,
field: 'obj value'
});
// MongoDB's limit(0) returns all documents, so we should get all 5
expect(result.histories).toHaveLength(5);
expect(result.total).toBe(5);
});
it('should filter by appId and chatId correctly', async () => {
const otherChatId = getNanoid();
// Create items in target chat
await createChatItems(5);
// Create items in another chat
await MongoChatItem.create({
teamId: testUser.teamId,
tmbId: testUser.tmbId,
appId,
chatId: otherChatId,
dataId: getNanoid(),
obj: ChatRoleEnum.Human,
value: [{ type: 'text', text: { content: 'Other chat' } }]
});
const result = await getChatItems({
appId,
chatId,
offset: 0,
limit: 10,
field: 'obj value'
});
// Should only return items from target chat
expect(result.histories).toHaveLength(5);
expect(result.total).toBe(5);
});
});
describe('Order Verification', () => {
it('should return items in chronological order (oldest first)', async () => {
const items = await createChatItems(10);
const result = await getChatItems({
appId,
chatId,
offset: 0,
limit: 10,
field: 'obj value'
});
// Verify order by comparing dataId with original items
expect(result.histories).toHaveLength(10);
result.histories.forEach((history, index) => {
expect(history.dataId).toBe(items[index].dataId);
});
});
});
describe('initialId Mode - Get items around target', () => {
it('should return items around the target item when initialId is provided', async () => {
const items = await createChatItems(10);
const targetItem = items[4]; // Middle item
const result = await getChatItems({
appId,
chatId,
initialId: targetItem.dataId,
limit: 5,
field: 'obj value'
});
// With limit 5: halfLimit=2, ceilLimit=3
// Returns 2 items before + target + 3 items after = 6 items
expect(result.histories).toHaveLength(6);
expect(result.histories[2].dataId).toBe(targetItem.dataId);
expect(result.total).toBe(10);
});
it('should handle initialId at the beginning of chat history', async () => {
const items = await createChatItems(10);
const firstItem = items[0];
const result = await getChatItems({
appId,
chatId,
initialId: firstItem.dataId,
limit: 5,
field: 'obj value'
});
// Should get first item and 4 items after it
expect(result.histories[0].dataId).toBe(firstItem.dataId);
expect(result.hasMorePrev).toBe(false);
expect(result.hasMoreNext).toBe(true);
});
it('should handle initialId at the end of chat history', async () => {
const items = await createChatItems(10);
const lastItem = items[9];
const result = await getChatItems({
appId,
chatId,
initialId: lastItem.dataId,
limit: 5,
field: 'obj value'
});
// Should get last item and items before it
expect(result.histories[result.histories.length - 1].dataId).toBe(lastItem.dataId);
expect(result.hasMorePrev).toBe(true);
expect(result.hasMoreNext).toBe(false);
});
it('should set hasMorePrev and hasMoreNext correctly with initialId', async () => {
const items = await createChatItems(20);
const middleItem = items[10];
const result = await getChatItems({
appId,
chatId,
initialId: middleItem.dataId,
limit: 5,
field: 'obj value'
});
expect(result.hasMorePrev).toBe(true);
expect(result.hasMoreNext).toBe(true);
expect(result.total).toBe(20);
});
it('should throw error when initialId does not exist', async () => {
await createChatItems(5);
await expect(
getChatItems({
appId,
chatId,
initialId: 'non-existent-id',
limit: 5,
field: 'obj value'
})
).rejects.toThrow('Target item not found');
});
it('should handle small limit with initialId', async () => {
const items = await createChatItems(10);
const middleItem = items[5];
const result = await getChatItems({
appId,
chatId,
initialId: middleItem.dataId,
limit: 3,
field: 'obj value'
});
// With limit 3: halfLimit=1, ceilLimit=2
// Returns 1 item before + target + 2 items after = 4 items
expect(result.histories).toHaveLength(4);
expect(result.histories[1].dataId).toBe(middleItem.dataId);
});
it('should handle odd limit with initialId', async () => {
const items = await createChatItems(15);
const middleItem = items[7];
const result = await getChatItems({
appId,
chatId,
initialId: middleItem.dataId,
limit: 7,
field: 'obj value'
});
// With limit 7: halfLimit=3, ceilLimit=4
// Returns 3 items before + target + 4 items after = 8 items
expect(result.histories).toHaveLength(8);
expect(result.histories[3].dataId).toBe(middleItem.dataId);
});
it('should handle even limit with initialId', async () => {
const items = await createChatItems(15);
const middleItem = items[7];
const result = await getChatItems({
appId,
chatId,
initialId: middleItem.dataId,
limit: 6,
field: 'obj value'
});
// With limit 6: halfLimit=3, ceilLimit=3
// Returns 3 items before + target + 3 items after = 7 items
expect(result.histories).toHaveLength(7);
expect(result.histories[3].dataId).toBe(middleItem.dataId);
});
it('should return latest items when no initialId provided', async () => {
const items = await createChatItems(20);
const result = await getChatItems({
appId,
chatId,
limit: 5,
field: 'obj value'
});
// Should return the 5 latest items (items 16-20)
expect(result.histories).toHaveLength(5);
expect(result.histories[0].dataId).toBe(items[15].dataId);
expect(result.hasMorePrev).toBe(true);
expect(result.hasMoreNext).toBe(false);
});
});
describe('prevId Mode - Get items before target', () => {
it('should return items before the target item when prevId is provided', async () => {
const items = await createChatItems(10);
const targetItem = items[5];
const result = await getChatItems({
appId,
chatId,
prevId: targetItem.dataId,
limit: 3,
field: 'obj value'
});
// Should return 3 items before the target (items 2, 3, 4)
expect(result.histories).toHaveLength(3);
expect(result.histories[0].dataId).toBe(items[2].dataId);
expect(result.histories[2].dataId).toBe(items[4].dataId);
expect(result.hasMoreNext).toBe(true); // Target item and items after exist
expect(result.total).toBe(10);
});
it('should set hasMorePrev correctly with prevId', async () => {
const items = await createChatItems(20);
const targetItem = items[15];
const result = await getChatItems({
appId,
chatId,
prevId: targetItem.dataId,
limit: 5,
field: 'obj value'
});
// Should return 5 items before item 15 (items 10-14)
expect(result.histories).toHaveLength(5);
expect(result.hasMorePrev).toBe(true); // Items 0-9 still exist
expect(result.hasMoreNext).toBe(true); // Target and items after exist
});
it('should handle prevId at the beginning of chat history', async () => {
const items = await createChatItems(10);
const earlyItem = items[2];
const result = await getChatItems({
appId,
chatId,
prevId: earlyItem.dataId,
limit: 5,
field: 'obj value'
});
// Should only return 2 items (items 0 and 1)
expect(result.histories).toHaveLength(2);
expect(result.histories[0].dataId).toBe(items[0].dataId);
expect(result.hasMorePrev).toBe(false);
expect(result.hasMoreNext).toBe(true);
});
it('should throw error when prevId does not exist', async () => {
await createChatItems(5);
await expect(
getChatItems({
appId,
chatId,
prevId: 'non-existent-id',
limit: 5,
field: 'obj value'
})
).rejects.toThrow('Prev item not found');
});
it('should return empty array when prevId is the first item', async () => {
const items = await createChatItems(10);
const firstItem = items[0];
const result = await getChatItems({
appId,
chatId,
prevId: firstItem.dataId,
limit: 5,
field: 'obj value'
});
// No items before the first item
expect(result.histories).toHaveLength(0);
expect(result.hasMorePrev).toBe(false);
expect(result.hasMoreNext).toBe(true);
});
it('should maintain chronological order with prevId', async () => {
const items = await createChatItems(10);
const targetItem = items[7];
const result = await getChatItems({
appId,
chatId,
prevId: targetItem.dataId,
limit: 4,
field: 'obj value'
});
// Should return items 3, 4, 5, 6 in order
expect(result.histories).toHaveLength(4);
for (let i = 0; i < result.histories.length; i++) {
expect(result.histories[i].dataId).toBe(items[3 + i].dataId);
}
});
});
describe('nextId Mode - Get items after target', () => {
it('should return items after the target item when nextId is provided', async () => {
const items = await createChatItems(10);
const targetItem = items[4];
const result = await getChatItems({
appId,
chatId,
nextId: targetItem.dataId,
limit: 3,
field: 'obj value'
});
// Should return 3 items after the target (items 5, 6, 7)
expect(result.histories).toHaveLength(3);
expect(result.histories[0].dataId).toBe(items[5].dataId);
expect(result.histories[2].dataId).toBe(items[7].dataId);
expect(result.hasMorePrev).toBe(true); // Target item and items before exist
expect(result.total).toBe(10);
});
it('should set hasMoreNext correctly with nextId', async () => {
const items = await createChatItems(20);
const targetItem = items[5];
const result = await getChatItems({
appId,
chatId,
nextId: targetItem.dataId,
limit: 5,
field: 'obj value'
});
// Should return 5 items after item 5 (items 6-10)
expect(result.histories).toHaveLength(5);
expect(result.hasMorePrev).toBe(true); // Target and items before exist
expect(result.hasMoreNext).toBe(true); // Items 11-19 still exist
});
it('should handle nextId at the end of chat history', async () => {
const items = await createChatItems(10);
const lateItem = items[7];
const result = await getChatItems({
appId,
chatId,
nextId: lateItem.dataId,
limit: 5,
field: 'obj value'
});
// Should only return 2 items (items 8 and 9)
expect(result.histories).toHaveLength(2);
expect(result.histories[0].dataId).toBe(items[8].dataId);
expect(result.hasMorePrev).toBe(true);
expect(result.hasMoreNext).toBe(false);
});
it('should throw error when nextId does not exist', async () => {
await createChatItems(5);
await expect(
getChatItems({
appId,
chatId,
nextId: 'non-existent-id',
limit: 5,
field: 'obj value'
})
).rejects.toThrow('Next item not found');
});
it('should return empty array when nextId is the last item', async () => {
const items = await createChatItems(10);
const lastItem = items[9];
const result = await getChatItems({
appId,
chatId,
nextId: lastItem.dataId,
limit: 5,
field: 'obj value'
});
// No items after the last item
expect(result.histories).toHaveLength(0);
expect(result.hasMorePrev).toBe(true);
expect(result.hasMoreNext).toBe(false);
});
it('should maintain chronological order with nextId', async () => {
const items = await createChatItems(10);
const targetItem = items[2];
const result = await getChatItems({
appId,
chatId,
nextId: targetItem.dataId,
limit: 4,
field: 'obj value'
});
// Should return items 3, 4, 5, 6 in order
expect(result.histories).toHaveLength(4);
for (let i = 0; i < result.histories.length; i++) {
expect(result.histories[i].dataId).toBe(items[3 + i].dataId);
}
});
});
describe('Pagination Mode Priorities', () => {
it('should use offset mode when offset is provided with other params', async () => {
const items = await createChatItems(10);
const result = await getChatItems({
appId,
chatId,
offset: 0,
initialId: items[5].dataId,
limit: 5,
field: 'obj value'
});
// Offset mode should take precedence - returns latest items
expect(result.histories).toHaveLength(5);
expect(result.hasMoreNext).toBe(false); // Offset mode starts from newest
});
it('should use prevId mode when both prevId and nextId are provided', async () => {
const items = await createChatItems(10);
const result = await getChatItems({
appId,
chatId,
prevId: items[5].dataId,
nextId: items[7].dataId,
limit: 3,
field: 'obj value'
});
// prevId mode should take precedence (checked before nextId in code)
expect(result.hasMoreNext).toBe(true);
// Should return items before items[5]
expect(result.histories.every((h) => h.dataId !== items[5].dataId)).toBe(true);
});
});
});
describe('updateChatFeedbackCount', () => {
let testUser: Awaited<ReturnType<typeof getUser>>;
let appId: string;
let chatId: string;
beforeEach(async () => {
testUser = await getUser('test-user-feedback-count');
// Create test app
const app = await MongoApp.create({
name: 'Test App',
type: AppTypeEnum.simple,
teamId: testUser.teamId,
tmbId: testUser.tmbId,
modules: []
});
appId = String(app._id);
chatId = getNanoid();
// Create chat record
await MongoChat.create({
chatId,
teamId: testUser.teamId,
tmbId: testUser.tmbId,
appId,
source: ChatSourceEnum.online
});
});
// Helper function to create chat item with feedback
const createChatItemWithFeedback = async (
feedback: {
userGoodFeedback?: string;
userBadFeedback?: string;
isFeedbackRead?: boolean;
},
obj: ChatRoleEnum = ChatRoleEnum.AI
) => {
return await MongoChatItem.create({
teamId: testUser.teamId,
tmbId: testUser.tmbId,
userId: testUser.userId,
appId,
chatId,
dataId: getNanoid(),
obj,
value: [{ type: 'text', text: { content: 'Test message' } }],
...feedback
});
};
it('should not set feedback flags when no feedback exists', async () => {
// Create AI items without feedback
await createChatItemWithFeedback({}, ChatRoleEnum.AI);
await createChatItemWithFeedback({}, ChatRoleEnum.AI);
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBeUndefined();
expect(chat?.hasBadFeedback).toBeUndefined();
expect(chat?.hasUnreadGoodFeedback).toBeUndefined();
expect(chat?.hasUnreadBadFeedback).toBeUndefined();
});
it('should set hasGoodFeedback to true when good feedback exists', async () => {
await createChatItemWithFeedback({
userGoodFeedback: 'Great response!'
});
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBe(true);
expect(chat?.hasBadFeedback).toBeUndefined();
});
it('should set hasBadFeedback to true when bad feedback exists', async () => {
await createChatItemWithFeedback({
userBadFeedback: 'Incorrect answer'
});
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBeUndefined();
expect(chat?.hasBadFeedback).toBe(true);
});
it('should set both feedback flags when both types exist', async () => {
await createChatItemWithFeedback({
userGoodFeedback: 'Great response!'
});
await createChatItemWithFeedback({
userBadFeedback: 'Incorrect answer'
});
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBe(true);
expect(chat?.hasBadFeedback).toBe(true);
});
it('should set hasUnreadGoodFeedback when good feedback is unread', async () => {
await createChatItemWithFeedback({
userGoodFeedback: 'Great response!',
isFeedbackRead: false
});
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBe(true);
expect(chat?.hasUnreadGoodFeedback).toBe(true);
});
it('should not set hasUnreadGoodFeedback when good feedback is read', async () => {
await createChatItemWithFeedback({
userGoodFeedback: 'Great response!',
isFeedbackRead: true
});
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBe(true);
expect(chat?.hasUnreadGoodFeedback).toBeUndefined();
});
it('should set hasUnreadBadFeedback when bad feedback is unread', async () => {
await createChatItemWithFeedback({
userBadFeedback: 'Incorrect answer',
isFeedbackRead: false
});
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasBadFeedback).toBe(true);
expect(chat?.hasUnreadBadFeedback).toBe(true);
});
it('should not set hasUnreadBadFeedback when bad feedback is read', async () => {
await createChatItemWithFeedback({
userBadFeedback: 'Incorrect answer',
isFeedbackRead: true
});
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasBadFeedback).toBe(true);
expect(chat?.hasUnreadBadFeedback).toBeUndefined();
});
it('should handle mixed read/unread feedback correctly', async () => {
// Unread good feedback
await createChatItemWithFeedback({
userGoodFeedback: 'Great!',
isFeedbackRead: false
});
// Read good feedback
await createChatItemWithFeedback({
userGoodFeedback: 'Nice!',
isFeedbackRead: true
});
// Unread bad feedback
await createChatItemWithFeedback({
userBadFeedback: 'Wrong',
isFeedbackRead: false
});
// Read bad feedback
await createChatItemWithFeedback({
userBadFeedback: 'Incorrect',
isFeedbackRead: true
});
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBe(true);
expect(chat?.hasBadFeedback).toBe(true);
expect(chat?.hasUnreadGoodFeedback).toBe(true);
expect(chat?.hasUnreadBadFeedback).toBe(true);
});
it('should only count AI messages, not Human messages', async () => {
// Human message with feedback (should be ignored)
await createChatItemWithFeedback(
{
userGoodFeedback: 'Great!'
},
ChatRoleEnum.Human
);
// AI message without feedback
await createChatItemWithFeedback({}, ChatRoleEnum.AI);
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBeUndefined();
expect(chat?.hasBadFeedback).toBeUndefined();
});
it('should handle multiple feedbacks of the same type', async () => {
// Create 3 good feedbacks
await createChatItemWithFeedback({
userGoodFeedback: 'Great response 1!'
});
await createChatItemWithFeedback({
userGoodFeedback: 'Great response 2!'
});
await createChatItemWithFeedback({
userGoodFeedback: 'Great response 3!'
});
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBe(true);
expect(chat?.hasBadFeedback).toBeUndefined();
});
it('should update flags correctly when feedback is removed', async () => {
// Create item with good feedback
const item = await createChatItemWithFeedback({
userGoodFeedback: 'Great response!'
});
await updateChatFeedbackCount({ appId, chatId });
let chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBe(true);
// Remove feedback
await MongoChatItem.updateOne({ _id: item._id }, { $unset: { userGoodFeedback: '' } });
await updateChatFeedbackCount({ appId, chatId });
chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBeUndefined();
});
it('should handle chat with no AI messages', async () => {
// Create only human messages
await createChatItemWithFeedback({}, ChatRoleEnum.Human);
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBeUndefined();
expect(chat?.hasBadFeedback).toBeUndefined();
expect(chat?.hasUnreadGoodFeedback).toBeUndefined();
expect(chat?.hasUnreadBadFeedback).toBeUndefined();
});
it('should handle isFeedbackRead undefined as unread', async () => {
// When isFeedbackRead is undefined, it should be treated as unread
await createChatItemWithFeedback({
userGoodFeedback: 'Great response!'
// isFeedbackRead is undefined
});
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBe(true);
expect(chat?.hasUnreadGoodFeedback).toBe(true);
});
it('should correctly aggregate large number of feedbacks', async () => {
// Create 10 good feedbacks (5 unread, 5 read)
for (let i = 0; i < 10; i++) {
await createChatItemWithFeedback({
userGoodFeedback: `Good ${i}`,
isFeedbackRead: i >= 5
});
}
// Create 8 bad feedbacks (3 unread, 5 read)
for (let i = 0; i < 8; i++) {
await createChatItemWithFeedback({
userBadFeedback: `Bad ${i}`,
isFeedbackRead: i >= 3
});
}
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBe(true);
expect(chat?.hasBadFeedback).toBe(true);
expect(chat?.hasUnreadGoodFeedback).toBe(true);
expect(chat?.hasUnreadBadFeedback).toBe(true);
});
it('should work correctly within a transaction session', async () => {
await createChatItemWithFeedback({
userGoodFeedback: 'Great response!'
});
// Test that it works with session parameter (session will be undefined in this test)
await updateChatFeedbackCount({ appId, chatId, session: undefined });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
expect(chat?.hasGoodFeedback).toBe(true);
});
it('should handle edge case with empty feedback strings', async () => {
// Create items with empty strings
await createChatItemWithFeedback({
userGoodFeedback: ''
});
await updateChatFeedbackCount({ appId, chatId });
const chat = await MongoChat.findOne({ appId, chatId }).lean();
// Empty string is still truthy in MongoDB's $ifNull check, so it counts as feedback
expect(chat?.hasGoodFeedback).toBe(true);
});
});