@@ -546,6 +546,183 @@ void main() {
546546 expect (msg.toString (), contains ('Message' ));
547547 expect (msg.toString (), contains ('parts: [TextPart(hi)]' ));
548548 });
549+
550+ group ('concatenate' , () {
551+ test ('combines parts and text from both messages' , () {
552+ final a = ChatMessage .model ('hello ' );
553+ final b = ChatMessage .model ('world' );
554+ final ChatMessage result = a.concatenate (b);
555+ expect (result.parts, hasLength (2 ));
556+ expect (result.text, equals ('hello world' ));
557+ });
558+
559+ test ('falls back to second finishStatus when first is null' , () {
560+ final a = ChatMessage (role: ChatMessageRole .model);
561+ final b = ChatMessage (
562+ role: ChatMessageRole .model,
563+ finishStatus: const FinishStatus .completed (),
564+ );
565+ final ChatMessage result = a.concatenate (b);
566+ expect (result.finishStatus, equals (const FinishStatus .completed ()));
567+ });
568+
569+ test ('finishStatus is null when both are null' , () {
570+ final a = ChatMessage (role: ChatMessageRole .model);
571+ final b = ChatMessage (role: ChatMessageRole .model);
572+ expect (a.concatenate (b).finishStatus, isNull);
573+ });
574+
575+ test ('same finishStatus on both is preserved' , () {
576+ final a = ChatMessage (
577+ role: ChatMessageRole .model,
578+ finishStatus: const FinishStatus .completed (),
579+ );
580+ final b = ChatMessage (
581+ role: ChatMessageRole .model,
582+ finishStatus: const FinishStatus .completed (),
583+ );
584+ final ChatMessage result = a.concatenate (b);
585+ expect (result.finishStatus, equals (const FinishStatus .completed ()));
586+ });
587+
588+ test ('equal metadata is preserved' , () {
589+ final a = ChatMessage (
590+ role: ChatMessageRole .model,
591+ metadata: const {'k' : 'v' },
592+ );
593+ final b = ChatMessage (
594+ role: ChatMessageRole .model,
595+ metadata: const {'k' : 'v' },
596+ );
597+ expect (a.concatenate (b).metadata, equals (const {'k' : 'v' }));
598+ });
599+
600+ test ('preserves non-text parts' , () {
601+ const toolCall = ToolPart .call (
602+ callId: 'c1' ,
603+ toolName: 't1' ,
604+ arguments: {},
605+ );
606+ const toolResult = ToolPart .result (
607+ callId: 'c1' ,
608+ toolName: 't1' ,
609+ result: 'ok' ,
610+ );
611+ final a = ChatMessage (
612+ role: ChatMessageRole .model,
613+ parts: [const TextPart ('text' ), toolCall],
614+ );
615+ final b = ChatMessage (role: ChatMessageRole .model, parts: [toolResult]);
616+ final ChatMessage result = a.concatenate (b);
617+ expect (result.parts, hasLength (3 ));
618+ expect (result.parts[0 ], isA <TextPart >());
619+ expect (result.parts[1 ], isA <ToolPart >());
620+ expect (result.parts[2 ], isA <ToolPart >());
621+ });
622+
623+ test ('throws on role mismatch' , () {
624+ final a = ChatMessage .user ('hi' );
625+ final b = ChatMessage .model ('there' );
626+ expect (() => a.concatenate (b), throwsA (isA <ArgumentError >()));
627+ });
628+
629+ test ('throws on conflicting finish statuses' , () {
630+ final a = ChatMessage (
631+ role: ChatMessageRole .model,
632+ finishStatus: const FinishStatus .completed (),
633+ );
634+ final b = ChatMessage (
635+ role: ChatMessageRole .model,
636+ finishStatus: const FinishStatus .notFinished (),
637+ );
638+ expect (() => a.concatenate (b), throwsA (isA <ArgumentError >()));
639+ });
640+
641+ test ('throws when metadata values differ for same key' , () {
642+ final a = ChatMessage (
643+ role: ChatMessageRole .model,
644+ metadata: const {'key' : 'from-a' },
645+ );
646+ final b = ChatMessage (
647+ role: ChatMessageRole .model,
648+ metadata: const {'key' : 'from-b' },
649+ );
650+ expect (() => a.concatenate (b), throwsA (isA <ArgumentError >()));
651+ });
652+
653+ test ('throws when metadata have different keys' , () {
654+ final a = ChatMessage (
655+ role: ChatMessageRole .model,
656+ metadata: const {'only-a' : 1 },
657+ );
658+ final b = ChatMessage (
659+ role: ChatMessageRole .model,
660+ metadata: const {'only-b' : 2 },
661+ );
662+ expect (() => a.concatenate (b), throwsA (isA <ArgumentError >()));
663+ });
664+ });
665+
666+ group ('copyWith' , () {
667+ final original = ChatMessage (
668+ role: ChatMessageRole .user,
669+ parts: const [TextPart ('hello' )],
670+ metadata: const {'k' : 'v' },
671+ finishStatus: const FinishStatus .completed (),
672+ );
673+
674+ test ('no arguments returns equal copy' , () {
675+ expect (original.copyWith (), equals (original));
676+ });
677+
678+ test ('replaces role' , () {
679+ final ChatMessage result = original.copyWith (
680+ role: ChatMessageRole .model,
681+ );
682+ expect (result.role, equals (ChatMessageRole .model));
683+ expect (result.parts, equals (original.parts));
684+ expect (result.metadata, equals (original.metadata));
685+ expect (result.finishStatus, equals (original.finishStatus));
686+ });
687+
688+ test ('replaces parts' , () {
689+ final newParts = [const TextPart ('world' )];
690+ final ChatMessage result = original.copyWith (parts: newParts);
691+ expect (result.parts, equals (newParts));
692+ expect (result.role, equals (original.role));
693+ expect (result.metadata, equals (original.metadata));
694+ expect (result.finishStatus, equals (original.finishStatus));
695+ });
696+
697+ test ('replaces metadata' , () {
698+ final ChatMessage result = original.copyWith (metadata: const {'x' : 1 });
699+ expect (result.metadata, equals (const {'x' : 1 }));
700+ expect (result.role, equals (original.role));
701+ expect (result.parts, equals (original.parts));
702+ expect (result.finishStatus, equals (original.finishStatus));
703+ });
704+
705+ test ('replaces finishStatus' , () {
706+ final ChatMessage result = original.copyWith (
707+ finishStatus: const FinishStatus .notFinished (),
708+ );
709+ expect (result.finishStatus, equals (const FinishStatus .notFinished ()));
710+ expect (result.role, equals (original.role));
711+ expect (result.parts, equals (original.parts));
712+ expect (result.metadata, equals (original.metadata));
713+ });
714+
715+ test ('replaces multiple fields at once' , () {
716+ final ChatMessage result = original.copyWith (
717+ role: ChatMessageRole .model,
718+ parts: const [TextPart ('new' )],
719+ );
720+ expect (result.role, equals (ChatMessageRole .model));
721+ expect (result.parts, equals (const [TextPart ('new' )]));
722+ expect (result.metadata, equals (original.metadata));
723+ expect (result.finishStatus, equals (original.finishStatus));
724+ });
725+ });
549726 });
550727
551728 group ('Parts' , () {
0 commit comments