@webex/contact-center 3.10.0-next.3 → 3.10.0-next.31

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 +175 -62
  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 +50 -59
  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 +9 -9
  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 +201 -41
  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 +54 -89
  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 +602 -5
  100. package/test/unit/spec/services/task/TaskUtils.ts +311 -9
  101. package/test/unit/spec/services/task/index.ts +283 -86
  102. package/umd/contact-center.min.js +2 -2
  103. package/umd/contact-center.min.js.map +1 -1
@@ -673,14 +673,181 @@ describe('TaskManager', () => {
673
673
  expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
674
674
  });
675
675
 
676
- 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
+
677
840
  const payload = {
678
841
  data: {
679
842
  type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
680
843
  agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
681
844
  eventTime: 1733211616959,
682
845
  eventType: 'RoutingMessage',
683
- interaction: {},
846
+ interaction: {
847
+ outboundType: 'OUTDIAL',
848
+ state: 'new',
849
+ isTerminated: true,
850
+ },
684
851
  interactionId: taskId,
685
852
  orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
686
853
  trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
@@ -688,14 +855,220 @@ describe('TaskManager', () => {
688
855
  destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
689
856
  owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
690
857
  queueMgr: 'aqm',
858
+ reason: 'CUSTOMER_BUSY',
859
+ reasonCode: 1022,
691
860
  },
692
861
  };
693
862
 
694
- 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
+ };
695
1067
 
696
1068
  webSocketManagerMock.emit('message', JSON.stringify(payload));
697
1069
 
698
1070
  expect(taskManager.getTask(taskId)).toBeUndefined();
1071
+ expect(removeTaskSpy).toHaveBeenCalled();
699
1072
  });
700
1073
 
701
1074
  it('handle AGENT_OFFER_CONSULT event', () => {
@@ -1420,7 +1793,7 @@ describe('TaskManager', () => {
1420
1793
  expect(spy).toHaveBeenCalledWith(taskEvent, task);
1421
1794
  });
1422
1795
  });
1423
- });
1796
+ });
1424
1797
 
1425
1798
  describe('Conference event handling', () => {
1426
1799
  let task;
@@ -1495,6 +1868,18 @@ describe('TaskManager', () => {
1495
1868
  interactionId: taskId,
1496
1869
  participantId: 'new-participant-123',
1497
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
+ },
1498
1883
  },
1499
1884
  };
1500
1885
 
@@ -1881,6 +2266,218 @@ describe('TaskManager', () => {
1881
2266
  expect(otherTask.data.isConferencing).toBeUndefined();
1882
2267
  expect(otherTask.emit).not.toHaveBeenCalled();
1883
2268
  });
1884
- });
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
+
1885
2482
  });
1886
2483