@@ -8,13 +8,14 @@ import {
88} from '@mongodb-js/testing-library-compass' ;
99import { AssistantChat } from './assistant-chat' ;
1010import { expect } from 'chai' ;
11- import { createMockChat } from '../test/utils' ;
11+ import { createMockChat } from '../../ test/utils' ;
1212import type { ConnectionInfo } from '@mongodb-js/connection-info' ;
1313import {
1414 AssistantActionsContext ,
1515 type AssistantMessage ,
16- } from './compass-assistant-provider' ;
16+ } from '.. /compass-assistant-provider' ;
1717import sinon from 'sinon' ;
18+ import type { TextPart } from 'ai' ;
1819
1920describe ( 'AssistantChat' , function ( ) {
2021 const mockMessages : AssistantMessage [ ] = [
@@ -533,6 +534,237 @@ describe('AssistantChat', function () {
533534 } ) ;
534535 } ) ;
535536
537+ describe ( 'messages with confirmation' , function ( ) {
538+ let mockConfirmationMessage : AssistantMessage ;
539+
540+ beforeEach ( function ( ) {
541+ mockConfirmationMessage = {
542+ id : 'confirmation-test' ,
543+ role : 'assistant' ,
544+ parts : [ { type : 'text' , text : 'This is a confirmation message.' } ] ,
545+ metadata : {
546+ confirmation : {
547+ state : 'pending' ,
548+ description : 'Are you sure you want to proceed with this action?' ,
549+ } ,
550+ } ,
551+ } ;
552+ } ) ;
553+
554+ it ( 'renders confirmation message when message has confirmation metadata' , function ( ) {
555+ renderWithChat ( [ mockConfirmationMessage ] ) ;
556+
557+ expect ( screen . getByText ( 'Please confirm your request' ) ) . to . exist ;
558+ expect (
559+ screen . getByText ( 'Are you sure you want to proceed with this action?' )
560+ ) . to . exist ;
561+ expect ( screen . getByText ( 'Confirm' ) ) . to . exist ;
562+ expect ( screen . getByText ( 'Cancel' ) ) . to . exist ;
563+ } ) ;
564+
565+ it ( 'does not render regular message content when confirmation metadata exists' , function ( ) {
566+ renderWithChat ( [ mockConfirmationMessage ] ) ;
567+
568+ // Should not show the message text content when confirmation is present
569+ expect ( screen . queryByText ( 'This is a confirmation message.' ) ) . to . not
570+ . exist ;
571+ } ) ;
572+
573+ it ( 'shows confirmation as pending when it is the last message' , function ( ) {
574+ renderWithChat ( [ mockConfirmationMessage ] ) ;
575+
576+ expect ( screen . getByText ( 'Confirm' ) ) . to . exist ;
577+ expect ( screen . getByText ( 'Cancel' ) ) . to . exist ;
578+ expect ( screen . queryByText ( 'Request confirmed' ) ) . to . not . exist ;
579+ expect ( screen . queryByText ( 'Request cancelled' ) ) . to . not . exist ;
580+ } ) ;
581+
582+ it ( 'shows confirmation as rejected when it is not the last message' , function ( ) {
583+ const messages : AssistantMessage [ ] = [
584+ mockConfirmationMessage ,
585+ {
586+ id : 'newer-message' ,
587+ role : 'user' as const ,
588+ parts : [ { type : 'text' , text : 'Another message' } ] ,
589+ } ,
590+ ] ;
591+
592+ renderWithChat ( messages ) ;
593+
594+ // The confirmation message (first one) should show as rejected since it's not the last
595+ expect ( screen . queryByText ( 'Confirm' ) ) . to . not . exist ;
596+ expect ( screen . queryByText ( 'Cancel' ) ) . to . not . exist ;
597+ expect ( screen . getByText ( 'Request cancelled' ) ) . to . exist ;
598+ } ) ;
599+
600+ it ( 'adds new confirmed message when confirmation is confirmed' , function ( ) {
601+ const { chat, ensureOptInAndSendStub } = renderWithChat ( [
602+ mockConfirmationMessage ,
603+ ] ) ;
604+
605+ const confirmButton = screen . getByText ( 'Confirm' ) ;
606+ userEvent . click ( confirmButton ) ;
607+
608+ // Should add a new message without confirmation metadata
609+ expect ( chat . messages ) . to . have . length ( 2 ) ;
610+ const newMessage = chat . messages [ 1 ] ;
611+ expect ( newMessage . id ) . to . equal ( 'confirmation-test-confirmed' ) ;
612+ expect ( newMessage . metadata ?. confirmation ) . to . be . undefined ;
613+ expect ( newMessage . parts ) . to . deep . equal ( mockConfirmationMessage . parts ) ;
614+
615+ // Should call ensureOptInAndSend to send the new message
616+ expect ( ensureOptInAndSendStub . calledOnce ) . to . be . true ;
617+ } ) ;
618+
619+ it ( 'updates confirmation state to confirmed and adds a new message when confirm button is clicked' , function ( ) {
620+ const { chat } = renderWithChat ( [ mockConfirmationMessage ] ) ;
621+
622+ const confirmButton = screen . getByText ( 'Confirm' ) ;
623+ userEvent . click ( confirmButton ) ;
624+
625+ // Original message should have updated confirmation state
626+ const originalMessage = chat . messages [ 0 ] ;
627+ expect ( originalMessage . metadata ?. confirmation ?. state ) . to . equal (
628+ 'confirmed'
629+ ) ;
630+
631+ expect ( chat . messages ) . to . have . length ( 2 ) ;
632+
633+ expect (
634+ screen . getByText ( ( mockConfirmationMessage . parts [ 0 ] as TextPart ) . text )
635+ ) . to . exist ;
636+ } ) ;
637+
638+ it ( 'updates confirmation state to rejected and does not add a new message when cancel button is clicked' , function ( ) {
639+ const { chat, ensureOptInAndSendStub } = renderWithChat ( [
640+ mockConfirmationMessage ,
641+ ] ) ;
642+
643+ const cancelButton = screen . getByText ( 'Cancel' ) ;
644+ userEvent . click ( cancelButton ) ;
645+
646+ // Original message should have updated confirmation state
647+ const originalMessage = chat . messages [ 0 ] ;
648+ expect ( originalMessage . metadata ?. confirmation ?. state ) . to . equal (
649+ 'rejected'
650+ ) ;
651+
652+ // Should not add a new message
653+ expect ( chat . messages ) . to . have . length ( 1 ) ;
654+
655+ // Should not call ensureOptInAndSend
656+ expect ( ensureOptInAndSendStub . notCalled ) . to . be . true ;
657+ } ) ;
658+
659+ it ( 'shows confirmed status after confirmation is confirmed' , function ( ) {
660+ const { chat } = renderWithChat ( [ mockConfirmationMessage ] ) ;
661+
662+ // Verify buttons are initially present
663+ expect ( screen . getByText ( 'Confirm' ) ) . to . exist ;
664+ expect ( screen . getByText ( 'Cancel' ) ) . to . exist ;
665+
666+ const confirmButton = screen . getByText ( 'Confirm' ) ;
667+ userEvent . click ( confirmButton ) ;
668+
669+ // The state update should be immediate - check the chat messages
670+ const updatedMessage = chat . messages [ 0 ] ;
671+ expect ( updatedMessage . metadata ?. confirmation ?. state ) . to . equal (
672+ 'confirmed'
673+ ) ;
674+ } ) ;
675+
676+ it ( 'shows cancelled status after confirmation is rejected' , function ( ) {
677+ const { chat } = renderWithChat ( [ mockConfirmationMessage ] ) ;
678+
679+ // Verify buttons are initially present
680+ expect ( screen . getByText ( 'Confirm' ) ) . to . exist ;
681+ expect ( screen . getByText ( 'Cancel' ) ) . to . exist ;
682+
683+ const cancelButton = screen . getByText ( 'Cancel' ) ;
684+ userEvent . click ( cancelButton ) ;
685+
686+ // The state update should be immediate - check the chat messages
687+ const updatedMessage = chat . messages [ 0 ] ;
688+ expect ( updatedMessage . metadata ?. confirmation ?. state ) . to . equal ( 'rejected' ) ;
689+ } ) ;
690+
691+ it ( 'handles multiple confirmation messages correctly' , function ( ) {
692+ const confirmationMessage1 : AssistantMessage = {
693+ id : 'confirmation-1' ,
694+ role : 'assistant' ,
695+ parts : [ { type : 'text' , text : 'First confirmation' } ] ,
696+ metadata : {
697+ confirmation : {
698+ state : 'pending' ,
699+ description : 'First confirmation description' ,
700+ } ,
701+ } ,
702+ } ;
703+
704+ const confirmationMessage2 : AssistantMessage = {
705+ id : 'confirmation-2' ,
706+ role : 'assistant' ,
707+ parts : [ { type : 'text' , text : 'Second confirmation' } ] ,
708+ metadata : {
709+ confirmation : {
710+ state : 'pending' ,
711+ description : 'Second confirmation description' ,
712+ } ,
713+ } ,
714+ } ;
715+
716+ renderWithChat ( [ confirmationMessage1 , confirmationMessage2 ] ) ;
717+
718+ expect ( screen . getAllByText ( 'Request cancelled' ) ) . to . have . length ( 1 ) ;
719+
720+ expect ( screen . getAllByText ( 'Confirm' ) ) . to . have . length ( 1 ) ;
721+ expect ( screen . getAllByText ( 'Cancel' ) ) . to . have . length ( 1 ) ;
722+ expect ( screen . getByText ( 'Second confirmation description' ) ) . to . exist ;
723+ } ) ;
724+
725+ it ( 'preserves other metadata when creating confirmed message' , function ( ) {
726+ const messageWithExtraMetadata : AssistantMessage = {
727+ id : 'confirmation-with-metadata' ,
728+ role : 'assistant' ,
729+ parts : [ { type : 'text' , text : 'Message with extra metadata' } ] ,
730+ metadata : {
731+ confirmation : {
732+ state : 'pending' ,
733+ description : 'Confirmation description' ,
734+ } ,
735+ displayText : 'Custom display text' ,
736+ isPermanent : true ,
737+ } ,
738+ } ;
739+
740+ const { chat } = renderWithChat ( [ messageWithExtraMetadata ] ) ;
741+
742+ const confirmButton = screen . getByText ( 'Confirm' ) ;
743+ userEvent . click ( confirmButton ) ;
744+
745+ // New confirmed message should preserve other metadata
746+ const newMessage = chat . messages [ 1 ] ;
747+ expect ( newMessage . metadata ?. displayText ) . to . equal ( 'Custom display text' ) ;
748+ expect ( newMessage . metadata ?. isPermanent ) . to . equal ( true ) ;
749+ expect ( newMessage . metadata ?. confirmation ) . to . be . undefined ;
750+ } ) ;
751+
752+ it ( 'does not render confirmation component for regular messages' , function ( ) {
753+ const regularMessage : AssistantMessage = {
754+ id : 'regular' ,
755+ role : 'assistant' ,
756+ parts : [ { type : 'text' , text : 'This is a regular message' } ] ,
757+ } ;
758+
759+ renderWithChat ( [ regularMessage ] ) ;
760+
761+ expect ( screen . queryByText ( 'Please confirm your request' ) ) . to . not . exist ;
762+ expect ( screen . queryByText ( 'Confirm' ) ) . to . not . exist ;
763+ expect ( screen . queryByText ( 'Cancel' ) ) . to . not . exist ;
764+ expect ( screen . getByText ( 'This is a regular message' ) ) . to . exist ;
765+ } ) ;
766+ } ) ;
767+
536768 describe ( 'related sources' , function ( ) {
537769 it ( 'displays related resources links for assistant messages that include them' , async function ( ) {
538770 renderWithChat ( mockMessages ) ;
0 commit comments