@webex/contact-center 3.12.0-task-refactor.7 → 3.12.0-task-refactor.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/cc.js +3 -4
  2. package/dist/cc.js.map +1 -1
  3. package/dist/constants.js +1 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/metrics/constants.js +2 -0
  6. package/dist/metrics/constants.js.map +1 -1
  7. package/dist/services/ApiAiAssistant.js +74 -3
  8. package/dist/services/ApiAiAssistant.js.map +1 -1
  9. package/dist/services/config/types.js +9 -1
  10. package/dist/services/config/types.js.map +1 -1
  11. package/dist/services/task/Task.js +32 -0
  12. package/dist/services/task/Task.js.map +1 -1
  13. package/dist/services/task/TaskManager.js +7 -2
  14. package/dist/services/task/TaskManager.js.map +1 -1
  15. package/dist/services/task/TaskUtils.js +3 -1
  16. package/dist/services/task/TaskUtils.js.map +1 -1
  17. package/dist/services/task/state-machine/TaskStateMachine.js +76 -0
  18. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  19. package/dist/services/task/state-machine/actions.js +113 -23
  20. package/dist/services/task/state-machine/actions.js.map +1 -1
  21. package/dist/services/task/state-machine/uiControlsComputer.js +99 -21
  22. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  23. package/dist/types/constants.d.ts +1 -0
  24. package/dist/types/metrics/constants.d.ts +2 -0
  25. package/dist/types/services/ApiAiAssistant.d.ts +10 -2
  26. package/dist/types/services/config/types.d.ts +16 -0
  27. package/dist/types/services/task/Task.d.ts +10 -0
  28. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +110 -0
  29. package/dist/types/services/task/state-machine/actions.d.ts +2 -0
  30. package/dist/types/types.d.ts +24 -0
  31. package/dist/types.js +15 -0
  32. package/dist/types.js.map +1 -1
  33. package/dist/webex.js +1 -1
  34. package/package.json +1 -1
  35. package/src/cc.ts +6 -4
  36. package/src/constants.ts +1 -0
  37. package/src/metrics/constants.ts +2 -0
  38. package/src/services/ApiAiAssistant.ts +102 -2
  39. package/src/services/config/types.ts +8 -0
  40. package/src/services/task/Task.ts +34 -0
  41. package/src/services/task/TaskManager.ts +7 -2
  42. package/src/services/task/TaskUtils.ts +5 -3
  43. package/src/services/task/ai-docs/AGENTS.md +7 -0
  44. package/src/services/task/ai-docs/ARCHITECTURE.md +12 -0
  45. package/src/services/task/state-machine/TaskStateMachine.ts +104 -0
  46. package/src/services/task/state-machine/actions.ts +151 -25
  47. package/src/services/task/state-machine/uiControlsComputer.ts +173 -30
  48. package/src/types.ts +25 -0
  49. package/test/unit/spec/cc.ts +2 -0
  50. package/test/unit/spec/services/ApiAiAssistant.ts +105 -17
  51. package/test/unit/spec/services/task/Task.ts +61 -0
  52. package/test/unit/spec/services/task/TaskManager.ts +42 -0
  53. package/test/unit/spec/services/task/TaskUtils.ts +65 -0
  54. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +676 -0
  55. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +597 -0
  56. package/test/unit/spec/services/task/voice/WebRTC.ts +99 -106
  57. package/umd/contact-center.min.js +2 -2
  58. package/umd/contact-center.min.js.map +1 -1
@@ -471,6 +471,606 @@ describe('Task state machine', () => {
471
471
  expect(snapshotAfterEnd.context.consultDestinationAgentJoined).toBe(false);
472
472
  });
473
473
 
474
+ it('returns to HELD with main-leg controls after AgentConsultEnded from Stable Prod while CONSULTING', () => {
475
+ const service = startMachine();
476
+ const baseTaskData = createTaskData({
477
+ agentId: 'agent-1',
478
+ mediaResourceId: 'interaction-1',
479
+ });
480
+
481
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: baseTaskData});
482
+ service.send({type: TaskEvent.ASSIGN, taskData: baseTaskData});
483
+ service.send({
484
+ type: TaskEvent.CONSULT,
485
+ destination: 'agent-2',
486
+ destinationType: 'agent',
487
+ });
488
+ service.send({type: TaskEvent.CONSULT_SUCCESS});
489
+ service.send({type: TaskEvent.CONSULTING_ACTIVE, consultDestinationAgentJoined: true});
490
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
491
+
492
+ const consultEndedTaskData = createTaskData({
493
+ agentId: 'agent-1',
494
+ mediaResourceId: 'interaction-1',
495
+ type: 'AgentConsultEnded' as any,
496
+ isConsulted: false,
497
+ interaction: {
498
+ state: 'connected',
499
+ interactionId: 'interaction-1',
500
+ mainInteractionId: 'interaction-1',
501
+ owner: 'agent-1',
502
+ participants: {
503
+ 'agent-1': {
504
+ id: 'agent-1',
505
+ pType: 'Agent',
506
+ hasLeft: false,
507
+ consultState: 'consultCompleted',
508
+ isConsulted: false,
509
+ },
510
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
511
+ },
512
+ media: {
513
+ 'interaction-1': {
514
+ mediaResourceId: 'interaction-1',
515
+ mType: 'mainCall',
516
+ isHold: true,
517
+ participants: ['customer-1', 'agent-1'],
518
+ },
519
+ },
520
+ } as any,
521
+ });
522
+
523
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
524
+
525
+ const snapshot = service.getSnapshot();
526
+ expect(snapshot.value).toBe(TaskState.HELD);
527
+ expect(snapshot.context.consultInitiator).toBe(false);
528
+ expect(snapshot.context.consultCallHeld).toBe(false);
529
+ expect(snapshot.context.consultDestinationAgentJoined).toBe(false);
530
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
531
+ expect(snapshot.context.uiControls.consult.endConsult).toEqual({
532
+ isVisible: false,
533
+ isEnabled: false,
534
+ });
535
+ expect(snapshot.context.uiControls.main.hold).toEqual({
536
+ isVisible: true,
537
+ isEnabled: true,
538
+ });
539
+ expect(snapshot.context.uiControls.main.consult).toEqual({
540
+ isVisible: true,
541
+ isEnabled: true,
542
+ });
543
+ expect(snapshot.context.uiControls.main.transfer).toEqual({
544
+ isVisible: true,
545
+ isEnabled: true,
546
+ });
547
+ expect(snapshot.context.uiControls.main.recording).toEqual({
548
+ isVisible: true,
549
+ isEnabled: true,
550
+ });
551
+ });
552
+
553
+ it('enables main consult after ending consult before consultee answers (CONSULT_INITIATING -> CONSULT_END)', () => {
554
+ const service = startMachine();
555
+ const baseTaskData = createTaskData({
556
+ agentId: 'agent-1',
557
+ mediaResourceId: 'interaction-1',
558
+ });
559
+
560
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: baseTaskData});
561
+ service.send({type: TaskEvent.ASSIGN, taskData: baseTaskData});
562
+ service.send({
563
+ type: TaskEvent.CONSULT,
564
+ destination: 'agent-2',
565
+ destinationType: 'agent',
566
+ });
567
+ // Consult requested but consultee (agent-2) has not answered yet.
568
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
569
+
570
+ // Agent 1 ends the consult before agent-2 answers. Backend AgentConsultEnded:
571
+ // main on hold, self consultCompleted, no consult media, consultee not in participants.
572
+ const consultEndedTaskData = createTaskData({
573
+ agentId: 'agent-1',
574
+ mediaResourceId: 'interaction-1',
575
+ consultMediaResourceId: 'consult-media',
576
+ destAgentId: 'agent-2',
577
+ destinationType: 'Agent',
578
+ type: 'AgentConsultEnded' as any,
579
+ isConsulted: false,
580
+ interaction: {
581
+ state: 'connected',
582
+ interactionId: 'interaction-1',
583
+ mainInteractionId: 'interaction-1',
584
+ owner: 'agent-1',
585
+ participants: {
586
+ 'agent-1': {
587
+ id: 'agent-1',
588
+ pType: 'Agent',
589
+ hasLeft: false,
590
+ hasJoined: true,
591
+ consultState: 'consultCompleted',
592
+ isConsulted: false,
593
+ },
594
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
595
+ },
596
+ media: {
597
+ 'interaction-1': {
598
+ mediaResourceId: 'interaction-1',
599
+ mType: 'mainCall',
600
+ isHold: true,
601
+ participants: ['customer-1', 'agent-1'],
602
+ },
603
+ },
604
+ } as any,
605
+ });
606
+
607
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
608
+
609
+ const snapshot = service.getSnapshot();
610
+ expect(snapshot.value).toBe(TaskState.HELD);
611
+ expect(snapshot.context.consultInitiator).toBe(false);
612
+ expect(snapshot.context.consultDestinationAgentJoined).toBe(false);
613
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
614
+ expect(snapshot.context.uiControls.main.consult).toEqual({
615
+ isVisible: true,
616
+ isEnabled: true,
617
+ });
618
+ expect(snapshot.context.uiControls.consult.endConsult).toEqual({
619
+ isVisible: false,
620
+ isEnabled: false,
621
+ });
622
+ });
623
+
624
+ it('stays HELD and clears consult UI when AgentConsultEnded arrives on HELD state', () => {
625
+ const service = startMachine();
626
+ const heldTaskData = createTaskData({
627
+ agentId: 'agent-1',
628
+ mediaResourceId: 'interaction-1',
629
+ interaction: {
630
+ state: 'hold',
631
+ interactionId: 'interaction-1',
632
+ mainInteractionId: 'interaction-1',
633
+ owner: 'agent-1',
634
+ participants: {
635
+ 'agent-1': {
636
+ id: 'agent-1',
637
+ pType: 'Agent',
638
+ hasLeft: false,
639
+ consultState: 'consulting',
640
+ isConsulted: false,
641
+ },
642
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
643
+ },
644
+ media: {
645
+ 'interaction-1': {
646
+ mediaResourceId: 'interaction-1',
647
+ mType: 'mainCall',
648
+ isHold: true,
649
+ participants: ['customer-1', 'agent-1'],
650
+ },
651
+ },
652
+ } as any,
653
+ });
654
+
655
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: heldTaskData});
656
+ service.send({type: TaskEvent.ASSIGN, taskData: heldTaskData});
657
+ service.send({
658
+ type: TaskEvent.HOLD_INITIATED,
659
+ mediaResourceId: heldTaskData.mediaResourceId,
660
+ });
661
+ service.send({
662
+ type: TaskEvent.HOLD_SUCCESS,
663
+ mediaResourceId: heldTaskData.mediaResourceId,
664
+ taskData: heldTaskData,
665
+ });
666
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
667
+
668
+ const consultEndedTaskData = createTaskData({
669
+ agentId: 'agent-1',
670
+ mediaResourceId: 'interaction-1',
671
+ type: 'AgentConsultEnded' as any,
672
+ isConsulted: false,
673
+ interaction: {
674
+ state: 'hold',
675
+ interactionId: 'interaction-1',
676
+ mainInteractionId: 'interaction-1',
677
+ owner: 'agent-1',
678
+ participants: {
679
+ 'agent-1': {
680
+ id: 'agent-1',
681
+ pType: 'Agent',
682
+ hasLeft: false,
683
+ consultState: 'consultCompleted',
684
+ isConsulted: false,
685
+ },
686
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
687
+ },
688
+ media: {
689
+ 'interaction-1': {
690
+ mediaResourceId: 'interaction-1',
691
+ mType: 'mainCall',
692
+ isHold: true,
693
+ participants: ['customer-1', 'agent-1'],
694
+ },
695
+ },
696
+ } as any,
697
+ });
698
+
699
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
700
+
701
+ const snapshot = service.getSnapshot();
702
+ expect(snapshot.value).toBe(TaskState.HELD);
703
+ expect(snapshot.context.consultInitiator).toBe(false);
704
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
705
+ expect(snapshot.context.uiControls.main.hold).toEqual({
706
+ isVisible: true,
707
+ isEnabled: true,
708
+ });
709
+ expect(snapshot.context.uiControls.consult.endConsult).toEqual({
710
+ isVisible: false,
711
+ isEnabled: false,
712
+ });
713
+ });
714
+
715
+ it('keeps main.consult enabled on HELD after AgentConsultFailed then AgentConsultEnded (RONA)', () => {
716
+ const service = startMachine();
717
+ const heldTaskData = createTaskData({
718
+ agentId: 'agent-1',
719
+ mediaResourceId: 'interaction-1',
720
+ type: 'AgentContactHeld' as any,
721
+ interaction: {
722
+ state: 'hold',
723
+ interactionId: 'interaction-1',
724
+ mainInteractionId: 'interaction-1',
725
+ owner: 'agent-1',
726
+ participants: {
727
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
728
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
729
+ } as any,
730
+ media: {
731
+ 'interaction-1': {
732
+ mediaResourceId: 'interaction-1',
733
+ mType: 'mainCall',
734
+ isHold: true,
735
+ participants: ['customer-1', 'agent-1'],
736
+ },
737
+ } as any,
738
+ } as any,
739
+ });
740
+
741
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: heldTaskData});
742
+ service.send({type: TaskEvent.ASSIGN, taskData: heldTaskData});
743
+ service.send({
744
+ type: TaskEvent.HOLD_INITIATED,
745
+ mediaResourceId: heldTaskData.mediaResourceId,
746
+ });
747
+ service.send({
748
+ type: TaskEvent.HOLD_SUCCESS,
749
+ mediaResourceId: heldTaskData.mediaResourceId,
750
+ taskData: heldTaskData,
751
+ });
752
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
753
+
754
+ const consultCreatedTaskData = createTaskData({
755
+ agentId: 'agent-1',
756
+ mediaResourceId: 'interaction-1',
757
+ consultMediaResourceId: 'consult-media',
758
+ consultingAgentId: 'agent-1',
759
+ destAgentId: 'agent-2',
760
+ isConsulted: false,
761
+ type: 'AgentConsultCreated' as any,
762
+ interaction: {
763
+ state: 'consult',
764
+ interactionId: 'interaction-1',
765
+ mainInteractionId: 'interaction-1',
766
+ owner: 'agent-1',
767
+ participants: {
768
+ 'agent-1': {
769
+ id: 'agent-1',
770
+ pType: 'Agent',
771
+ hasLeft: false,
772
+ consultState: 'consultInitiated',
773
+ isConsulted: false,
774
+ },
775
+ 'agent-2': {
776
+ id: 'agent-2',
777
+ pType: 'Agent',
778
+ hasLeft: false,
779
+ consultState: 'consultReserved',
780
+ isConsulted: true,
781
+ },
782
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
783
+ } as any,
784
+ media: {
785
+ 'interaction-1': {
786
+ mediaResourceId: 'interaction-1',
787
+ mType: 'mainCall',
788
+ isHold: true,
789
+ participants: ['customer-1', 'agent-1'],
790
+ },
791
+ 'consult-media': {
792
+ mediaResourceId: 'consult-media',
793
+ mType: 'consult',
794
+ participants: ['agent-2', 'agent-1'],
795
+ },
796
+ } as any,
797
+ } as any,
798
+ });
799
+ service.send({type: TaskEvent.CONSULT_CREATED, taskData: consultCreatedTaskData});
800
+
801
+ const consultFailedTaskData = createTaskData({
802
+ agentId: 'agent-1',
803
+ mediaResourceId: 'interaction-1',
804
+ consultMediaResourceId: 'consult-media',
805
+ consultingAgentId: 'agent-1',
806
+ destAgentId: 'agent-2',
807
+ isConsulted: false,
808
+ type: 'AgentConsultFailed' as any,
809
+ interaction: {
810
+ state: 'connected',
811
+ interactionId: 'interaction-1',
812
+ mainInteractionId: 'interaction-1',
813
+ owner: 'agent-1',
814
+ participants: {
815
+ 'agent-1': {
816
+ id: 'agent-1',
817
+ pType: 'Agent',
818
+ hasLeft: false,
819
+ consultState: 'consultCompleted',
820
+ isConsulted: false,
821
+ },
822
+ 'agent-2': {
823
+ id: 'agent-2',
824
+ pType: 'Agent',
825
+ hasLeft: false,
826
+ consultState: 'consultReserved',
827
+ isConsulted: true,
828
+ },
829
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
830
+ } as any,
831
+ media: {
832
+ 'interaction-1': {
833
+ mediaResourceId: 'interaction-1',
834
+ mType: 'mainCall',
835
+ isHold: true,
836
+ participants: ['customer-1', 'agent-1'],
837
+ },
838
+ } as any,
839
+ } as any,
840
+ });
841
+ service.send({type: TaskEvent.CONSULT_FAILED, taskData: consultFailedTaskData});
842
+
843
+ const consultEndedTaskData = createTaskData({
844
+ agentId: 'agent-1',
845
+ mediaResourceId: 'interaction-1',
846
+ consultMediaResourceId: 'consult-media',
847
+ isConsulted: false,
848
+ type: 'AgentConsultEnded' as any,
849
+ interaction: consultFailedTaskData.interaction,
850
+ });
851
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
852
+
853
+ const snapshot = service.getSnapshot();
854
+ expect(snapshot.value).toBe(TaskState.HELD);
855
+ expect(snapshot.context.consultInitiator).toBe(false);
856
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
857
+ expect(snapshot.context.uiControls.main.consult).toEqual({
858
+ isVisible: true,
859
+ isEnabled: true,
860
+ });
861
+ expect(snapshot.context.uiControls.main.hold).toEqual({
862
+ isVisible: true,
863
+ isEnabled: true,
864
+ });
865
+ expect(snapshot.context.uiControls.main.transfer).toEqual({
866
+ isVisible: true,
867
+ isEnabled: true,
868
+ });
869
+ });
870
+
871
+ it('returns to main leg (HELD) and does not clear the task after AgentConsultFailed then AgentConsultEnded while CONSULTING (RONA)', () => {
872
+ const service = startMachine();
873
+ const heldTaskData = createTaskData({
874
+ agentId: 'agent-1',
875
+ mediaResourceId: 'interaction-1',
876
+ type: 'AgentContactHeld' as any,
877
+ interaction: {
878
+ state: 'hold',
879
+ interactionId: 'interaction-1',
880
+ mainInteractionId: 'interaction-1',
881
+ owner: 'agent-1',
882
+ participants: {
883
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
884
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
885
+ } as any,
886
+ media: {
887
+ 'interaction-1': {
888
+ mediaResourceId: 'interaction-1',
889
+ mType: 'mainCall',
890
+ isHold: true,
891
+ participants: ['customer-1', 'agent-1'],
892
+ },
893
+ } as any,
894
+ } as any,
895
+ });
896
+
897
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: heldTaskData});
898
+ service.send({type: TaskEvent.ASSIGN, taskData: heldTaskData});
899
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: heldTaskData.mediaResourceId});
900
+ service.send({
901
+ type: TaskEvent.HOLD_SUCCESS,
902
+ mediaResourceId: heldTaskData.mediaResourceId,
903
+ taskData: heldTaskData,
904
+ });
905
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
906
+
907
+ // Agent 1 initiates a consult and AgentConsulting arrives during ringing, moving the
908
+ // initiator into CONSULTING before the consultee answers.
909
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-2', destinationType: 'agent'});
910
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
911
+ service.send({type: TaskEvent.CONSULT_SUCCESS});
912
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
913
+ expect(service.getSnapshot().context.consultInitiator).toBe(true);
914
+
915
+ // Consultee RONAs: AgentConsultFailed arrives while still in CONSULTING. Main stays held.
916
+ const consultFailedTaskData = createTaskData({
917
+ agentId: 'agent-1',
918
+ mediaResourceId: 'interaction-1',
919
+ consultMediaResourceId: 'consult-media',
920
+ consultingAgentId: 'agent-1',
921
+ destAgentId: 'agent-2',
922
+ isConsulted: false,
923
+ type: 'AgentConsultFailed' as any,
924
+ interaction: {
925
+ state: 'consult',
926
+ interactionId: 'interaction-1',
927
+ mainInteractionId: 'interaction-1',
928
+ owner: 'agent-1',
929
+ participants: {
930
+ 'agent-1': {
931
+ id: 'agent-1',
932
+ pType: 'Agent',
933
+ hasLeft: false,
934
+ consultState: 'consultCompleted',
935
+ isConsulted: false,
936
+ },
937
+ 'agent-2': {
938
+ id: 'agent-2',
939
+ pType: 'Agent',
940
+ hasLeft: false,
941
+ hasJoined: false,
942
+ consultState: 'consultReserved',
943
+ isConsulted: true,
944
+ },
945
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
946
+ } as any,
947
+ media: {
948
+ 'interaction-1': {
949
+ mediaResourceId: 'interaction-1',
950
+ mType: 'mainCall',
951
+ isHold: true,
952
+ participants: ['customer-1', 'agent-1'],
953
+ },
954
+ 'consult-media': {
955
+ mediaResourceId: 'consult-media',
956
+ mType: 'consult',
957
+ participants: ['agent-2', 'agent-1'],
958
+ },
959
+ } as any,
960
+ } as any,
961
+ });
962
+ service.send({type: TaskEvent.CONSULT_FAILED, taskData: consultFailedTaskData});
963
+ // The initiator must leave CONSULTING and fall back to the held main leg (not stay in
964
+ // CONSULTING, which would let the trailing AgentConsultEnded terminate the task).
965
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
966
+
967
+ // AgentConsultEnded closes out the consult leg.
968
+ const consultEndedTaskData = createTaskData({
969
+ agentId: 'agent-1',
970
+ mediaResourceId: 'interaction-1',
971
+ consultMediaResourceId: 'consult-media',
972
+ isConsulted: false,
973
+ type: 'AgentConsultEnded' as any,
974
+ interaction: {
975
+ state: 'connected',
976
+ interactionId: 'interaction-1',
977
+ mainInteractionId: 'interaction-1',
978
+ owner: 'agent-1',
979
+ participants: {
980
+ 'agent-1': {
981
+ id: 'agent-1',
982
+ pType: 'Agent',
983
+ hasLeft: false,
984
+ consultState: 'consultCompleted',
985
+ isConsulted: false,
986
+ },
987
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
988
+ } as any,
989
+ media: {
990
+ 'interaction-1': {
991
+ mediaResourceId: 'interaction-1',
992
+ mType: 'mainCall',
993
+ isHold: true,
994
+ participants: ['customer-1', 'agent-1'],
995
+ },
996
+ } as any,
997
+ } as any,
998
+ });
999
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
1000
+
1001
+ const snapshot = service.getSnapshot();
1002
+ // The task must remain on the main leg (HELD) and never reach TERMINATED.
1003
+ expect(snapshot.value).toBe(TaskState.HELD);
1004
+ expect(snapshot.value).not.toBe(TaskState.TERMINATED);
1005
+ expect(snapshot.context.consultInitiator).toBe(false);
1006
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
1007
+ expect(snapshot.context.uiControls.main.consult).toEqual({
1008
+ isVisible: true,
1009
+ isEnabled: true,
1010
+ });
1011
+ });
1012
+
1013
+ it('transitions CONSULT_INITIATING to CONSULTING on CONSULTING_ACTIVE and marks destination joined', () => {
1014
+ const service = startMachine();
1015
+ const baseTaskData = createTaskData();
1016
+ const consultingTaskData = createTaskData({
1017
+ agentId: 'agent-1',
1018
+ isConsulted: false,
1019
+ consultMediaResourceId: 'consult-media',
1020
+ interaction: {
1021
+ state: 'consulting',
1022
+ interactionId: 'interaction-1',
1023
+ mainInteractionId: 'interaction-1',
1024
+ participants: {
1025
+ 'agent-1': {
1026
+ id: 'agent-1',
1027
+ pType: 'Agent',
1028
+ hasLeft: false,
1029
+ consultState: 'consulting',
1030
+ isConsulted: false,
1031
+ },
1032
+ 'agent-2': {
1033
+ id: 'agent-2',
1034
+ pType: 'Agent',
1035
+ hasLeft: false,
1036
+ hasJoined: true,
1037
+ consultState: 'consulting',
1038
+ isConsulted: true,
1039
+ },
1040
+ } as any,
1041
+ media: {
1042
+ 'interaction-1': {mediaResourceId: 'interaction-1', mType: 'mainCall', isHold: true},
1043
+ 'consult-media': {
1044
+ mediaResourceId: 'consult-media',
1045
+ mType: 'consult',
1046
+ participants: ['agent-1', 'agent-2'],
1047
+ },
1048
+ } as any,
1049
+ } as any,
1050
+ });
1051
+
1052
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: baseTaskData});
1053
+ service.send({type: TaskEvent.ASSIGN, taskData: baseTaskData});
1054
+ service.send({
1055
+ type: TaskEvent.CONSULT,
1056
+ destination: 'agent-2',
1057
+ destinationType: 'agent',
1058
+ });
1059
+
1060
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
1061
+
1062
+ service.send({
1063
+ type: TaskEvent.CONSULTING_ACTIVE,
1064
+ consultDestinationAgentJoined: true,
1065
+ taskData: consultingTaskData,
1066
+ });
1067
+
1068
+ const snapshot = service.getSnapshot();
1069
+ expect(snapshot.value).toBe(TaskState.CONSULTING);
1070
+ expect(snapshot.context.consultInitiator).toBe(true);
1071
+ expect(snapshot.context.consultDestinationAgentJoined).toBe(true);
1072
+ });
1073
+
474
1074
  it('keeps consultDestinationAgentJoined false while consultee is only reserved, then sets true on actual join', () => {
475
1075
  const service = startMachine();
476
1076
  const pendingTaskData = createTaskData({
@@ -708,6 +1308,82 @@ describe('Task state machine', () => {
708
1308
  expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
709
1309
  });
710
1310
 
1311
+ it('transitions CONFERENCING to CONSULTING on CONSULTING_ACTIVE and marks DN consult joined', () => {
1312
+ const service = startMachine();
1313
+ const conferenceTaskData = createConferenceConsultTaskData({
1314
+ interactionState: 'conference',
1315
+ includeSecondAgent: true,
1316
+ conferenceHoldParticipant: false,
1317
+ });
1318
+
1319
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1320
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
1321
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
1322
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1323
+
1324
+ const dnConsultTaskData = createTaskData({
1325
+ type: 'AgentConsulting' as any,
1326
+ agentId: 'agent-1',
1327
+ consultingAgentId: 'agent-1',
1328
+ isConsulted: false,
1329
+ destinationType: 'DN',
1330
+ consultMediaResourceId: 'consult-media-1',
1331
+ interaction: {
1332
+ state: 'conference',
1333
+ mainInteractionId: 'interaction-1',
1334
+ interactionId: 'interaction-1',
1335
+ participants: {
1336
+ 'agent-1': {
1337
+ id: 'agent-1',
1338
+ pType: 'Agent',
1339
+ hasLeft: false,
1340
+ consultState: 'consulting',
1341
+ isConsulted: false,
1342
+ },
1343
+ 'agent-2': {
1344
+ id: 'agent-2',
1345
+ pType: 'Agent',
1346
+ hasLeft: false,
1347
+ consultState: 'conferencing',
1348
+ },
1349
+ 'dn-dest': {
1350
+ id: 'dn-dest',
1351
+ pType: 'DN',
1352
+ hasLeft: false,
1353
+ hasJoined: true,
1354
+ },
1355
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
1356
+ },
1357
+ media: {
1358
+ 'interaction-1': {
1359
+ mediaResourceId: 'interaction-1',
1360
+ mType: 'mainCall',
1361
+ participants: ['agent-1', 'agent-2', 'customer-1'],
1362
+ isHold: false,
1363
+ },
1364
+ 'consult-media-1': {
1365
+ mediaResourceId: 'consult-media-1',
1366
+ mType: 'consult',
1367
+ participants: ['agent-1', 'dn-dest'],
1368
+ isHold: false,
1369
+ },
1370
+ },
1371
+ } as any,
1372
+ });
1373
+
1374
+ service.send({
1375
+ type: TaskEvent.CONSULTING_ACTIVE,
1376
+ consultDestinationAgentJoined: true,
1377
+ taskData: dnConsultTaskData,
1378
+ });
1379
+
1380
+ const snapshot = service.getSnapshot();
1381
+ expect(snapshot.value).toBe(TaskState.CONSULTING);
1382
+ expect(snapshot.context.consultDestinationAgentJoined).toBe(true);
1383
+ expect(snapshot.context.consultFromConference).toBe(true);
1384
+ expect(snapshot.context.consultDestinationType).toBe('entryPoint');
1385
+ });
1386
+
711
1387
  it('transitions to conferencing when merge event is received', () => {
712
1388
  const service = startMachine();
713
1389
  const taskData = createTaskData({consultingAgentId: 'agent-1'});