@webex/contact-center 3.10.0-next.9 → 3.10.0-set-bitrate.2

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 (89) hide show
  1. package/dist/cc.js +2 -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.map +1 -1
  31. package/dist/services/constants.js.map +1 -1
  32. package/dist/services/core/Err.js.map +1 -1
  33. package/dist/services/core/GlobalTypes.js.map +1 -1
  34. package/dist/services/core/Utils.js +2 -3
  35. package/dist/services/core/Utils.js.map +1 -1
  36. package/dist/services/core/WebexRequest.js +1 -2
  37. package/dist/services/core/WebexRequest.js.map +1 -1
  38. package/dist/services/core/aqm-reqs.js +2 -3
  39. package/dist/services/core/aqm-reqs.js.map +1 -1
  40. package/dist/services/core/constants.js.map +1 -1
  41. package/dist/services/core/types.js.map +1 -1
  42. package/dist/services/core/websocket/WebSocketManager.js +1 -2
  43. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  44. package/dist/services/core/websocket/connection-service.js +1 -1
  45. package/dist/services/core/websocket/connection-service.js.map +1 -1
  46. package/dist/services/core/websocket/keepalive.worker.js.map +1 -1
  47. package/dist/services/core/websocket/types.js.map +1 -1
  48. package/dist/services/index.js +1 -1
  49. package/dist/services/index.js.map +1 -1
  50. package/dist/services/task/AutoWrapup.js +1 -1
  51. package/dist/services/task/AutoWrapup.js.map +1 -1
  52. package/dist/services/task/TaskManager.js +121 -33
  53. package/dist/services/task/TaskManager.js.map +1 -1
  54. package/dist/services/task/TaskUtils.js +90 -1
  55. package/dist/services/task/TaskUtils.js.map +1 -1
  56. package/dist/services/task/constants.js +3 -1
  57. package/dist/services/task/constants.js.map +1 -1
  58. package/dist/services/task/contact.js +0 -2
  59. package/dist/services/task/contact.js.map +1 -1
  60. package/dist/services/task/dialer.js.map +1 -1
  61. package/dist/services/task/index.js +1 -1
  62. package/dist/services/task/index.js.map +1 -1
  63. package/dist/services/task/types.js +375 -0
  64. package/dist/services/task/types.js.map +1 -1
  65. package/dist/types/metrics/constants.d.ts +4 -0
  66. package/dist/types/services/task/TaskUtils.d.ts +42 -0
  67. package/dist/types/services/task/constants.d.ts +2 -0
  68. package/dist/types/services/task/types.d.ts +32 -0
  69. package/dist/types.js +5 -0
  70. package/dist/types.js.map +1 -1
  71. package/dist/utils/PageCache.js +1 -1
  72. package/dist/utils/PageCache.js.map +1 -1
  73. package/dist/webex-config.js.map +1 -1
  74. package/dist/webex.js +2 -2
  75. package/dist/webex.js.map +1 -1
  76. package/package.json +9 -9
  77. package/src/cc.ts +1 -0
  78. package/src/metrics/behavioral-events.ts +12 -0
  79. package/src/metrics/constants.ts +4 -0
  80. package/src/services/task/TaskManager.ts +135 -22
  81. package/src/services/task/TaskUtils.ts +109 -1
  82. package/src/services/task/constants.ts +2 -0
  83. package/src/services/task/types.ts +34 -0
  84. package/test/unit/spec/cc.ts +1 -0
  85. package/test/unit/spec/metrics/behavioral-events.ts +14 -0
  86. package/test/unit/spec/services/task/TaskManager.ts +378 -4
  87. package/test/unit/spec/services/task/TaskUtils.ts +305 -3
  88. package/umd/contact-center.min.js +2 -2
  89. 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;
@@ -2105,5 +2478,6 @@ describe('TaskManager', () => {
2105
2478
  );
2106
2479
  });
2107
2480
  });
2481
+
2108
2482
  });
2109
2483