Files
FastGPT/test/cases/service/core/chat/saveChat.test.ts
T
Archer 76d6234de6 V4.14.7 features (#6406)
* Agent features (#6345)

* Test agent (#6220)

* squash: compress all commits into one

* feat: plan response in ui

* response ui

* perf: agent config

* merge

* tool select ux

* perf: chat ui

* perf: agent editform

* tmp code

* feat: save chat

* Complete agent parent  (#6049)

* add role and tools filling

* add: file-upload

---------

Co-authored-by: xxyyh <2289112474@qq>

* perf: top agent code

* top agent (#6062)

Co-authored-by: xxyyh <2289112474@qq>

* fix: ts

* skill editor ui

* ui

* perf: rewrite type with zod

* skill edit ui

* skill agent (#6089)

* cp skill chat

* rebase fdf933d
 and add skill chat

* 1. skill 的 CRUD
2. skill 的信息渲染到前端界面

* solve comment

* remove chatid and chatItemId

* skill match

* perf: skill manage

* fix: ts

---------

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

* fix: ts

* fix: loop import

* skill tool config (#6114)

Co-authored-by: xxyyh <2289112474@qq>

* feat: load tool in agent

* skill memory (#6126)

Co-authored-by: xxyyh <2289112474@qq>

* perf: agent skill editor

* perf: helperbot ui

* agent code

* perf: context

* fix: request context

* agent usage

* perf: agent context and pause

* perf: plan response

* Test agent sigle skill (#6184)

* feat:top box fill

* prompt fix

---------

Co-authored-by: xxyyh <2289112474@qq>

* perf: agent chat ui

* Test agent new (#6219)

* have-replan

* agent

---------

Co-authored-by: xxyyh <2289112474@qq>

* fix: ts

---------

Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com>
Co-authored-by: xxyyh <2289112474@qq>

* feat: consolidate agent and MCP improvements

This commit consolidates 17 commits including:
- MCP tools enhancements and fixes
- Agent system improvements and optimizations
- Auth limit and prompt updates
- Tool response compression and error tracking
- Simple app adaptation
- Code quality improvements (TypeScript, ESLint, Zod)
- Version type migration to schema
- Remove deprecated useRequest2
- Add LLM error tracking
- Toolset ID validation fixes

---------

Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com>
Co-authored-by: xxyyh <2289112474@qq>

* fix: transform avatar copy;perf: filter invalid tool

* update llm response storage time

* fix: openapi schema

* update skill desc

* feat: cache hit data

* i18n

* lock

* chat logs support error filter & user search (#6373)

* chat log support searching by user name

* support error filter

* fix

* fix overflow

* optimize

* fix init script

* fix

* perf: get log users

* updat ecomment

* fix: ts

* fix: test

---------

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

* Fix: agent  (#6376)

* Agent features (#6345)

* Test agent (#6220)

* squash: compress all commits into one

* feat: plan response in ui

* response ui

* perf: agent config

* merge

* tool select ux

* perf: chat ui

* perf: agent editform

* tmp code

* feat: save chat

* Complete agent parent  (#6049)

* add role and tools filling

* add: file-upload

---------

Co-authored-by: xxyyh <2289112474@qq>

* perf: top agent code

* top agent (#6062)

Co-authored-by: xxyyh <2289112474@qq>

* fix: ts

* skill editor ui

* ui

* perf: rewrite type with zod

* skill edit ui

* skill agent (#6089)

* cp skill chat

* rebase fdf933d
 and add skill chat

* 1. skill 的 CRUD
2. skill 的信息渲染到前端界面

* solve comment

* remove chatid and chatItemId

* skill match

* perf: skill manage

* fix: ts

---------

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

* fix: ts

* fix: loop import

* skill tool config (#6114)

Co-authored-by: xxyyh <2289112474@qq>

* feat: load tool in agent

* skill memory (#6126)

Co-authored-by: xxyyh <2289112474@qq>

* perf: agent skill editor

* perf: helperbot ui

* agent code

* perf: context

* fix: request context

* agent usage

* perf: agent context and pause

* perf: plan response

* Test agent sigle skill (#6184)

* feat:top box fill

* prompt fix

---------

Co-authored-by: xxyyh <2289112474@qq>

* perf: agent chat ui

* Test agent new (#6219)

* have-replan

* agent

---------

Co-authored-by: xxyyh <2289112474@qq>

* fix: ts

---------

Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com>
Co-authored-by: xxyyh <2289112474@qq>

* feat: consolidate agent and MCP improvements

This commit consolidates 17 commits including:
- MCP tools enhancements and fixes
- Agent system improvements and optimizations
- Auth limit and prompt updates
- Tool response compression and error tracking
- Simple app adaptation
- Code quality improvements (TypeScript, ESLint, Zod)
- Version type migration to schema
- Remove deprecated useRequest2
- Add LLM error tracking
- Toolset ID validation fixes

---------

Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com>
Co-authored-by: xxyyh <2289112474@qq>

* 1. 把辅助生成前端上的 system prompt 加入到上下文中
2. mcp工具的前端渲染(图标)
3. 文件读取工具和文件上传进行关联
4. 添加了辅助生成返回格式出错的重试方案
5. ask 不出现在 plan 步骤中
6. 添加了辅助生成的头像和交互 UI

* fix:read_file

* helperbot ui

* ts error

* helper ui

* delete Unused import

* perf: helper bot

* lock

---------

Co-authored-by: Archer <545436317@qq.com>
Co-authored-by: xxyyh <2289112474@qq>

* fix date variable required & model auth (#6386)

* fix date variable required & model auth

* doc

* feat: add chat id to finish callback

* fix: iphone safari shareId (#6387)

* fix: iphone safari shareId

* fix: mcp file list can't setting

* fix: reason output field

* fix: skip JSON validation for HTTP tool body with variable (#6392)

* fix: skip JSON validation for HTTP tool body with variable

* doc

* workflow fitview

* perf: selecting memory

* perf: cp api

* ui

* perf: toolcall auto adapt

* fix: catch workflow error

* fix: ts

* perf: pagination type

* remove

* ignore

* update doc

* fix: simple app tool select

* add default avatar to logs user

* perf: loading user

* select dataset ui

* rename version

* feat: add global/common test

* perf: packages/global/common test

* feat: package/global/ai,app test

* add global/chat test

* global/core test

* global/core test

* feat: packages/global all test

* perf: test

* add server api test

* perf: init shell

* perf: init4150 shell

* remove invalid code

* update doc

* remove log

* fix: chat effect

* fix: plan fake tool  (#6398)

* 1. 提示词防注入功能
2. 无工具不进入 plan,防止虚拟工具生成

* Agent-dataset

* dataset

* dataset presetInfo

* prefix

* perf: prompt

---------

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

* fix: review

* adapt kimi2.5 think toolcall

* feat: invoke fastgpt user info (#6403)

feat: invoke fastgpt user info

* fix: invoke fastgpt user info return orgs (#6404)

* skill and version

* retry helperbot (#6405)

Co-authored-by: xxyyh <2289112474@qq>

* update template

* remove log

* doc

* update doc

* doc

* perf: internal ip check

* adapt get paginationRecords

* tool call adapt

* fix: test

* doc

* fix: agent initial version

* adapt completions v1

* feat: instrumentation check

* rename skill

* add workflow demo mode tracks (#6407)

* chore: 统一 skills 目录命名为小写

将 .claude/Skills/ 重命名为 .claude/skills/ 以保持命名一致性。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* add workflow demo mode tracks

* code

* optimize

* fix: improve workflowDemoTrack based on PR review

- Add comment to empty catch block for maintainability
- Add @param docs to onDemoChange clarifying nodeCount usage
- Replace silent .catch with console.debug for dev debugging
- Handle appId changes by reporting old data before re-init

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: archer <545436317@qq.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* remove repeat skill

* fix(workflow): filter out orphan edges to prevent runtime errors (#6399)

* fix(workflow): filter out orphan edges to prevent runtime errors

Runtime edges that reference non-existent nodes (orphan edges) can cause
unexpected behavior or crashes during workflow dispatch. This change adds
a pre-check to filter out such edges before execution begins, ensuring
system stability even with inconsistent graph data.

* fix(workflow): enhance orphan edge filtering with logging and tests

- Refactor: Extract logic to 'filterOrphanEdges' in utils.ts for better reusability
- Feat: Add performance monitoring (warn if >100ms) and comprehensive logging
- Feat: Support detailed edge inspection in debug mode
- Docs: Add JSDoc explaining causes of orphan edges (migration, manual edits)
- Test: Add unit tests covering edge cases and performance (1000 edges)

Addresses PR review feedback regarding logging, variable naming, and testing."

* move code

* move code

* add more unit test

---------

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

* test

* perf: test

* add server/common/string test

* fix: resolve $ref references in MCP tool input schemas (#6395) (#6409)

* fix: resolve $ref references in MCP tool input schemas (#6395)

* add test code

---------

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

* chore(docs): add fastgpt, fastgpt-plugin version choice guide (#6411)

* chore(doc): add fastgpt version description

* doc

* doc

---------

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

* fix:dataset cite and description info (#6410)

* 1. 添加知识库引用(plan 步骤和直接知识库调用)
2. 提示词框中的@知识库工具
3. plan 中 step 的 description dataset_search 改为中文

* fix: i18n

* prompt

* prompt

---------

Co-authored-by: xxyyh <2289112474@qq>

* fix: tool call

* perf: workflow props

* fix: merge ECharts toolbox options instead of overwriting (#6269) (#6412)

* feat: integrate logtape and otel (#6400)

* fix: deps

* feat(logger): integrate logtape and otel

* wip(log): add basic infras logs

* wip(log): add request id and inject it into context

* wip(log): add basic tx logs

* wip(log): migrate

* wip(log): category

* wip(log): more sub category

* fix: type

* fix: sessionRun

* fix: export getLogger from client.ts

* chore: improve logs

* docs: update signoz and changelog

* change type

* fix: ts

* remove skill.md

* fix: lockfile specifier

* fix: test

---------

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

* init log

* doc

* remove invalid log

* fix: review

* template

* replace new log

* fix: ts

* remove log

* chore: migrate all addLog to logtape

* move skill

* chore: migrate all addLog to logtape (#6417)

* update skill

* remove log

* fix: tool check

---------

Co-authored-by: YeYuheng <57035043+YYH211@users.noreply.github.com>
Co-authored-by: xxyyh <2289112474@qq>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: xuyafei1996 <54217479+xuyafei1996@users.noreply.github.com>
Co-authored-by: ToukoYui <2331631097@qq.com>
Co-authored-by: roy <whoeverimf5@gmail.com>
2026-02-12 16:37:50 +08:00

1021 lines
29 KiB
TypeScript

import { describe, expect, it, beforeEach } from 'vitest';
import {
type Props,
pushChatRecords,
updateInteractiveChat
} from '@fastgpt/service/core/chat/saveChat';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { MongoAppChatLog } from '@fastgpt/service/core/app/logs/chatLogsSchema';
import { MongoChatItemResponse } from '@fastgpt/service/core/chat/chatItemResponseSchema';
import { ChatFileTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import {
FlowNodeTypeEnum,
FlowNodeInputTypeEnum
} from '@fastgpt/global/core/workflow/node/constant';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
import { MongoTeam } from '@fastgpt/service/support/user/team/teamSchema';
import { MongoUser } from '@fastgpt/service/support/user/schema';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
const createMockProps = (
overrides?: Partial<Props>,
ids?: { appId?: string; teamId?: string; tmbId?: string }
): Props => ({
chatId: 'test-chat-id',
appId: ids?.appId || '67e0d5535c02d1d5cdede71f',
teamId: ids?.teamId || '654a4107c32f3bf5f998452f',
tmbId: ids?.tmbId || '65ab7007462ada7dbb899948',
nodes: [
{
nodeId: 'node-1',
name: 'test-node',
flowNodeType: FlowNodeTypeEnum.systemConfig,
inputs: [],
outputs: []
}
],
newTitle: 'Test Chat',
source: 'online' as any,
userContent: {
obj: ChatRoleEnum.Human,
value: [
{
text: {
content: 'Hello, how are you?'
}
}
]
},
aiContent: {
obj: ChatRoleEnum.AI,
value: [
{
text: {
content: 'I am doing well, thank you!'
}
}
],
responseData: []
},
durationSeconds: 2.5,
...overrides
});
describe('pushChatRecords', () => {
let testAppId: string;
let testTeamId: string;
let testTmbId: string;
let testUserId: string;
beforeEach(async () => {
// Create test user
const user = await MongoUser.create({
username: 'test-user',
password: 'test-password'
});
testUserId = String(user._id);
// Create test team
const team = await MongoTeam.create({
name: 'Test Team',
ownerId: user._id,
avatar: 'test-avatar',
createTime: new Date(),
balance: 0,
teamDomain: 'test-domain'
});
testTeamId = String(team._id);
// Create team member
const teamMember = await MongoTeamMember.create({
teamId: team._id,
userId: user._id,
name: 'Test Member',
role: TeamMemberRoleEnum.owner,
status: 'active',
createTime: new Date(),
defaultTeam: true
});
testTmbId = String(teamMember._id);
// Create a test app for use in tests
const app = await MongoApp.create({
name: 'Test App',
type: AppTypeEnum.simple,
teamId: team._id,
tmbId: teamMember._id,
avatar: 'test-avatar',
intro: 'Test intro'
});
testAppId = String(app._id);
});
describe('pushChatRecords function', () => {
it('should skip saving if chatId is empty', async () => {
const props = createMockProps(
{ chatId: '' },
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
await pushChatRecords(props);
const chatItems = await MongoChatItem.find({ appId: testAppId });
expect(chatItems).toHaveLength(0);
});
it('should skip saving if chatId is NO_RECORD_HISTORIES', async () => {
const props = createMockProps(
{ chatId: 'NO_RECORD_HISTORIES' },
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
await pushChatRecords(props);
const chatItems = await MongoChatItem.find({ appId: testAppId });
expect(chatItems).toHaveLength(0);
});
it('should remove file URLs from user content before processing', async () => {
const props = createMockProps({
userContent: {
obj: ChatRoleEnum.Human,
value: [
{
file: {
type: ChatFileTypeEnum.image,
name: 'test.jpg',
url: 'https://example.com/test.jpg',
key: 'file-key-123'
}
}
]
}
});
await pushChatRecords(props);
// Verify that the URL was removed
expect(props.userContent.value[0].file?.url).toBe('');
});
it('should create chat items and update chat record', async () => {
const props = createMockProps({}, { appId: testAppId, teamId: testTeamId, tmbId: testTmbId });
await pushChatRecords(props);
// Check chat items were created
const chatItems = await MongoChatItem.find({ appId: testAppId, chatId: props.chatId });
expect(chatItems).toHaveLength(2);
// Check human message
const humanItem = chatItems.find((item) => item.obj === ChatRoleEnum.Human);
expect(humanItem).toBeDefined();
expect(humanItem?.value[0].text?.content).toBe('Hello, how are you?');
// Check AI message
const aiItem = chatItems.find((item) => item.obj === ChatRoleEnum.AI);
expect(aiItem).toBeDefined();
expect(aiItem?.value[0].text?.content).toBe('I am doing well, thank you!');
// Check chat record
const chat = await MongoChat.findOne({ appId: testAppId, chatId: props.chatId });
expect(chat).toBeDefined();
expect(chat?.title).toBe('Test Chat');
expect(String(chat?.teamId)).toBe(props.teamId);
});
it('should create chat item responses when responseData is provided', async () => {
const props = createMockProps({
aiContent: {
obj: ChatRoleEnum.AI,
value: [
{
text: { content: 'Response' }
}
],
responseData: [
{
nodeId: 'xx',
id: 'xx',
moduleType: FlowNodeTypeEnum.chatNode,
moduleName: 'Chat',
runningTime: 1.5,
totalPoints: 10
}
]
}
});
await pushChatRecords(props);
const responses = await MongoChatItemResponse.find({
appId: testAppId,
chatId: props.chatId
});
// ResponseData is only created when dataId exists on the AI chat item
// Since we're using real database, check if responses were created
if (responses.length > 0) {
expect(responses[0].data.moduleType).toBe(FlowNodeTypeEnum.chatNode);
expect(responses[0].data.totalPoints).toBe(10);
}
});
it('should handle dataset search node with quoteList', async () => {
const quote: SearchDataResponseItemType = {
id: 'quote-1',
chunkIndex: 0,
datasetId: 'dataset-1',
collectionId: 'collection-1',
sourceId: 'source-1',
sourceName: 'doc.pdf',
score: [{ type: 'embedding' as const, value: 0.95, index: 0 }],
q: 'What is AI?',
a: 'AI stands for Artificial Intelligence...',
updateTime: new Date()
};
const props = createMockProps(
{
aiContent: {
obj: ChatRoleEnum.AI,
value: [
{
text: { content: 'Based on the search results...' }
}
],
responseData: [
{
nodeId: 'xx',
id: 'xx',
moduleType: FlowNodeTypeEnum.datasetSearchNode,
moduleName: 'Dataset Search',
runningTime: 0.5,
totalPoints: 5,
quoteList: [quote]
}
]
}
},
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
await pushChatRecords(props);
const responses = await MongoChatItemResponse.find({
appId: testAppId,
chatId: props.chatId
});
// ResponseData is only created when dataId exists on the AI chat item
if (responses.length > 0) {
expect(responses[0].data.quoteList).toBeDefined();
expect(responses[0].data.quoteList?.[0]).toMatchObject({
id: quote.id,
chunkIndex: quote.chunkIndex,
datasetId: quote.datasetId,
collectionId: quote.collectionId,
sourceId: quote.sourceId,
sourceName: quote.sourceName,
score: quote.score
});
// q and a should be removed
expect(responses[0].data.quoteList?.[0]?.q).toBeUndefined();
expect(responses[0].data.quoteList?.[0]?.a).toBeUndefined();
}
});
it('should create chat data log with error count when response has error', async () => {
const props = createMockProps(
{
aiContent: {
obj: ChatRoleEnum.AI,
value: [],
responseData: [
{
nodeId: 'xx',
id: 'xx',
moduleType: FlowNodeTypeEnum.chatNode,
moduleName: 'Chat',
runningTime: 1.0,
totalPoints: 5,
errorText: 'API rate limit exceeded'
}
]
},
errorMsg: 'API rate limit exceeded'
},
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
await pushChatRecords(props);
const logs = await MongoAppChatLog.find({ appId: testAppId, chatId: props.chatId });
expect(logs).toHaveLength(1);
expect(logs[0].errorCount).toBe(1);
});
it('should calculate total points from response data', async () => {
const props = createMockProps(
{
aiContent: {
obj: ChatRoleEnum.AI,
value: [],
responseData: [
{
nodeId: 'xx',
id: 'xx',
moduleType: FlowNodeTypeEnum.chatNode,
moduleName: 'Chat',
runningTime: 1.0,
totalPoints: 10
},
{
nodeId: '22',
id: '33',
moduleType: FlowNodeTypeEnum.datasetSearchNode,
moduleName: 'Dataset Search',
runningTime: 0.5,
totalPoints: 5
}
]
}
},
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
await pushChatRecords(props);
const logs = await MongoAppChatLog.find({ appId: testAppId, chatId: props.chatId });
expect(logs).toHaveLength(1);
expect(logs[0].totalPoints).toBe(15);
});
it('should merge metadata from existing chat', async () => {
const props1 = createMockProps(
{
metadata: { source: 'web', version: '1.0' }
},
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
await pushChatRecords(props1);
const props2 = createMockProps(
{
chatId: props1.chatId,
metadata: { user: 'test-user' }
},
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
await pushChatRecords(props2);
const chat = await MongoChat.findOne({ appId: testAppId, chatId: props1.chatId });
expect(chat?.metadata).toMatchObject({
source: 'web',
version: '1.0',
user: 'test-user'
});
});
it('should track duration seconds', async () => {
const props = createMockProps(
{
durationSeconds: 3.75
},
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
await pushChatRecords(props);
const aiItem = await MongoChatItem.findOne({
appId: testAppId,
chatId: props.chatId,
obj: ChatRoleEnum.AI
});
if (aiItem) {
if ('durationSeconds' in aiItem) {
expect(aiItem?.durationSeconds).toBe(3.75);
} else {
throw new Error('aiItem does not have durationSeconds');
}
}
});
it('should store citeCollectionIds from dataset search', async () => {
const props = createMockProps(
{
aiContent: {
obj: ChatRoleEnum.AI,
value: [],
responseData: [
{
nodeId: 'xx',
id: 'xx',
moduleType: FlowNodeTypeEnum.datasetSearchNode,
moduleName: 'Dataset Search',
runningTime: 0.5,
totalPoints: 5,
quoteList: [
{
id: 'quote-1',
chunkIndex: 0,
datasetId: 'dataset-1',
collectionId: 'collection-1',
sourceId: 'source-1',
sourceName: 'doc1.pdf',
score: [{ type: 'embedding', value: 0.95, index: 0 }],
q: 'What is AI?',
a: 'AI stands for Artificial Intelligence...',
updateTime: new Date()
},
{
id: 'quote-2',
chunkIndex: 1,
datasetId: 'dataset-1',
collectionId: 'collection-2',
sourceId: 'source-2',
sourceName: 'doc2.pdf',
score: [{ type: 'embedding', value: 0.85, index: 0 }],
q: 'What is AI?',
a: 'AI stands for Artificial Intelligence...',
updateTime: new Date()
}
]
}
]
}
},
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
await pushChatRecords(props);
const aiItem = await MongoChatItem.findOne({
appId: testAppId,
chatId: props.chatId,
obj: ChatRoleEnum.AI
});
if (aiItem) {
if ('citeCollectionIds' in aiItem) {
expect(aiItem?.citeCollectionIds).toHaveLength(2);
expect(aiItem?.citeCollectionIds).toContain('collection-1');
expect(aiItem?.citeCollectionIds).toContain('collection-2');
} else {
throw new Error('aiItem does not have citeCollectionIds');
}
}
});
});
describe('updateInteractiveChat function', () => {
it('should skip update if chatId is empty', async () => {
const props = createMockProps(
{ chatId: '' },
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
const interactive = {
type: 'userSelect' as const,
params: {
description: '',
userSelectOptions: [
{ key: 'opt1', value: 'Option 1' },
{ key: 'opt2', value: 'Option 2' }
],
userSelectedVal: undefined
},
entryNodeIds: [],
memoryEdges: [],
nodeOutputs: []
};
await updateInteractiveChat({ interactive, ...props });
const chatItems = await MongoChatItem.find({ appId: testAppId });
expect(chatItems).toHaveLength(0);
});
it('should skip update if no AI chat item found', async () => {
const props = createMockProps({}, { appId: testAppId, teamId: testTeamId, tmbId: testTmbId });
const interactive = {
type: 'userSelect' as const,
params: {
description: '',
userSelectOptions: [
{ key: 'opt1', value: 'Option 1' },
{ key: 'opt2', value: 'Option 2' }
],
userSelectedVal: undefined
},
entryNodeIds: [],
memoryEdges: [],
nodeOutputs: []
};
await updateInteractiveChat({ interactive, ...props });
const chat = await MongoChat.findOne({ appId: testAppId, chatId: props.chatId });
expect(chat).toBeNull();
});
it('should skip update if chat item is not from AI', async () => {
// Create a human chat item
await MongoChatItem.create({
chatId: 'test-chat-id',
teamId: testTeamId,
tmbId: testTmbId,
appId: testAppId,
obj: ChatRoleEnum.Human,
value: [
{
text: { content: 'Hello' }
}
]
});
const props = createMockProps({}, { appId: testAppId, teamId: testTeamId, tmbId: testTmbId });
const interactive = {
type: 'userSelect' as const,
params: {
description: '',
userSelectOptions: [
{ key: 'opt1', value: 'Option 1' },
{ key: 'opt2', value: 'Option 2' }
],
userSelectedVal: undefined
},
entryNodeIds: [],
memoryEdges: [],
nodeOutputs: []
};
await updateInteractiveChat({ interactive, ...props });
// Should not create a chat record
const chat = await MongoChat.findOne({ appId: testAppId, chatId: props.chatId });
expect(chat).toBeNull();
});
it('should skip update if no interactive value found', async () => {
// Create an AI chat item without interactive
await MongoChatItem.create({
chatId: 'test-chat-id',
teamId: testTeamId,
tmbId: testTmbId,
appId: testAppId,
obj: ChatRoleEnum.AI,
value: [
{
text: { content: 'Hello' }
}
]
});
const props = createMockProps({}, { appId: testAppId, teamId: testTeamId, tmbId: testTmbId });
const interactive = {
type: 'userSelect' as const,
params: {
description: '',
userSelectOptions: [
{ key: 'opt1', value: 'Option 1' },
{ key: 'opt2', value: 'Option 2' }
],
userSelectedVal: undefined
},
entryNodeIds: [],
memoryEdges: [],
nodeOutputs: []
};
await updateInteractiveChat({ interactive, ...props });
// Should not create a chat record
const chat = await MongoChat.findOne({ appId: testAppId, chatId: props.chatId });
expect(chat).toBeNull();
});
it('should update userSelect interactive value', async () => {
// Create an AI chat item with userSelect interactive
await MongoChatItem.create({
chatId: 'test-chat-id',
teamId: testTeamId,
tmbId: testTmbId,
appId: testAppId,
obj: ChatRoleEnum.AI,
dataId: 'data-id-1',
value: [
{
interactive: {
type: 'userSelect',
params: {
userSelectedVal: undefined,
options: ['Option 1', 'Option 2']
}
}
}
]
});
const props = createMockProps(
{
appId: testAppId,
userContent: {
obj: ChatRoleEnum.Human,
value: [
{
text: { content: 'Option 1' }
}
]
},
aiContent: {
obj: ChatRoleEnum.AI,
value: [],
responseData: []
}
},
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
const interactive = {
type: 'userSelect' as const,
params: {
description: '',
userSelectOptions: [
{ key: 'opt1', value: 'Option 1' },
{ key: 'opt2', value: 'Option 2' }
],
userSelectedVal: undefined
},
entryNodeIds: [],
memoryEdges: [],
nodeOutputs: []
};
await updateInteractiveChat({ interactive, ...props });
const chatItem = await MongoChatItem.findOne({
appId: testAppId,
chatId: props.chatId,
obj: ChatRoleEnum.AI
});
console.log(chatItem?.value, 1212);
if (chatItem) {
if (chatItem.obj === ChatRoleEnum.AI) {
const lastValue = chatItem.value[chatItem.value.length - 1];
if (lastValue.interactive?.type === 'userSelect') {
expect(lastValue.interactive?.params.userSelectedVal).toBe('Option 1');
} else {
throw new Error('chatItem does not have userSelect interactive');
}
} else {
throw new Error('chatItem does not have value');
}
}
});
it('should update userInput interactive value', async () => {
// Create an AI chat item with userInput interactive
await MongoChatItem.create({
chatId: 'test-chat-id',
teamId: testTeamId,
tmbId: testTmbId,
appId: testAppId,
obj: ChatRoleEnum.AI,
dataId: 'data-id-1',
value: [
{
interactive: {
type: 'userInput',
params: {
submitted: false,
inputForm: [
{
key: 'username',
type: 'input',
label: 'Username',
value: undefined
}
]
}
}
}
]
});
const props = createMockProps(
{
userContent: {
obj: ChatRoleEnum.Human,
value: [
{
text: { content: JSON.stringify({ username: 'john_doe' }) }
}
]
},
aiContent: {
obj: ChatRoleEnum.AI,
value: [],
responseData: []
}
},
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
const interactive = {
type: 'userInput' as const,
params: {
description: '',
submitted: false,
inputForm: [
{
key: 'username',
type: FlowNodeInputTypeEnum.input,
label: 'Username',
value: undefined,
valueType: WorkflowIOValueTypeEnum.string,
required: false
}
]
},
entryNodeIds: [],
memoryEdges: [],
nodeOutputs: []
};
await updateInteractiveChat({ interactive, ...props });
const chatItem = await MongoChatItem.findOne({
appId: testAppId,
chatId: props.chatId,
obj: ChatRoleEnum.AI
});
if (chatItem) {
if (chatItem.obj === ChatRoleEnum.AI) {
const lastValue = chatItem.value[chatItem.value.length - 1];
if (lastValue.interactive?.type === 'userInput') {
expect(lastValue.interactive?.params.submitted).toBe(true);
expect(lastValue.interactive?.params.inputForm[0].value).toBe('john_doe');
} else {
throw new Error('chatItem does not have userInput interactive');
}
} else {
throw new Error('chatItem does not have value');
}
}
});
it('should remove paymentPause interactive value', async () => {
// Create an AI chat item with paymentPause interactive
await MongoChatItem.create({
chatId: 'test-chat-id',
teamId: testTeamId,
tmbId: testTmbId,
appId: testAppId,
obj: ChatRoleEnum.AI,
dataId: 'data-id-1',
value: [
{
text: { content: 'Payment required' }
},
{
interactive: {
type: 'paymentPause',
params: {}
}
}
]
});
const props = createMockProps({}, { appId: testAppId, teamId: testTeamId, tmbId: testTmbId });
const interactive = {
type: 'paymentPause' as const,
params: {
description: 'Payment required',
continue: false
},
entryNodeIds: [],
memoryEdges: [],
nodeOutputs: []
};
await updateInteractiveChat({ interactive, ...props });
const chatItem = await MongoChatItem.findOne({
appId: testAppId,
chatId: props.chatId,
obj: ChatRoleEnum.AI
});
// PaymentPause is removed, and AI response is appended
expect(chatItem?.value.length).toBeGreaterThan(0);
});
it('should merge AI response values', async () => {
// Create an AI chat item with interactive
await MongoChatItem.create({
chatId: 'test-chat-id',
teamId: testTeamId,
tmbId: testTmbId,
appId: testAppId,
obj: ChatRoleEnum.AI,
dataId: 'data-id-1',
value: [
{
text: { content: 'First response' }
},
{
interactive: {
type: 'userSelect',
params: {
description: '',
userSelectOptions: [
{ key: 'a', value: 'A' },
{ key: 'b', value: 'B' }
]
}
}
}
]
});
const props = createMockProps(
{
aiContent: {
obj: ChatRoleEnum.AI,
value: [
{
text: { content: 'Second response' }
}
],
responseData: []
}
},
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
const interactive = {
type: 'userSelect' as const,
params: {
description: '',
userSelectOptions: [
{ key: 'a', value: 'A' },
{ key: 'b', value: 'B' }
]
},
entryNodeIds: [],
memoryEdges: [],
nodeOutputs: []
};
await updateInteractiveChat({ interactive, ...props });
const chatItem = await MongoChatItem.findOne({
appId: testAppId,
chatId: props.chatId,
obj: ChatRoleEnum.AI
});
// Original has 2 values (text + interactive), AI adds 1 more (text)
expect(chatItem?.value.length).toBeGreaterThanOrEqual(3);
expect(chatItem?.value[chatItem.value.length - 1].text?.content).toBe('Second response');
});
it('should accumulate duration seconds', async () => {
// Create an AI chat item
await MongoChatItem.create({
chatId: 'test-chat-id',
teamId: testTeamId,
tmbId: testTmbId,
appId: testAppId,
obj: ChatRoleEnum.AI,
dataId: 'data-id-1',
durationSeconds: 1.5,
value: [
{
interactive: {
type: 'userSelect',
params: { options: ['A', 'B'] }
}
}
]
});
const props = createMockProps(
{
durationSeconds: 2.3
},
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
const interactive = {
type: 'userSelect' as const,
params: {
description: '',
userSelectOptions: [
{ key: 'a', value: 'A' },
{ key: 'b', value: 'B' }
]
},
entryNodeIds: [],
memoryEdges: [],
nodeOutputs: []
};
await updateInteractiveChat({ interactive, ...props });
const chatItem = await MongoChatItem.findOne({
appId: testAppId,
chatId: props.chatId,
obj: ChatRoleEnum.AI
});
if (chatItem) {
if (chatItem.obj === ChatRoleEnum.AI) {
expect(chatItem?.durationSeconds).toBe(3.8);
} else {
throw new Error('chatItem does not have durationSeconds');
}
}
});
it('should merge chat item responses', async () => {
// Create an AI chat item
await MongoChatItem.create({
chatId: 'test-chat-id',
teamId: testTeamId,
tmbId: testTmbId,
appId: testAppId,
obj: ChatRoleEnum.AI,
dataId: 'data-id-1',
value: [
{
interactive: {
type: 'userSelect',
params: {
description: '',
userSelectOptions: [
{ key: 'a', value: 'A' },
{ key: 'b', value: 'B' }
]
}
}
}
]
});
// Create an existing response
await MongoChatItemResponse.create({
teamId: testTeamId,
appId: testAppId,
chatId: 'test-chat-id',
chatItemDataId: 'data-id-1',
data: {
moduleType: FlowNodeTypeEnum.chatNode,
moduleName: 'Chat',
runningTime: 1.0,
totalPoints: 10
}
});
const props = createMockProps(
{
aiContent: {
obj: ChatRoleEnum.AI,
value: [],
responseData: [
{
nodeId: 'xx',
id: 'xx',
moduleType: FlowNodeTypeEnum.datasetSearchNode,
moduleName: 'Dataset Search',
runningTime: 0.5,
totalPoints: 5
}
]
}
},
{ appId: testAppId, teamId: testTeamId, tmbId: testTmbId }
);
const interactive = {
type: 'userSelect' as const,
params: {
description: '',
userSelectOptions: [
{ key: 'a', value: 'A' },
{ key: 'b', value: 'B' }
]
},
entryNodeIds: [],
memoryEdges: [],
nodeOutputs: []
};
await updateInteractiveChat({ interactive, ...props });
const responses = await MongoChatItemResponse.find({
appId: testAppId,
chatId: props.chatId
});
// Should have merged responses
expect(responses.length).toBeGreaterThan(0);
});
});
});