@@ -2,7 +2,7 @@ import { eq, inArray } from 'drizzle-orm';
22import { afterEach , beforeEach , describe , expect , it } from 'vitest' ;
33
44import { getTestDB } from '../../../core/getTestDB' ;
5- import { agents , messages , sessions , topics , users } from '../../../schemas' ;
5+ import { agents , messagePlugins , messages , sessions , topics , users } from '../../../schemas' ;
66import { LobeChatDatabase } from '../../../type' ;
77import { CreateTopicParams , TopicModel } from '../../topic' ;
88
@@ -279,6 +279,129 @@ describe('TopicModel - Create', () => {
279279 expect ( duplicatedMessages [ 1 ] . content ) . toBe ( 'Assistant message' ) ;
280280 } ) ;
281281
282+ it ( 'should correctly map parentId references when duplicating messages' , async ( ) => {
283+ const topicId = 'topic-with-parent-refs' ;
284+
285+ await serverDB . transaction ( async ( tx ) => {
286+ await tx . insert ( topics ) . values ( { id : topicId , sessionId, userId, title : 'Original Topic' } ) ;
287+ await tx . insert ( messages ) . values ( [
288+ { id : 'msg1' , role : 'user' , topicId, userId, content : 'First message' , parentId : null } ,
289+ {
290+ id : 'msg2' ,
291+ role : 'assistant' ,
292+ topicId,
293+ userId,
294+ content : 'Reply to first' ,
295+ parentId : 'msg1' ,
296+ } ,
297+ {
298+ id : 'msg3' ,
299+ role : 'tool' ,
300+ topicId,
301+ userId,
302+ content : 'Tool response' ,
303+ parentId : 'msg2' ,
304+ } ,
305+ {
306+ id : 'msg4' ,
307+ role : 'assistant' ,
308+ topicId,
309+ userId,
310+ content : 'Final message' ,
311+ parentId : 'msg3' ,
312+ } ,
313+ ] ) ;
314+ } ) ;
315+
316+ const { topic : duplicatedTopic , messages : duplicatedMessages } = await topicModel . duplicate (
317+ topicId ,
318+ 'Duplicated Topic' ,
319+ ) ;
320+
321+ expect ( duplicatedMessages ) . toHaveLength ( 4 ) ;
322+
323+ const msgMap = new Map ( duplicatedMessages . map ( ( m ) => [ m . content , m ] ) ) ;
324+ const newMsg1 = msgMap . get ( 'First message' ) ! ;
325+ const newMsg2 = msgMap . get ( 'Reply to first' ) ! ;
326+ const newMsg3 = msgMap . get ( 'Tool response' ) ! ;
327+ const newMsg4 = msgMap . get ( 'Final message' ) ! ;
328+
329+ expect ( newMsg1 . parentId ) . toBeNull ( ) ;
330+ expect ( newMsg2 . parentId ) . toBe ( newMsg1 . id ) ;
331+ expect ( newMsg3 . parentId ) . toBe ( newMsg2 . id ) ;
332+ expect ( newMsg4 . parentId ) . toBe ( newMsg3 . id ) ;
333+
334+ expect ( newMsg1 . id ) . not . toBe ( 'msg1' ) ;
335+ expect ( newMsg2 . id ) . not . toBe ( 'msg2' ) ;
336+ expect ( newMsg3 . id ) . not . toBe ( 'msg3' ) ;
337+ expect ( newMsg4 . id ) . not . toBe ( 'msg4' ) ;
338+ } ) ;
339+
340+ it ( 'should correctly map tool_call_id when duplicating messages with tools' , async ( ) => {
341+ const topicId = 'topic-with-tools' ;
342+ const originalToolId = 'toolu_original_123' ;
343+
344+ await serverDB . transaction ( async ( tx ) => {
345+ await tx . insert ( topics ) . values ( { id : topicId , sessionId, userId, title : 'Original Topic' } ) ;
346+
347+ // Insert assistant message with tools
348+ await tx . insert ( messages ) . values ( {
349+ id : 'msg1' ,
350+ role : 'assistant' ,
351+ topicId,
352+ userId,
353+ content : 'Using tool' ,
354+ parentId : null ,
355+ tools : [ { id : originalToolId , type : 'builtin' , apiName : 'broadcast' } ] ,
356+ } ) ;
357+
358+ // Insert tool message
359+ await tx . insert ( messages ) . values ( {
360+ id : 'msg2' ,
361+ role : 'tool' ,
362+ topicId,
363+ userId,
364+ content : 'Tool response' ,
365+ parentId : 'msg1' ,
366+ } ) ;
367+
368+ // Insert messagePlugins entry
369+ await tx . insert ( messagePlugins ) . values ( {
370+ id : 'msg2' ,
371+ userId,
372+ toolCallId : originalToolId ,
373+ apiName : 'broadcast' ,
374+ } ) ;
375+ } ) ;
376+
377+ const { topic : duplicatedTopic , messages : duplicatedMessages } = await topicModel . duplicate (
378+ topicId ,
379+ 'Duplicated Topic' ,
380+ ) ;
381+
382+ expect ( duplicatedMessages ) . toHaveLength ( 2 ) ;
383+
384+ const msgMap = new Map ( duplicatedMessages . map ( ( m ) => [ m . role , m ] ) ) ;
385+ const newAssistant = msgMap . get ( 'assistant' ) ! ;
386+ const newTool = msgMap . get ( 'tool' ) ! ;
387+
388+ // Check that tools array has new IDs
389+ expect ( newAssistant . tools ) . toBeDefined ( ) ;
390+ const newTools = newAssistant . tools as any [ ] ;
391+ expect ( newTools ) . toHaveLength ( 1 ) ;
392+ expect ( newTools [ 0 ] . id ) . not . toBe ( originalToolId ) ;
393+ expect ( newTools [ 0 ] . id ) . toMatch ( / ^ t o o l u _ / ) ;
394+
395+ // Check that messagePlugins was copied with new toolCallId
396+ const newPlugin = await serverDB . query . messagePlugins . findFirst ( {
397+ where : eq ( messagePlugins . id , newTool . id ) ,
398+ } ) ;
399+
400+ expect ( newPlugin ) . toBeDefined ( ) ;
401+ expect ( newPlugin ! . toolCallId ) . toBe ( newTools [ 0 ] . id ) ;
402+ expect ( newPlugin ! . toolCallId ) . not . toBe ( originalToolId ) ;
403+ } ) ;
404+
282405 it ( 'should throw an error if the topic to duplicate does not exist' , async ( ) => {
283406 const topicId = 'nonexistent-topic' ;
284407
0 commit comments