@webex/contact-center 3.10.0-next.2 → 3.10.0-next.21

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 (103) hide show
  1. package/dist/cc.js +13 -1
  2. package/dist/cc.js.map +1 -1
  3. package/dist/config.js.map +1 -1
  4. package/dist/constants.js.map +1 -1
  5. package/dist/index.js +17 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/logger-proxy.js.map +1 -1
  8. package/dist/metrics/MetricsManager.js +2 -1
  9. package/dist/metrics/MetricsManager.js.map +1 -1
  10. package/dist/metrics/behavioral-events.js +12 -0
  11. package/dist/metrics/behavioral-events.js.map +1 -1
  12. package/dist/metrics/constants.js +4 -0
  13. package/dist/metrics/constants.js.map +1 -1
  14. package/dist/services/AddressBook.js +2 -3
  15. package/dist/services/AddressBook.js.map +1 -1
  16. package/dist/services/EntryPoint.js +2 -3
  17. package/dist/services/EntryPoint.js.map +1 -1
  18. package/dist/services/Queue.js +2 -3
  19. package/dist/services/Queue.js.map +1 -1
  20. package/dist/services/WebCallingService.js +1 -1
  21. package/dist/services/WebCallingService.js.map +1 -1
  22. package/dist/services/agent/index.js +1 -2
  23. package/dist/services/agent/index.js.map +1 -1
  24. package/dist/services/agent/types.js +10 -0
  25. package/dist/services/agent/types.js.map +1 -1
  26. package/dist/services/config/Util.js.map +1 -1
  27. package/dist/services/config/constants.js.map +1 -1
  28. package/dist/services/config/index.js +1 -1
  29. package/dist/services/config/index.js.map +1 -1
  30. package/dist/services/config/types.js +2 -2
  31. package/dist/services/config/types.js.map +1 -1
  32. package/dist/services/constants.js.map +1 -1
  33. package/dist/services/core/Err.js.map +1 -1
  34. package/dist/services/core/GlobalTypes.js.map +1 -1
  35. package/dist/services/core/Utils.js +92 -74
  36. package/dist/services/core/Utils.js.map +1 -1
  37. package/dist/services/core/WebexRequest.js +1 -2
  38. package/dist/services/core/WebexRequest.js.map +1 -1
  39. package/dist/services/core/aqm-reqs.js +2 -3
  40. package/dist/services/core/aqm-reqs.js.map +1 -1
  41. package/dist/services/core/constants.js +17 -1
  42. package/dist/services/core/constants.js.map +1 -1
  43. package/dist/services/core/types.js.map +1 -1
  44. package/dist/services/core/websocket/WebSocketManager.js +1 -2
  45. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  46. package/dist/services/core/websocket/connection-service.js +1 -1
  47. package/dist/services/core/websocket/connection-service.js.map +1 -1
  48. package/dist/services/core/websocket/keepalive.worker.js.map +1 -1
  49. package/dist/services/core/websocket/types.js.map +1 -1
  50. package/dist/services/index.js +1 -1
  51. package/dist/services/index.js.map +1 -1
  52. package/dist/services/task/AutoWrapup.js +1 -1
  53. package/dist/services/task/AutoWrapup.js.map +1 -1
  54. package/dist/services/task/TaskManager.js +177 -56
  55. package/dist/services/task/TaskManager.js.map +1 -1
  56. package/dist/services/task/TaskUtils.js +122 -5
  57. package/dist/services/task/TaskUtils.js.map +1 -1
  58. package/dist/services/task/constants.js +3 -1
  59. package/dist/services/task/constants.js.map +1 -1
  60. package/dist/services/task/contact.js +0 -2
  61. package/dist/services/task/contact.js.map +1 -1
  62. package/dist/services/task/dialer.js.map +1 -1
  63. package/dist/services/task/index.js +46 -40
  64. package/dist/services/task/index.js.map +1 -1
  65. package/dist/services/task/types.js +377 -4
  66. package/dist/services/task/types.js.map +1 -1
  67. package/dist/types/cc.d.ts +6 -0
  68. package/dist/types/index.d.ts +1 -1
  69. package/dist/types/metrics/constants.d.ts +4 -0
  70. package/dist/types/services/config/types.d.ts +4 -4
  71. package/dist/types/services/core/Utils.d.ts +32 -17
  72. package/dist/types/services/core/constants.d.ts +14 -0
  73. package/dist/types/services/task/TaskUtils.d.ts +59 -3
  74. package/dist/types/services/task/constants.d.ts +2 -0
  75. package/dist/types/services/task/types.d.ts +57 -13
  76. package/dist/types.js +5 -0
  77. package/dist/types.js.map +1 -1
  78. package/dist/utils/PageCache.js +1 -1
  79. package/dist/utils/PageCache.js.map +1 -1
  80. package/dist/webex-config.js.map +1 -1
  81. package/dist/webex.js +2 -2
  82. package/dist/webex.js.map +1 -1
  83. package/package.json +8 -8
  84. package/src/cc.ts +12 -0
  85. package/src/index.ts +1 -0
  86. package/src/metrics/behavioral-events.ts +12 -0
  87. package/src/metrics/constants.ts +4 -0
  88. package/src/services/config/types.ts +2 -2
  89. package/src/services/core/Utils.ts +101 -85
  90. package/src/services/core/constants.ts +16 -0
  91. package/src/services/task/TaskManager.ts +204 -36
  92. package/src/services/task/TaskUtils.ts +145 -5
  93. package/src/services/task/constants.ts +2 -0
  94. package/src/services/task/index.ts +50 -63
  95. package/src/services/task/types.ts +60 -13
  96. package/test/unit/spec/cc.ts +1 -0
  97. package/test/unit/spec/metrics/behavioral-events.ts +14 -0
  98. package/test/unit/spec/services/core/Utils.ts +262 -31
  99. package/test/unit/spec/services/task/TaskManager.ts +748 -5
  100. package/test/unit/spec/services/task/TaskUtils.ts +311 -9
  101. package/test/unit/spec/services/task/index.ts +323 -68
  102. package/umd/contact-center.min.js +2 -2
  103. package/umd/contact-center.min.js.map +1 -1
@@ -479,6 +479,73 @@ describe('TaskManager', () => {
479
479
  );
480
480
  });
481
481
 
482
+ it('should set isConferenceInProgress correctly when creating task via AGENT_CONTACT with conference in progress', () => {
483
+ const testAgentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
484
+ taskManager.setAgentId(testAgentId);
485
+ taskManager.taskCollection = [];
486
+
487
+ const payload = {
488
+ data: {
489
+ ...initalPayload.data,
490
+ type: CC_EVENTS.AGENT_CONTACT,
491
+ interaction: {
492
+ mediaType: 'telephony',
493
+ state: 'conference',
494
+ participants: {
495
+ [testAgentId]: { pType: 'Agent', hasLeft: false },
496
+ 'agent-2': { pType: 'Agent', hasLeft: false },
497
+ 'customer-1': { pType: 'Customer', hasLeft: false },
498
+ },
499
+ media: {
500
+ [taskId]: {
501
+ mType: 'mainCall',
502
+ participants: [testAgentId, 'agent-2', 'customer-1'],
503
+ },
504
+ },
505
+ },
506
+ },
507
+ };
508
+
509
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
510
+
511
+ const createdTask = taskManager.getTask(taskId);
512
+ expect(createdTask).toBeDefined();
513
+ expect(createdTask.data.isConferenceInProgress).toBe(true);
514
+ });
515
+
516
+ it('should set isConferenceInProgress to false when creating task via AGENT_CONTACT with only one agent', () => {
517
+ const testAgentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
518
+ taskManager.setAgentId(testAgentId);
519
+ taskManager.taskCollection = [];
520
+
521
+ const payload = {
522
+ data: {
523
+ ...initalPayload.data,
524
+ type: CC_EVENTS.AGENT_CONTACT,
525
+ interaction: {
526
+ mediaType: 'telephony',
527
+ state: 'connected',
528
+ participants: {
529
+ [testAgentId]: { pType: 'Agent', hasLeft: false },
530
+ 'customer-1': { pType: 'Customer', hasLeft: false },
531
+ },
532
+ media: {
533
+ [taskId]: {
534
+ mType: 'mainCall',
535
+ participants: [testAgentId, 'customer-1'],
536
+ },
537
+ },
538
+ },
539
+ },
540
+ };
541
+
542
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
543
+
544
+ const createdTask = taskManager.getTask(taskId);
545
+ expect(createdTask).toBeDefined();
546
+ expect(createdTask.data.isConferenceInProgress).toBe(false);
547
+ });
548
+
482
549
  it('should emit TASK_END event on AGENT_WRAPUP event', () => {
483
550
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
484
551
 
@@ -606,14 +673,181 @@ describe('TaskManager', () => {
606
673
  expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
607
674
  });
608
675
 
609
- it('should remove currentTask from taskCollection on AGENT_OUTBOUND_FAILED event', () => {
676
+ describe('Auto-Answer Functionality', () => {
677
+ it('should emit both TASK_OFFER_CONTACT and TASK_AUTO_ANSWERED events when auto-answer succeeds', async () => {
678
+ // Step 1: Create the task first with initial payload
679
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
680
+
681
+ const task = taskManager.getTask(taskId);
682
+ const taskEmitSpy = jest.spyOn(task, 'emit');
683
+ const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
684
+ const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
685
+
686
+ // Step 2: Trigger AGENT_OFFER_CONTACT with auto-answer
687
+ const autoAnswerPayload = {
688
+ data: {
689
+ ...initalPayload.data,
690
+ type: CC_EVENTS.AGENT_OFFER_CONTACT,
691
+ isAutoAnswering: true,
692
+ interaction: {
693
+ ...initalPayload.data.interaction,
694
+ mediaType: 'telephony',
695
+ state: 'new',
696
+ },
697
+ },
698
+ };
699
+
700
+ webSocketManagerMock.emit('message', JSON.stringify(autoAnswerPayload));
701
+
702
+ // Wait for async auto-answer to complete
703
+ await new Promise(process.nextTick);
704
+
705
+ // Verify accept was called
706
+ expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
707
+
708
+ // Verify BOTH events were emitted
709
+ expect(taskManagerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OFFER_CONTACT, task);
710
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
711
+ });
712
+
713
+ it('should NOT emit TASK_AUTO_ANSWERED event when auto-answer fails', async () => {
714
+ // Step 1: Create the task first with initial payload
715
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
716
+
717
+ const task = taskManager.getTask(taskId);
718
+ const taskEmitSpy = jest.spyOn(task, 'emit');
719
+ const taskAcceptSpy = jest.spyOn(task, 'accept').mockRejectedValue(new Error('Accept failed'));
720
+
721
+ // Step 2: Trigger AGENT_OFFER_CONTACT with auto-answer (will fail)
722
+ const autoAnswerPayload = {
723
+ data: {
724
+ ...initalPayload.data,
725
+ type: CC_EVENTS.AGENT_OFFER_CONTACT,
726
+ isAutoAnswering: true,
727
+ interaction: {
728
+ ...initalPayload.data.interaction,
729
+ mediaType: 'telephony',
730
+ state: 'new',
731
+ },
732
+ },
733
+ };
734
+
735
+ webSocketManagerMock.emit('message', JSON.stringify(autoAnswerPayload));
736
+
737
+ // Wait for async auto-answer to complete
738
+ await new Promise(process.nextTick);
739
+
740
+ // Verify accept was called
741
+ expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
742
+
743
+ // Verify TASK_AUTO_ANSWERED event was NOT emitted on failure
744
+ expect(taskEmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
745
+ });
746
+
747
+ it('should emit both TASK_OFFER_CONSULT and TASK_AUTO_ANSWERED events for consult with auto-answer', async () => {
748
+ // Step 1: Create the task first with initial payload
749
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
750
+
751
+ const task = taskManager.getTask(taskId);
752
+ const taskEmitSpy = jest.spyOn(task, 'emit');
753
+ const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
754
+
755
+ // Step 2: Trigger AGENT_OFFER_CONSULT with auto-answer
756
+ const consultAutoAnswerPayload = {
757
+ data: {
758
+ ...initalPayload.data,
759
+ type: CC_EVENTS.AGENT_OFFER_CONSULT,
760
+ isAutoAnswering: true,
761
+ isConsulted: true,
762
+ interaction: {
763
+ ...initalPayload.data.interaction,
764
+ mediaType: 'telephony',
765
+ state: 'consult',
766
+ },
767
+ },
768
+ };
769
+
770
+ webSocketManagerMock.emit('message', JSON.stringify(consultAutoAnswerPayload));
771
+
772
+ // Wait for async auto-answer to complete
773
+ await new Promise(process.nextTick);
774
+
775
+ // Verify accept was called
776
+ expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
777
+
778
+ // Verify BOTH events were emitted
779
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OFFER_CONSULT, task);
780
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
781
+
782
+ // Verify isConsulted flag is set correctly
783
+ expect(task.data.isConsulted).toBe(true);
784
+ });
785
+
786
+ it('should NOT emit TASK_AUTO_ANSWERED when isAutoAnswering is false', async () => {
787
+ // Step 1: Create the task first with initial payload
788
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
789
+
790
+ const task = taskManager.getTask(taskId);
791
+ const taskEmitSpy = jest.spyOn(task, 'emit');
792
+ const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
793
+
794
+ // Step 2: Trigger AGENT_OFFER_CONTACT without auto-answer
795
+ const normalPayload = {
796
+ data: {
797
+ ...initalPayload.data,
798
+ type: CC_EVENTS.AGENT_OFFER_CONTACT,
799
+ isAutoAnswering: false,
800
+ interaction: {
801
+ ...initalPayload.data.interaction,
802
+ mediaType: 'telephony',
803
+ state: 'new',
804
+ },
805
+ },
806
+ };
807
+
808
+ webSocketManagerMock.emit('message', JSON.stringify(normalPayload));
809
+
810
+ // Wait for any async operations
811
+ await new Promise(process.nextTick);
812
+
813
+ // Verify accept was NOT called
814
+ expect(taskAcceptSpy).not.toHaveBeenCalled();
815
+
816
+ // Verify TASK_AUTO_ANSWERED event was NOT emitted
817
+ expect(taskEmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, expect.anything());
818
+ });
819
+ });
820
+
821
+ it('should NOT remove OUTDIAL task from taskCollection on AGENT_OUTBOUND_FAILED when terminated (wrap-up flow)', () => {
822
+ const task = taskManager.getTask(taskId);
823
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
824
+ task.data = {
825
+ ...task.data,
826
+ ...newData,
827
+ interaction: {
828
+ ...task.data.interaction,
829
+ ...newData.interaction,
830
+ outboundType: 'OUTDIAL',
831
+ state: 'new',
832
+ isTerminated: true,
833
+ },
834
+ };
835
+ return task;
836
+ });
837
+ task.unregisterWebCallListeners = jest.fn();
838
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
839
+
610
840
  const payload = {
611
841
  data: {
612
842
  type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
613
843
  agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
614
844
  eventTime: 1733211616959,
615
845
  eventType: 'RoutingMessage',
616
- interaction: {},
846
+ interaction: {
847
+ outboundType: 'OUTDIAL',
848
+ state: 'new',
849
+ isTerminated: true,
850
+ },
617
851
  interactionId: taskId,
618
852
  orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
619
853
  trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
@@ -621,14 +855,220 @@ describe('TaskManager', () => {
621
855
  destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
622
856
  owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
623
857
  queueMgr: 'aqm',
858
+ reason: 'CUSTOMER_BUSY',
859
+ reasonCode: 1022,
624
860
  },
625
861
  };
626
862
 
627
- taskManager.taskCollection[taskId] = taskManager.getTask(taskId);
863
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
864
+
865
+ expect(taskManager.getTask(taskId)).toBeDefined();
866
+ expect(removeTaskSpy).not.toHaveBeenCalled();
867
+ });
868
+
869
+ it('should emit TASK_OUTDIAL_FAILED event on AGENT_OUTBOUND_FAILED', () => {
870
+ const task = taskManager.getTask(taskId);
871
+ task.updateTaskData = jest.fn().mockReturnValue(task);
872
+ const taskEmitSpy = jest.spyOn(task, 'emit');
873
+ const payload = {
874
+ data: {
875
+ type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
876
+ interactionId: taskId,
877
+ reason: 'CUSTOMER_BUSY',
878
+ },
879
+ };
880
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
881
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OUTDIAL_FAILED, 'CUSTOMER_BUSY');
882
+ });
883
+
884
+ it('should handle AGENT_OUTBOUND_FAILED gracefully when task is undefined', () => {
885
+ const payload = {
886
+ data: {
887
+ type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
888
+ interactionId: 'non-existent-task-id',
889
+ reason: 'CUSTOMER_BUSY',
890
+ },
891
+ };
892
+ // Should not throw error when task doesn't exist
893
+ expect(() => {
894
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
895
+ }).not.toThrow();
896
+ });
897
+
898
+ it('should NOT remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp exists', () => {
899
+ const task = taskManager.getTask(taskId);
900
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
901
+ task.data = {
902
+ ...task.data,
903
+ ...newData,
904
+ interaction: {
905
+ ...task.data.interaction,
906
+ outboundType: 'OUTDIAL',
907
+ state: 'new',
908
+ mediaType: 'telephony',
909
+ },
910
+ agentsPendingWrapUp: ['agent-123'],
911
+ };
912
+ return task;
913
+ });
914
+ task.unregisterWebCallListeners = jest.fn();
915
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
916
+
917
+ const payload = {
918
+ data: {
919
+ type: CC_EVENTS.CONTACT_ENDED,
920
+ interactionId: taskId,
921
+ interaction: {
922
+ outboundType: 'OUTDIAL',
923
+ state: 'new',
924
+ mediaType: 'telephony',
925
+ },
926
+ agentsPendingWrapUp: ['agent-123'],
927
+ },
928
+ };
929
+
930
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
931
+
932
+ expect(removeTaskSpy).not.toHaveBeenCalled();
933
+ expect(taskManager.getTask(taskId)).toBeDefined();
934
+ });
935
+
936
+ it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is empty', () => {
937
+ const task = taskManager.getTask(taskId);
938
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
939
+ task.data = {
940
+ ...task.data,
941
+ ...newData,
942
+ interaction: {
943
+ ...task.data.interaction,
944
+ outboundType: 'OUTDIAL',
945
+ state: 'new',
946
+ mediaType: 'telephony',
947
+ },
948
+ agentsPendingWrapUp: [],
949
+ };
950
+ return task;
951
+ });
952
+ task.unregisterWebCallListeners = jest.fn();
953
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
954
+
955
+ const payload = {
956
+ data: {
957
+ type: CC_EVENTS.CONTACT_ENDED,
958
+ interactionId: taskId,
959
+ interaction: {
960
+ outboundType: 'OUTDIAL',
961
+ state: 'new',
962
+ mediaType: 'telephony',
963
+ },
964
+ agentsPendingWrapUp: [],
965
+ },
966
+ };
967
+
968
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
969
+
970
+ expect(removeTaskSpy).toHaveBeenCalled();
971
+ });
972
+
973
+ it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is undefined', () => {
974
+ const task = taskManager.getTask(taskId);
975
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
976
+ task.data = {
977
+ ...task.data,
978
+ ...newData,
979
+ interaction: {
980
+ ...task.data.interaction,
981
+ outboundType: 'OUTDIAL',
982
+ state: 'new',
983
+ mediaType: 'telephony',
984
+ },
985
+ // agentsPendingWrapUp is undefined
986
+ };
987
+ return task;
988
+ });
989
+ task.unregisterWebCallListeners = jest.fn();
990
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
991
+
992
+ const payload = {
993
+ data: {
994
+ type: CC_EVENTS.CONTACT_ENDED,
995
+ interactionId: taskId,
996
+ interaction: {
997
+ outboundType: 'OUTDIAL',
998
+ state: 'new',
999
+ mediaType: 'telephony',
1000
+ },
1001
+ // agentsPendingWrapUp not included
1002
+ },
1003
+ };
1004
+
1005
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1006
+
1007
+ expect(removeTaskSpy).toHaveBeenCalled();
1008
+ });
1009
+
1010
+ it('should handle CONTACT_ENDED gracefully when task is undefined', () => {
1011
+ const payload = {
1012
+ data: {
1013
+ type: CC_EVENTS.CONTACT_ENDED,
1014
+ interactionId: 'non-existent-task-id',
1015
+ interaction: {
1016
+ state: 'new',
1017
+ },
1018
+ },
1019
+ };
1020
+ // Should not throw error when task doesn't exist
1021
+ expect(() => {
1022
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1023
+ }).not.toThrow();
1024
+ });
1025
+
1026
+ it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
1027
+ const task = taskManager.getTask(taskId);
1028
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1029
+ task.data = {
1030
+ ...task.data,
1031
+ ...newData,
1032
+ interaction: {
1033
+ ...task.data.interaction,
1034
+ ...newData.interaction,
1035
+ outboundType: 'OUTDIAL',
1036
+ state: 'new',
1037
+ isTerminated: false,
1038
+ },
1039
+ };
1040
+ return task;
1041
+ });
1042
+ task.unregisterWebCallListeners = jest.fn();
1043
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1044
+
1045
+ const payload = {
1046
+ data: {
1047
+ type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
1048
+ agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
1049
+ eventTime: 1733211616959,
1050
+ eventType: 'RoutingMessage',
1051
+ interaction: {
1052
+ outboundType: 'OUTDIAL',
1053
+ state: 'new',
1054
+ isTerminated: false,
1055
+ },
1056
+ interactionId: taskId,
1057
+ orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
1058
+ trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
1059
+ mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
1060
+ destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
1061
+ owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
1062
+ queueMgr: 'aqm',
1063
+ reason: 'USER_DECLINED',
1064
+ reasonCode: 156,
1065
+ },
1066
+ };
628
1067
 
629
1068
  webSocketManagerMock.emit('message', JSON.stringify(payload));
630
1069
 
631
1070
  expect(taskManager.getTask(taskId)).toBeUndefined();
1071
+ expect(removeTaskSpy).toHaveBeenCalled();
632
1072
  });
633
1073
 
634
1074
  it('handle AGENT_OFFER_CONSULT event', () => {
@@ -1353,7 +1793,7 @@ describe('TaskManager', () => {
1353
1793
  expect(spy).toHaveBeenCalledWith(taskEvent, task);
1354
1794
  });
1355
1795
  });
1356
- });
1796
+ });
1357
1797
 
1358
1798
  describe('Conference event handling', () => {
1359
1799
  let task;
@@ -1428,6 +1868,18 @@ describe('TaskManager', () => {
1428
1868
  interactionId: taskId,
1429
1869
  participantId: 'new-participant-123',
1430
1870
  participantType: 'agent',
1871
+ interaction: {
1872
+ participants: {
1873
+ [agentId]: { pType: 'Agent', hasLeft: false },
1874
+ 'new-participant-123': { pType: 'Agent', hasLeft: false },
1875
+ },
1876
+ media: {
1877
+ [taskId]: {
1878
+ mType: 'mainCall',
1879
+ participants: [agentId, 'new-participant-123'],
1880
+ },
1881
+ },
1882
+ },
1431
1883
  },
1432
1884
  };
1433
1885
 
@@ -1438,7 +1890,86 @@ describe('TaskManager', () => {
1438
1890
  // No specific task event emission for participant joined - just data update
1439
1891
  });
1440
1892
 
1893
+ it('should call updateTaskData only once for PARTICIPANT_JOINED_CONFERENCE with pre-calculated isConferenceInProgress', () => {
1894
+ const payload = {
1895
+ data: {
1896
+ type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE,
1897
+ interactionId: taskId,
1898
+ participantId: 'new-agent-789',
1899
+ interaction: {
1900
+ participants: {
1901
+ [agentId]: { pType: 'Agent', hasLeft: false },
1902
+ 'agent-2': { pType: 'Agent', hasLeft: false },
1903
+ 'new-agent-789': { pType: 'Agent', hasLeft: false },
1904
+ 'customer-1': { pType: 'Customer', hasLeft: false },
1905
+ },
1906
+ media: {
1907
+ [taskId]: {
1908
+ mType: 'mainCall',
1909
+ participants: [agentId, 'agent-2', 'new-agent-789', 'customer-1'],
1910
+ },
1911
+ },
1912
+ },
1913
+ },
1914
+ };
1915
+
1916
+ const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
1917
+
1918
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1919
+
1920
+ // Verify updateTaskData was called exactly once
1921
+ expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
1922
+
1923
+ // Verify it was called with isConferenceInProgress already calculated
1924
+ expect(updateTaskDataSpy).toHaveBeenCalledWith(
1925
+ expect.objectContaining({
1926
+ participantId: 'new-agent-789',
1927
+ isConferenceInProgress: true, // 3 active agents
1928
+ })
1929
+ );
1930
+
1931
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
1932
+ });
1933
+
1441
1934
  describe('PARTICIPANT_LEFT_CONFERENCE event handling', () => {
1935
+ it('should call updateTaskData only once for PARTICIPANT_LEFT_CONFERENCE with pre-calculated isConferenceInProgress', () => {
1936
+ const payload = {
1937
+ data: {
1938
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1939
+ interactionId: taskId,
1940
+ interaction: {
1941
+ participants: {
1942
+ [agentId]: { pType: 'Agent', hasLeft: false },
1943
+ 'agent-2': { pType: 'Agent', hasLeft: true }, // This agent left
1944
+ 'customer-1': { pType: 'Customer', hasLeft: false },
1945
+ },
1946
+ media: {
1947
+ [taskId]: {
1948
+ mType: 'mainCall',
1949
+ participants: [agentId, 'customer-1'], // agent-2 removed from participants
1950
+ },
1951
+ },
1952
+ },
1953
+ },
1954
+ };
1955
+
1956
+ const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
1957
+
1958
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1959
+
1960
+ // Verify updateTaskData was called exactly once
1961
+ expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
1962
+
1963
+ // Verify it was called with isConferenceInProgress already calculated
1964
+ expect(updateTaskDataSpy).toHaveBeenCalledWith(
1965
+ expect.objectContaining({
1966
+ isConferenceInProgress: false, // Only 1 active agent remains
1967
+ })
1968
+ );
1969
+
1970
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1971
+ });
1972
+
1442
1973
  it('should emit TASK_PARTICIPANT_LEFT event when participant leaves conference', () => {
1443
1974
  const payload = {
1444
1975
  data: {
@@ -1735,6 +2266,218 @@ describe('TaskManager', () => {
1735
2266
  expect(otherTask.data.isConferencing).toBeUndefined();
1736
2267
  expect(otherTask.emit).not.toHaveBeenCalled();
1737
2268
  });
1738
- });
2269
+ });
2270
+
2271
+ describe('CONTACT_MERGED event handling', () => {
2272
+ let task;
2273
+ let taskEmitSpy;
2274
+ let managerEmitSpy;
2275
+
2276
+ beforeEach(() => {
2277
+ // Create initial task
2278
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
2279
+ task = taskManager.getTask(taskId);
2280
+ taskEmitSpy = jest.spyOn(task, 'emit');
2281
+ managerEmitSpy = jest.spyOn(taskManager, 'emit');
2282
+ });
2283
+
2284
+ it('should update existing task data and emit TASK_MERGED event when CONTACT_MERGED is received', () => {
2285
+ const mergedPayload = {
2286
+ data: {
2287
+ type: CC_EVENTS.CONTACT_MERGED,
2288
+ interactionId: taskId,
2289
+ agentId: taskDataMock.agentId,
2290
+ interaction: {
2291
+ ...taskDataMock.interaction,
2292
+ state: 'merged',
2293
+ customField: 'updated-value',
2294
+ },
2295
+ },
2296
+ };
2297
+
2298
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
2299
+
2300
+ const updatedTask = taskManager.getTask(taskId);
2301
+ expect(updatedTask).toBeDefined();
2302
+ expect(updatedTask.data.interaction.customField).toBe('updated-value');
2303
+ expect(updatedTask.data.interaction.state).toBe('merged');
2304
+ expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, updatedTask);
2305
+ });
2306
+
2307
+ it('should create new task when CONTACT_MERGED is received for non-existing task', () => {
2308
+ const newMergedTaskId = 'new-merged-task-id';
2309
+ const mergedPayload = {
2310
+ data: {
2311
+ type: CC_EVENTS.CONTACT_MERGED,
2312
+ interactionId: newMergedTaskId,
2313
+ agentId: taskDataMock.agentId,
2314
+ interaction: {
2315
+ mediaType: 'telephony',
2316
+ state: 'merged',
2317
+ participants: {
2318
+ [taskDataMock.agentId]: {
2319
+ isWrapUp: false,
2320
+ hasJoined: true,
2321
+ },
2322
+ },
2323
+ },
2324
+ },
2325
+ };
2326
+
2327
+ // Verify task doesn't exist before
2328
+ expect(taskManager.getTask(newMergedTaskId)).toBeUndefined();
2329
+
2330
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
2331
+
2332
+ // Verify task was created
2333
+ const newTask = taskManager.getTask(newMergedTaskId);
2334
+ expect(newTask).toBeDefined();
2335
+ expect(newTask.data.interactionId).toBe(newMergedTaskId);
2336
+ expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, newTask);
2337
+ });
2338
+
2339
+ it('should remove child task when childInteractionId is present in CONTACT_MERGED', () => {
2340
+ const childTaskId = 'child-task-id';
2341
+ const parentTaskId = 'parent-task-id';
2342
+
2343
+ // Create child task
2344
+ const childPayload = {
2345
+ data: {
2346
+ type: CC_EVENTS.AGENT_CONTACT_RESERVED,
2347
+ interactionId: childTaskId,
2348
+ agentId: taskDataMock.agentId,
2349
+ interaction: {mediaType: 'telephony'},
2350
+ },
2351
+ };
2352
+ webSocketManagerMock.emit('message', JSON.stringify(childPayload));
2353
+
2354
+ // Verify child task exists
2355
+ expect(taskManager.getTask(childTaskId)).toBeDefined();
2356
+
2357
+ // Create parent task
2358
+ const parentPayload = {
2359
+ data: {
2360
+ type: CC_EVENTS.AGENT_CONTACT_RESERVED,
2361
+ interactionId: parentTaskId,
2362
+ agentId: taskDataMock.agentId,
2363
+ interaction: {mediaType: 'telephony'},
2364
+ },
2365
+ };
2366
+ webSocketManagerMock.emit('message', JSON.stringify(parentPayload));
2367
+
2368
+ // Send CONTACT_MERGED with childInteractionId
2369
+ const mergedPayload = {
2370
+ data: {
2371
+ type: CC_EVENTS.CONTACT_MERGED,
2372
+ interactionId: parentTaskId,
2373
+ childInteractionId: childTaskId,
2374
+ agentId: taskDataMock.agentId,
2375
+ interaction: {
2376
+ mediaType: 'telephony',
2377
+ state: 'merged',
2378
+ },
2379
+ },
2380
+ };
2381
+
2382
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
2383
+
2384
+ // Verify child task was removed
2385
+ expect(taskManager.getTask(childTaskId)).toBeUndefined();
2386
+
2387
+ // Verify parent task still exists
2388
+ expect(taskManager.getTask(parentTaskId)).toBeDefined();
2389
+
2390
+ // Verify TASK_MERGED event was emitted
2391
+ expect(managerEmitSpy).toHaveBeenCalledWith(
2392
+ TASK_EVENTS.TASK_MERGED,
2393
+ expect.objectContaining({
2394
+ data: expect.objectContaining({
2395
+ interactionId: parentTaskId,
2396
+ }),
2397
+ })
2398
+ );
2399
+ });
2400
+
2401
+ it('should handle CONTACT_MERGED with EP-DN participant correctly', () => {
2402
+ const epdnTaskId = 'epdn-merged-task';
2403
+ const mergedPayload = {
2404
+ data: {
2405
+ type: CC_EVENTS.CONTACT_MERGED,
2406
+ interactionId: epdnTaskId,
2407
+ agentId: taskDataMock.agentId,
2408
+ interaction: {
2409
+ mediaType: 'telephony',
2410
+ state: 'merged',
2411
+ participants: {
2412
+ [taskDataMock.agentId]: {
2413
+ type: 'Agent',
2414
+ isWrapUp: false,
2415
+ hasJoined: true,
2416
+ },
2417
+ 'epdn-participant': {
2418
+ type: 'EpDn',
2419
+ epId: 'entry-point-123',
2420
+ isWrapUp: false,
2421
+ },
2422
+ },
2423
+ },
2424
+ },
2425
+ };
2426
+
2427
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
2428
+
2429
+ const mergedTask = taskManager.getTask(epdnTaskId);
2430
+ expect(mergedTask).toBeDefined();
2431
+ expect(mergedTask.data.interaction.participants['epdn-participant']).toBeDefined();
2432
+ expect(mergedTask.data.interaction.participants['epdn-participant'].type).toBe('EpDn');
2433
+ expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, mergedTask);
2434
+ });
2435
+
2436
+ it('should not affect other tasks when CONTACT_MERGED is received', () => {
2437
+ const otherTaskId = 'other-unrelated-task';
2438
+ const otherPayload = {
2439
+ data: {
2440
+ type: CC_EVENTS.AGENT_CONTACT_RESERVED,
2441
+ interactionId: otherTaskId,
2442
+ agentId: taskDataMock.agentId,
2443
+ interaction: {mediaType: 'chat'},
2444
+ },
2445
+ };
2446
+ webSocketManagerMock.emit('message', JSON.stringify(otherPayload));
2447
+
2448
+ const otherTask = taskManager.getTask(otherTaskId);
2449
+ const otherTaskEmitSpy = jest.spyOn(otherTask, 'emit');
2450
+
2451
+ // Send CONTACT_MERGED for the original task
2452
+ const mergedPayload = {
2453
+ data: {
2454
+ type: CC_EVENTS.CONTACT_MERGED,
2455
+ interactionId: taskId,
2456
+ agentId: taskDataMock.agentId,
2457
+ interaction: {
2458
+ mediaType: 'telephony',
2459
+ state: 'merged',
2460
+ },
2461
+ },
2462
+ };
2463
+
2464
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
2465
+
2466
+ // Verify other task was not affected
2467
+ expect(otherTaskEmitSpy).not.toHaveBeenCalled();
2468
+ expect(otherTask.data.interaction.mediaType).toBe('chat');
2469
+
2470
+ // Verify original task was updated
2471
+ expect(managerEmitSpy).toHaveBeenCalledWith(
2472
+ TASK_EVENTS.TASK_MERGED,
2473
+ expect.objectContaining({
2474
+ data: expect.objectContaining({
2475
+ interactionId: taskId,
2476
+ }),
2477
+ })
2478
+ );
2479
+ });
2480
+ });
2481
+
1739
2482
  });
1740
2483