@webex/contact-center 3.9.0-next.9 → 3.10.0-multi-llms.1

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 (100) hide show
  1. package/dist/cc.js +193 -47
  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/index.js +9 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/logger-proxy.js +24 -1
  8. package/dist/logger-proxy.js.map +1 -1
  9. package/dist/metrics/behavioral-events.js +89 -0
  10. package/dist/metrics/behavioral-events.js.map +1 -1
  11. package/dist/metrics/constants.js +30 -2
  12. package/dist/metrics/constants.js.map +1 -1
  13. package/dist/services/AddressBook.js +271 -0
  14. package/dist/services/AddressBook.js.map +1 -0
  15. package/dist/services/EntryPoint.js +227 -0
  16. package/dist/services/EntryPoint.js.map +1 -0
  17. package/dist/services/Queue.js +261 -0
  18. package/dist/services/Queue.js.map +1 -0
  19. package/dist/services/config/constants.js +36 -2
  20. package/dist/services/config/constants.js.map +1 -1
  21. package/dist/services/config/index.js +29 -21
  22. package/dist/services/config/index.js.map +1 -1
  23. package/dist/services/config/types.js +33 -1
  24. package/dist/services/config/types.js.map +1 -1
  25. package/dist/services/core/Utils.js +91 -31
  26. package/dist/services/core/Utils.js.map +1 -1
  27. package/dist/services/core/constants.js +17 -1
  28. package/dist/services/core/constants.js.map +1 -1
  29. package/dist/services/task/TaskManager.js +150 -7
  30. package/dist/services/task/TaskManager.js.map +1 -1
  31. package/dist/services/task/TaskUtils.js +104 -0
  32. package/dist/services/task/TaskUtils.js.map +1 -0
  33. package/dist/services/task/constants.js +26 -1
  34. package/dist/services/task/constants.js.map +1 -1
  35. package/dist/services/task/contact.js +86 -0
  36. package/dist/services/task/contact.js.map +1 -1
  37. package/dist/services/task/index.js +302 -39
  38. package/dist/services/task/index.js.map +1 -1
  39. package/dist/services/task/types.js +12 -0
  40. package/dist/services/task/types.js.map +1 -1
  41. package/dist/types/cc.d.ts +121 -35
  42. package/dist/types/constants.d.ts +1 -0
  43. package/dist/types/index.d.ts +4 -3
  44. package/dist/types/metrics/constants.d.ts +24 -1
  45. package/dist/types/services/AddressBook.d.ts +74 -0
  46. package/dist/types/services/EntryPoint.d.ts +67 -0
  47. package/dist/types/services/Queue.d.ts +76 -0
  48. package/dist/types/services/config/constants.d.ts +35 -1
  49. package/dist/types/services/config/index.d.ts +6 -9
  50. package/dist/types/services/config/types.d.ts +79 -58
  51. package/dist/types/services/core/Utils.d.ts +33 -5
  52. package/dist/types/services/core/constants.d.ts +14 -0
  53. package/dist/types/services/task/TaskUtils.d.ts +42 -0
  54. package/dist/types/services/task/constants.d.ts +23 -0
  55. package/dist/types/services/task/contact.d.ts +10 -0
  56. package/dist/types/services/task/index.d.ts +84 -3
  57. package/dist/types/services/task/types.d.ts +245 -21
  58. package/dist/types/types.d.ts +162 -0
  59. package/dist/types/utils/PageCache.d.ts +173 -0
  60. package/dist/types.js +17 -0
  61. package/dist/types.js.map +1 -1
  62. package/dist/utils/PageCache.js +192 -0
  63. package/dist/utils/PageCache.js.map +1 -0
  64. package/dist/webex.js +1 -1
  65. package/package.json +10 -9
  66. package/src/cc.ts +217 -52
  67. package/src/constants.ts +1 -0
  68. package/src/index.ts +17 -2
  69. package/src/logger-proxy.ts +24 -1
  70. package/src/metrics/behavioral-events.ts +94 -0
  71. package/src/metrics/constants.ts +34 -1
  72. package/src/services/AddressBook.ts +291 -0
  73. package/src/services/EntryPoint.ts +241 -0
  74. package/src/services/Queue.ts +277 -0
  75. package/src/services/config/constants.ts +42 -2
  76. package/src/services/config/index.ts +30 -30
  77. package/src/services/config/types.ts +59 -58
  78. package/src/services/core/Utils.ts +101 -41
  79. package/src/services/core/constants.ts +16 -0
  80. package/src/services/task/TaskManager.ts +181 -9
  81. package/src/services/task/TaskUtils.ts +113 -0
  82. package/src/services/task/constants.ts +25 -0
  83. package/src/services/task/contact.ts +80 -0
  84. package/src/services/task/index.ts +364 -54
  85. package/src/services/task/types.ts +264 -20
  86. package/src/types.ts +180 -0
  87. package/src/utils/PageCache.ts +252 -0
  88. package/test/unit/spec/cc.ts +282 -85
  89. package/test/unit/spec/metrics/behavioral-events.ts +42 -0
  90. package/test/unit/spec/services/AddressBook.ts +332 -0
  91. package/test/unit/spec/services/EntryPoint.ts +259 -0
  92. package/test/unit/spec/services/Queue.ts +323 -0
  93. package/test/unit/spec/services/config/index.ts +279 -65
  94. package/test/unit/spec/services/core/Utils.ts +262 -31
  95. package/test/unit/spec/services/task/TaskManager.ts +752 -1
  96. package/test/unit/spec/services/task/TaskUtils.ts +131 -0
  97. package/test/unit/spec/services/task/contact.ts +31 -1
  98. package/test/unit/spec/services/task/index.ts +675 -69
  99. package/umd/contact-center.min.js +2 -2
  100. package/umd/contact-center.min.js.map +1 -1
@@ -39,7 +39,8 @@ describe('Task', () => {
39
39
  let loggerInfoSpy;
40
40
  let loggerLogSpy;
41
41
  let loggerErrorSpy;
42
- let getDestinationAgentIdSpy;
42
+ let calculateDestAgentIdSpy;
43
+ let calculateDestTypeSpy;
43
44
 
44
45
  const taskId = '0ae913a4-c857-4705-8d49-76dd3dde75e4';
45
46
  const mockTrack = {} as MediaStreamTrack;
@@ -75,6 +76,9 @@ describe('Task', () => {
75
76
  wrapup: jest.fn().mockResolvedValue({}),
76
77
  pauseRecording: jest.fn().mockResolvedValue({}),
77
78
  resumeRecording: jest.fn().mockResolvedValue({}),
79
+ consultConference: jest.fn().mockResolvedValue({}),
80
+ exitConference: jest.fn().mockResolvedValue({}),
81
+ conferenceTransfer: jest.fn().mockResolvedValue({}),
78
82
  };
79
83
 
80
84
  mockMetricsManager = {
@@ -116,6 +120,32 @@ describe('Task', () => {
116
120
  interaction: {
117
121
  mediaType: 'telephony',
118
122
  mainInteractionId: taskId,
123
+ participants: {
124
+ '723a8ffb-a26e-496d-b14a-ff44fb83b64f': {
125
+ pType: 'Agent',
126
+ type: 'AGENT',
127
+ id: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
128
+ hasLeft: false,
129
+ hasJoined: true,
130
+ isWrapUp: false,
131
+ },
132
+ 'f520d6b5-28ad-4f2f-b83e-781bb64af617': {
133
+ pType: 'Agent',
134
+ type: 'AGENT',
135
+ id: 'f520d6b5-28ad-4f2f-b83e-781bb64af617',
136
+ hasLeft: false,
137
+ hasJoined: true,
138
+ isWrapUp: false,
139
+ },
140
+ 'ebeb893b-ba67-4f36-8418-95c7492b28c2': {
141
+ pType: 'Agent',
142
+ type: 'AGENT',
143
+ id: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
144
+ hasLeft: false,
145
+ hasJoined: true,
146
+ isWrapUp: false,
147
+ },
148
+ },
119
149
  media: {
120
150
  '58a45567-4e61-4f4b-a580-5bc86357bef0': {
121
151
  holdTimestamp: null,
@@ -142,13 +172,18 @@ describe('Task', () => {
142
172
  },
143
173
  };
144
174
 
145
- // Mock destination agent id resolution from participants
146
- getDestinationAgentIdSpy = jest
147
- .spyOn(Utils, 'getDestinationAgentId')
148
- .mockReturnValue(taskDataMock.destAgentId);
175
+ // Mock calculateDestAgentId to return the expected destination agent
176
+ calculateDestAgentIdSpy = jest.spyOn(Utils, 'calculateDestAgentId').mockReturnValue(taskDataMock.destAgentId);
177
+
178
+ // Mock calculateDestType to return 'agent' by default
179
+ calculateDestTypeSpy = jest.spyOn(Utils, 'calculateDestType').mockReturnValue('agent');
149
180
 
150
- // Create an instance of Task
151
- task = new Task(contactMock, webCallingService, taskDataMock);
181
+ // Create an instance of Task with wrapupData and agentId
182
+ task = new Task(contactMock, webCallingService, taskDataMock, {
183
+ wrapUpProps: { wrapUpReasonList: [] },
184
+ autoWrapEnabled: false,
185
+ autoWrapAfterSeconds: 0
186
+ }, taskDataMock.agentId);
152
187
 
153
188
  // Mock navigator.mediaDevices
154
189
  global.navigator.mediaDevices = {
@@ -214,7 +249,7 @@ describe('Task', () => {
214
249
  });
215
250
 
216
251
  describe('updateTaskData cases', () => {
217
- it('test updating the task data by overwrite', async () => {
252
+ it('updates the task data by overwrite', async () => {
218
253
  const newData = {
219
254
  type: CC_EVENTS.AGENT_CONTACT_ASSIGNED,
220
255
  agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
@@ -263,12 +298,12 @@ describe('Task', () => {
263
298
  expect(task.data).toEqual(newData);
264
299
  });
265
300
 
266
- it('test updating the task data by merging', async () => {
301
+ it('updates the task data by merging with key removal', async () => {
267
302
  const newData = {
268
- // ...taskDataMock, // Purposefully omit this to test scenario when other keys isn't present
303
+ // Purposefully omit other keys to test remove and merge behavior
269
304
  isConsulting: true, // Add a new custom key to test persistence
270
305
  interaction: {
271
- // ...taskDataMock.interaction, // Purposefully omit this to test scenario when a nested key isn't present
306
+ // Purposefully omit other interaction keys to test removal
272
307
  media: {
273
308
  '58a45567-4e61-4f4b-a580-5bc86357bef0': {
274
309
  holdTimestamp: null,
@@ -295,11 +330,12 @@ describe('Task', () => {
295
330
  },
296
331
  };
297
332
 
333
+ // The reconcileData method removes keys from oldData that are not in newData
334
+ // This means only keys present in newData will remain in the final result
298
335
  const expectedData: TaskData = {
299
- ...taskDataMock,
300
- isConsulting: true,
336
+ isConsulting: true, // New key is added
301
337
  interaction: {
302
- ...taskDataMock.interaction,
338
+ // Only the media key from newData.interaction remains
303
339
  media: {
304
340
  '58a45567-4e61-4f4b-a580-5bc86357bef0': {
305
341
  holdTimestamp: null,
@@ -332,6 +368,60 @@ describe('Task', () => {
332
368
 
333
369
  expect(task.data).toEqual(expectedData);
334
370
  });
371
+
372
+ it('updates the task data by merging and preserving existing keys', async () => {
373
+ const newData = {
374
+ ...taskDataMock, // Include all existing keys to test merge without removal
375
+ isConsulting: true, // Add a new custom key
376
+ interaction: {
377
+ ...taskDataMock.interaction, // Include existing interaction data
378
+ media: {
379
+ ...taskDataMock.interaction.media, // Include existing media
380
+ '58a45567-4e61-4f4b-a580-5bc86357bef0': {
381
+ holdTimestamp: null,
382
+ isHold: true,
383
+ mType: 'consult',
384
+ mediaMgr: 'callmm',
385
+ mediaResourceId: '58a45567-4e61-4f4b-a580-5bc86357bef0',
386
+ mediaType: 'telephony',
387
+ participants: [
388
+ 'f520d6b5-28ad-4f2f-b83e-781bb64af617',
389
+ '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
390
+ ],
391
+ },
392
+ },
393
+ },
394
+ };
395
+
396
+ const expectedData: TaskData = {
397
+ ...taskDataMock,
398
+ isConsulting: true,
399
+ interaction: {
400
+ ...taskDataMock.interaction,
401
+ media: {
402
+ ...taskDataMock.interaction.media,
403
+ '58a45567-4e61-4f4b-a580-5bc86357bef0': {
404
+ holdTimestamp: null,
405
+ isHold: true,
406
+ mType: 'consult',
407
+ mediaMgr: 'callmm',
408
+ mediaResourceId: '58a45567-4e61-4f4b-a580-5bc86357bef0',
409
+ mediaType: 'telephony',
410
+ participants: [
411
+ 'f520d6b5-28ad-4f2f-b83e-781bb64af617',
412
+ '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
413
+ ],
414
+ },
415
+ },
416
+ },
417
+ };
418
+
419
+ expect(task.data).toEqual(taskDataMock);
420
+ const shouldOverwrite = false;
421
+ task.updateTaskData(newData, shouldOverwrite);
422
+
423
+ expect(task.data).toEqual(expectedData);
424
+ });
335
425
  });
336
426
 
337
427
  it('should accept a task and answer call when using BROWSER login option', async () => {
@@ -567,6 +657,40 @@ describe('Task', () => {
567
657
  );
568
658
  });
569
659
 
660
+ it('should hold the task with custom mediaResourceId and return the expected response', async () => {
661
+ const customMediaResourceId = 'custom-media-resource-id-123';
662
+ const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
663
+ contactMock.hold.mockResolvedValue(expectedResponse);
664
+
665
+ const response = await task.hold(customMediaResourceId);
666
+
667
+ expect(contactMock.hold).toHaveBeenCalledWith({
668
+ interactionId: taskId,
669
+ data: {mediaResourceId: customMediaResourceId},
670
+ });
671
+ expect(response).toEqual(expectedResponse);
672
+ expect(loggerInfoSpy).toHaveBeenCalledWith(`Holding task`, {
673
+ module: TASK_FILE,
674
+ method: 'hold',
675
+ interactionId: task.data.interactionId,
676
+ });
677
+ expect(loggerLogSpy).toHaveBeenCalledWith(`Task placed on hold successfully`, {
678
+ module: TASK_FILE,
679
+ method: 'hold',
680
+ interactionId: task.data.interactionId,
681
+ });
682
+ expect(mockMetricsManager.trackEvent).toHaveBeenNthCalledWith(
683
+ 1,
684
+ METRIC_EVENT_NAMES.TASK_HOLD_SUCCESS,
685
+ {
686
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(expectedResponse),
687
+ taskId: taskDataMock.interactionId,
688
+ mediaResourceId: customMediaResourceId,
689
+ },
690
+ ['operational', 'behavioral']
691
+ );
692
+ });
693
+
570
694
  it('should handle errors in hold method', async () => {
571
695
  const error = {details: (global as any).makeFailure('Hold Failed')};
572
696
  contactMock.hold.mockImplementation(() => {
@@ -596,6 +720,36 @@ describe('Task', () => {
596
720
  );
597
721
  });
598
722
 
723
+ it('should handle errors in hold method with custom mediaResourceId', async () => {
724
+ const customMediaResourceId = 'custom-media-resource-id-456';
725
+ const error = {details: (global as any).makeFailure('Hold Failed with custom mediaResourceId')};
726
+ contactMock.hold.mockImplementation(() => {
727
+ throw error;
728
+ });
729
+
730
+ await expect(task.hold(customMediaResourceId)).rejects.toThrow(error.details.data.reason);
731
+ expect(generateTaskErrorObjectSpy).toHaveBeenCalledWith(error, 'hold', TASK_FILE);
732
+ const expectedTaskErrorFieldsHold = {
733
+ trackingId: error.details.trackingId,
734
+ errorMessage: error.details.data.reason,
735
+ errorType: '',
736
+ errorData: '',
737
+ reasonCode: 0,
738
+ };
739
+ expect(mockMetricsManager.trackEvent).toHaveBeenNthCalledWith(
740
+ 1,
741
+ METRIC_EVENT_NAMES.TASK_HOLD_FAILED,
742
+ {
743
+ taskId: taskDataMock.interactionId,
744
+ mediaResourceId: customMediaResourceId,
745
+ error: error.toString(),
746
+ ...expectedTaskErrorFieldsHold,
747
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details),
748
+ },
749
+ ['operational', 'behavioral']
750
+ );
751
+ });
752
+
599
753
  it('should resume the task and return the expected response', async () => {
600
754
  const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
601
755
  contactMock.unHold.mockResolvedValue(expectedResponse);
@@ -620,6 +774,29 @@ describe('Task', () => {
620
774
  );
621
775
  });
622
776
 
777
+ it('should resume the task with custom mediaResourceId and return the expected response', async () => {
778
+ const customMediaResourceId = 'custom-media-resource-id-789';
779
+ const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
780
+ contactMock.unHold.mockResolvedValue(expectedResponse);
781
+ const response = await task.resume(customMediaResourceId);
782
+ expect(contactMock.unHold).toHaveBeenCalledWith({
783
+ interactionId: taskId,
784
+ data: {mediaResourceId: customMediaResourceId},
785
+ });
786
+ expect(response).toEqual(expectedResponse);
787
+ expect(mockMetricsManager.trackEvent).toHaveBeenNthCalledWith(
788
+ 1,
789
+ METRIC_EVENT_NAMES.TASK_RESUME_SUCCESS,
790
+ {
791
+ taskId: taskDataMock.interactionId,
792
+ mainInteractionId: taskDataMock.interaction.mainInteractionId,
793
+ mediaResourceId: customMediaResourceId,
794
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(expectedResponse),
795
+ },
796
+ ['operational', 'behavioral']
797
+ );
798
+ });
799
+
623
800
  it('should handle errors in resume method', async () => {
624
801
  const error = {details: (global as any).makeFailure('Resume Failed')};
625
802
  contactMock.unHold.mockImplementation(() => {
@@ -651,6 +828,36 @@ describe('Task', () => {
651
828
  );
652
829
  });
653
830
 
831
+ it('should handle errors in resume method with custom mediaResourceId', async () => {
832
+ const customMediaResourceId = 'custom-media-resource-id-999';
833
+ const error = {details: (global as any).makeFailure('Resume Failed with custom mediaResourceId')};
834
+ contactMock.unHold.mockImplementation(() => {
835
+ throw error;
836
+ });
837
+
838
+ await expect(task.resume(customMediaResourceId)).rejects.toThrow(error.details.data.reason);
839
+ expect(generateTaskErrorObjectSpy).toHaveBeenCalledWith(error, 'resume', TASK_FILE);
840
+ const expectedTaskErrorFieldsResume = {
841
+ trackingId: error.details.trackingId,
842
+ errorMessage: error.details.data.reason,
843
+ errorType: '',
844
+ errorData: '',
845
+ reasonCode: 0,
846
+ };
847
+ expect(mockMetricsManager.trackEvent).toHaveBeenNthCalledWith(
848
+ 1,
849
+ METRIC_EVENT_NAMES.TASK_RESUME_FAILED,
850
+ {
851
+ taskId: taskDataMock.interactionId,
852
+ mainInteractionId: taskDataMock.interaction.mainInteractionId,
853
+ mediaResourceId: customMediaResourceId,
854
+ ...expectedTaskErrorFieldsResume,
855
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details),
856
+ },
857
+ ['operational', 'behavioral']
858
+ );
859
+ });
860
+
654
861
  it('should initiate a consult call and return the expected response', async () => {
655
862
  const consultPayload = {
656
863
  to: '1234',
@@ -671,8 +878,8 @@ describe('Task', () => {
671
878
  expect(loggerLogSpy).toHaveBeenCalledWith(`Consult started successfully to ${consultPayload.to}`, {
672
879
  module: TASK_FILE,
673
880
  method: 'consult',
674
- trackingId: expectedResponse.trackingId,
675
881
  interactionId: task.data.interactionId,
882
+ trackingId: '1234',
676
883
  });
677
884
  expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
678
885
  METRIC_EVENT_NAMES.TASK_CONSULT_START_SUCCESS,
@@ -818,15 +1025,16 @@ describe('Task', () => {
818
1025
  );
819
1026
  });
820
1027
 
821
- it('should send DIALNUMBER when task destinationType is DN during consultTransfer', async () => {
1028
+ it('should send DIALNUMBER when calculateDestType returns dialNumber during consultTransfer', async () => {
822
1029
  const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
823
1030
  contactMock.consultTransfer.mockResolvedValue(expectedResponse);
824
1031
 
825
- // Ensure task data indicates DN scenario
826
- task.data.destinationType = 'DN' as unknown as string;
1032
+ // Mock calculateDestType to return dialNumber
1033
+ calculateDestTypeSpy.mockReturnValue(CONSULT_TRANSFER_DESTINATION_TYPE.DIALNUMBER);
827
1034
 
828
1035
  await task.consultTransfer();
829
1036
 
1037
+ expect(calculateDestTypeSpy).toHaveBeenCalledWith(taskDataMock.interaction, taskDataMock.agentId);
830
1038
  expect(contactMock.consultTransfer).toHaveBeenCalledWith({
831
1039
  interactionId: taskId,
832
1040
  data: {
@@ -836,15 +1044,16 @@ describe('Task', () => {
836
1044
  });
837
1045
  });
838
1046
 
839
- it('should send ENTRYPOINT when task destinationType is EPDN during consultTransfer', async () => {
1047
+ it('should send ENTRYPOINT when calculateDestType returns entryPoint during consultTransfer', async () => {
840
1048
  const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
841
1049
  contactMock.consultTransfer.mockResolvedValue(expectedResponse);
842
1050
 
843
- // Ensure task data indicates EP/EPDN scenario
844
- task.data.destinationType = 'EPDN' as unknown as string;
1051
+ // Mock calculateDestType to return entryPoint
1052
+ calculateDestTypeSpy.mockReturnValue(CONSULT_TRANSFER_DESTINATION_TYPE.ENTRYPOINT);
845
1053
 
846
1054
  await task.consultTransfer();
847
1055
 
1056
+ expect(calculateDestTypeSpy).toHaveBeenCalledWith(taskDataMock.interaction, taskDataMock.agentId);
848
1057
  expect(contactMock.consultTransfer).toHaveBeenCalledWith({
849
1058
  interactionId: taskId,
850
1059
  data: {
@@ -854,15 +1063,16 @@ describe('Task', () => {
854
1063
  });
855
1064
  });
856
1065
 
857
- it('should keep AGENT when task destinationType is neither DN nor EPDN/ENTRYPOINT', async () => {
1066
+ it('should use AGENT when calculateDestType returns agent during consultTransfer', async () => {
858
1067
  const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
859
1068
  contactMock.consultTransfer.mockResolvedValue(expectedResponse);
860
1069
 
861
- // Ensure task data indicates non-DN and non-EP/EPDN scenario
862
- task.data.destinationType = 'SOMETHING_ELSE' as unknown as string;
1070
+ // Mock calculateDestType to return agent (default behavior)
1071
+ calculateDestTypeSpy.mockReturnValue(CONSULT_TRANSFER_DESTINATION_TYPE.AGENT);
863
1072
 
864
1073
  await task.consultTransfer();
865
1074
 
1075
+ expect(calculateDestTypeSpy).toHaveBeenCalledWith(taskDataMock.interaction, taskDataMock.agentId);
866
1076
  expect(contactMock.consultTransfer).toHaveBeenCalledWith({
867
1077
  interactionId: taskId,
868
1078
  data: {
@@ -899,7 +1109,11 @@ describe('Task', () => {
899
1109
  const taskWithoutDestAgentId = new Task(contactMock, webCallingService, {
900
1110
  ...taskDataMock,
901
1111
  destAgentId: undefined,
902
- });
1112
+ }, {
1113
+ wrapUpProps: { wrapUpReasonList: [] },
1114
+ autoWrapEnabled: false,
1115
+ autoWrapAfterSeconds: 0
1116
+ }, taskDataMock.agentId);
903
1117
 
904
1118
  const queueConsultTransferPayload: ConsultTransferPayLoad = {
905
1119
  to: 'some-queue-id',
@@ -907,61 +1121,123 @@ describe('Task', () => {
907
1121
  };
908
1122
 
909
1123
  // For this negative case, ensure computed destination is empty
910
- getDestinationAgentIdSpy.mockReturnValueOnce('');
1124
+ calculateDestAgentIdSpy.mockReturnValueOnce('');
911
1125
 
912
1126
  await expect(
913
1127
  taskWithoutDestAgentId.consultTransfer(queueConsultTransferPayload)
914
- ).rejects.toThrow('Error while performing consultTransfer');
1128
+ ).rejects.toThrow('No agent has accepted this queue consult yet');
915
1129
  });
916
1130
 
917
- it('should handle errors in consult transfer', async () => {
918
- const consultPayload = {
919
- destination: '1234',
920
- destinationType: DESTINATION_TYPE.AGENT,
921
- };
922
- const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
923
- contactMock.consult.mockResolvedValue(expectedResponse);
1131
+ describe('consultTransfer', () => {
1132
+ it('should successfully perform consult transfer with agent destination', async () => {
1133
+ const expectedResponse: TaskResponse = {
1134
+ data: {interactionId: taskId},
1135
+ trackingId: 'test-tracking-id'
1136
+ } as AgentContact;
1137
+ contactMock.consultTransfer.mockResolvedValue(expectedResponse);
1138
+
1139
+ calculateDestTypeSpy.mockReturnValue(CONSULT_TRANSFER_DESTINATION_TYPE.AGENT);
924
1140
 
925
- const response = await task.consult(consultPayload);
1141
+ const result = await task.consultTransfer();
926
1142
 
927
- expect(contactMock.consult).toHaveBeenCalledWith({interactionId: taskId, data: consultPayload});
928
- expect(response).toEqual(expectedResponse);
1143
+ expect(calculateDestAgentIdSpy).toHaveBeenCalledWith(taskDataMock.interaction, taskDataMock.agentId);
1144
+ expect(calculateDestTypeSpy).toHaveBeenCalledWith(taskDataMock.interaction, taskDataMock.agentId);
1145
+ expect(contactMock.consultTransfer).toHaveBeenCalledWith({
1146
+ interactionId: taskId,
1147
+ data: {
1148
+ to: taskDataMock.destAgentId,
1149
+ destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
1150
+ },
1151
+ });
1152
+ expect(result).toEqual(expectedResponse);
1153
+ expect(loggerInfoSpy).toHaveBeenCalledWith(
1154
+ `Initiating consult transfer to ${taskDataMock.destAgentId}`,
1155
+ {
1156
+ module: TASK_FILE,
1157
+ method: 'consultTransfer',
1158
+ interactionId: taskId,
1159
+ }
1160
+ );
1161
+ expect(loggerLogSpy).toHaveBeenCalledWith(
1162
+ `Consult transfer completed successfully to ${taskDataMock.destAgentId}`,
1163
+ {
1164
+ module: TASK_FILE,
1165
+ method: 'consultTransfer',
1166
+ trackingId: expectedResponse.trackingId,
1167
+ interactionId: taskId,
1168
+ }
1169
+ );
1170
+ });
1171
+
1172
+ it('should track metrics on successful consult transfer', async () => {
1173
+ const expectedResponse: TaskResponse = {
1174
+ data: {interactionId: taskId},
1175
+ trackingId: 'test-tracking-id'
1176
+ } as AgentContact;
1177
+ contactMock.consultTransfer.mockResolvedValue(expectedResponse);
1178
+
1179
+ await task.consultTransfer();
1180
+
1181
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
1182
+ METRIC_EVENT_NAMES.TASK_TRANSFER_SUCCESS,
1183
+ {
1184
+ taskId: taskDataMock.interactionId,
1185
+ destination: taskDataMock.destAgentId,
1186
+ destinationType: 'agent',
1187
+ isConsultTransfer: true,
1188
+ ...MetricsManager.getCommonTrackingFieldForAQMResponse(expectedResponse),
1189
+ },
1190
+ ['operational', 'behavioral', 'business']
1191
+ );
1192
+ });
929
1193
 
930
- const error = {details: (global as any).makeFailure('Consult Transfer Failed')};
931
- contactMock.consultTransfer.mockImplementation(() => {
932
- throw error;
1194
+ it('should throw error when no destination agent is found', async () => {
1195
+ calculateDestAgentIdSpy.mockReturnValue('');
1196
+
1197
+ await expect(task.consultTransfer()).rejects.toThrow('No agent has accepted this queue consult yet');
1198
+
1199
+ expect(contactMock.consultTransfer).not.toHaveBeenCalled();
933
1200
  });
934
1201
 
935
- const consultTransferPayload: ConsultTransferPayLoad = {
936
- to: '1234',
937
- destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
938
- };
1202
+ it('should handle and rethrow contact method errors', async () => {
1203
+ const mockError = new Error('Consult Transfer Failed');
1204
+ contactMock.consultTransfer.mockRejectedValue(mockError);
1205
+ generateTaskErrorObjectSpy.mockReturnValue(mockError);
939
1206
 
940
- await expect(task.consultTransfer(consultTransferPayload)).rejects.toThrow(
941
- error.details.data.reason
942
- );
943
- expect(generateTaskErrorObjectSpy).toHaveBeenCalledWith(error, 'consultTransfer', TASK_FILE);
944
- const expectedTaskErrorFieldsConsultTransfer = {
945
- trackingId: error.details.trackingId,
946
- errorMessage: error.details.data.reason,
947
- errorType: '',
948
- errorData: '',
949
- reasonCode: 0,
950
- };
951
- expect(mockMetricsManager.trackEvent).toHaveBeenNthCalledWith(
952
- 2,
953
- METRIC_EVENT_NAMES.TASK_TRANSFER_FAILED,
954
- {
955
- taskId: taskDataMock.interactionId,
956
- destination: taskDataMock.destAgentId,
957
- destinationType: CONSULT_TRANSFER_DESTINATION_TYPE.AGENT,
958
- isConsultTransfer: true,
959
- error: error.toString(),
960
- ...expectedTaskErrorFieldsConsultTransfer,
961
- ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(error.details),
962
- },
963
- ['operational', 'behavioral', 'business']
964
- );
1207
+ await expect(task.consultTransfer()).rejects.toThrow('Consult Transfer Failed');
1208
+
1209
+ expect(generateTaskErrorObjectSpy).toHaveBeenCalledWith(mockError, 'consultTransfer', TASK_FILE);
1210
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
1211
+ METRIC_EVENT_NAMES.TASK_TRANSFER_FAILED,
1212
+ expect.objectContaining({
1213
+ taskId: taskDataMock.interactionId,
1214
+ destination: taskDataMock.destAgentId,
1215
+ destinationType: 'agent',
1216
+ isConsultTransfer: true,
1217
+ error: mockError.toString(),
1218
+ }),
1219
+ ['operational', 'behavioral', 'business']
1220
+ );
1221
+ });
1222
+
1223
+ it('should dynamically calculate destAgentId when not available', async () => {
1224
+ const consultedAgentId = 'dynamic-agent-123';
1225
+ calculateDestAgentIdSpy.mockReturnValue(consultedAgentId);
1226
+
1227
+ const expectedResponse: TaskResponse = {data: {interactionId: taskId}} as AgentContact;
1228
+ contactMock.consultTransfer.mockResolvedValue(expectedResponse);
1229
+
1230
+ await task.consultTransfer();
1231
+
1232
+ expect(calculateDestAgentIdSpy).toHaveBeenCalledWith(taskDataMock.interaction, taskDataMock.agentId);
1233
+ expect(contactMock.consultTransfer).toHaveBeenCalledWith({
1234
+ interactionId: taskId,
1235
+ data: {
1236
+ to: consultedAgentId,
1237
+ destinationType: 'agent',
1238
+ },
1239
+ });
1240
+ });
965
1241
  });
966
1242
 
967
1243
  it('should do vteamTransfer if destinationType is queue and return the expected response', async () => {
@@ -1575,4 +1851,334 @@ describe('Task', () => {
1575
1851
  });
1576
1852
  });
1577
1853
  });
1854
+
1855
+ describe('Conference methods', () => {
1856
+ beforeEach(() => {
1857
+ contactMock = {
1858
+ consultConference: jest.fn(),
1859
+ exitConference: jest.fn(),
1860
+ conferenceTransfer: jest.fn(),
1861
+ };
1862
+
1863
+ task = new Task(contactMock, webCallingService, taskDataMock, {
1864
+ wrapUpProps: { wrapUpReasonList: [] },
1865
+ autoWrapEnabled: false,
1866
+ autoWrapAfterSeconds: 0
1867
+ }, taskDataMock.agentId);
1868
+ });
1869
+
1870
+ describe('consultConference', () => {
1871
+
1872
+ it('should successfully start conference and emit event', async () => {
1873
+ const mockResponse = {
1874
+ trackingId: 'test-tracking-id',
1875
+ interactionId: taskId,
1876
+ };
1877
+ contactMock.consultConference.mockResolvedValue(mockResponse);
1878
+
1879
+
1880
+ const result = await task.consultConference();
1881
+
1882
+ expect(contactMock.consultConference).toHaveBeenCalledWith({
1883
+ interactionId: taskId,
1884
+ data: {
1885
+ agentId: taskDataMock.agentId, // From task data agent ID
1886
+ to: taskDataMock.destAgentId, // From calculateDestAgentId() using task participants
1887
+ destinationType: 'agent', // From consultation data
1888
+ },
1889
+ });
1890
+ expect(result).toEqual(mockResponse);
1891
+ expect(LoggerProxy.info).toHaveBeenCalledWith(`Initiating consult conference to ${taskDataMock.destAgentId}`, {
1892
+ module: TASK_FILE,
1893
+ method: 'consultConference',
1894
+ interactionId: taskId,
1895
+ });
1896
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Consult conference started successfully', {
1897
+ module: TASK_FILE,
1898
+ method: 'consultConference',
1899
+ interactionId: taskId,
1900
+ });
1901
+ });
1902
+
1903
+ it('should handle basic validation scenarios', async () => {
1904
+ // Agent Desktop logic validates data structure but not participant availability
1905
+ // This test confirms the method works with the Agent Desktop data flow
1906
+ const mockResponse = {
1907
+ trackingId: 'test-tracking-validation',
1908
+ interactionId: taskId,
1909
+ };
1910
+ contactMock.consultConference.mockResolvedValue(mockResponse);
1911
+
1912
+ const result = await task.consultConference();
1913
+ expect(result).toEqual(mockResponse);
1914
+ });
1915
+
1916
+ it('should handle and rethrow contact method errors', async () => {
1917
+ const mockError = new Error('Conference start failed');
1918
+ contactMock.consultConference.mockRejectedValue(mockError);
1919
+ generateTaskErrorObjectSpy.mockReturnValue(mockError);
1920
+
1921
+ await expect(task.consultConference()).rejects.toThrow('Conference start failed');
1922
+ expect(LoggerProxy.error).toHaveBeenCalledWith('Failed to start consult conference', {
1923
+ module: TASK_FILE,
1924
+ method: 'consultConference',
1925
+ interactionId: taskId,
1926
+ });
1927
+ });
1928
+
1929
+ it('should dynamically calculate destAgentId from participants when this.data.destAgentId is null', async () => {
1930
+ // Simulate scenario where destAgentId is not preserved (e.g., after hold/unhold)
1931
+ task.data.destAgentId = null;
1932
+
1933
+ const consultedAgentId = 'consulted-agent-123';
1934
+ calculateDestAgentIdSpy.mockReturnValue(consultedAgentId);
1935
+
1936
+ const mockResponse = {
1937
+ trackingId: 'test-tracking-dynamic',
1938
+ interactionId: taskId,
1939
+ };
1940
+ contactMock.consultConference.mockResolvedValue(mockResponse);
1941
+
1942
+ const result = await task.consultConference();
1943
+
1944
+ // Verify calculateDestAgentId was called to dynamically calculate the destination
1945
+ expect(calculateDestAgentIdSpy).toHaveBeenCalledWith(
1946
+ taskDataMock.interaction,
1947
+ taskDataMock.agentId
1948
+ );
1949
+
1950
+ // Verify the conference was called with the dynamically calculated destAgentId
1951
+ expect(contactMock.consultConference).toHaveBeenCalledWith({
1952
+ interactionId: taskId,
1953
+ data: {
1954
+ agentId: taskDataMock.agentId,
1955
+ to: consultedAgentId, // Dynamically calculated value
1956
+ destinationType: 'agent',
1957
+ },
1958
+ });
1959
+ expect(result).toEqual(mockResponse);
1960
+ });
1961
+
1962
+ it('should throw error when no destination agent is found (queue consult not accepted)', async () => {
1963
+ // Simulate queue consult scenario where no agent has accepted yet
1964
+ calculateDestAgentIdSpy.mockReturnValue(''); // No agent found
1965
+
1966
+ await expect(task.consultConference()).rejects.toThrow('No agent has accepted this queue consult yet');
1967
+
1968
+ // Verify the conference was NOT called
1969
+ expect(contactMock.consultConference).not.toHaveBeenCalled();
1970
+ });
1971
+
1972
+ it('should calculate destination type from participant type for regular agents', async () => {
1973
+ const destAgentId = 'consulted-agent-456';
1974
+
1975
+ calculateDestAgentIdSpy = jest.spyOn(Utils, 'calculateDestAgentId').mockReturnValue(destAgentId);
1976
+ calculateDestTypeSpy = jest.spyOn(Utils, 'calculateDestType').mockReturnValue('agent');
1977
+
1978
+ const mockResponse = {trackingId: 'test-tracking-id', interactionId: taskId};
1979
+ contactMock.consultConference.mockResolvedValue(mockResponse);
1980
+
1981
+ await task.consultConference();
1982
+
1983
+ expect(calculateDestTypeSpy).toHaveBeenCalledWith(
1984
+ task.data.interaction,
1985
+ taskDataMock.agentId
1986
+ );
1987
+
1988
+ expect(contactMock.consultConference).toHaveBeenCalledWith({
1989
+ interactionId: taskId,
1990
+ data: {
1991
+ agentId: taskDataMock.agentId,
1992
+ to: destAgentId,
1993
+ destinationType: 'agent',
1994
+ },
1995
+ });
1996
+ });
1997
+
1998
+ it('should use DN destination type for dial number participants', async () => {
1999
+ const destAgentId = 'dn-uuid-123';
2000
+
2001
+ calculateDestAgentIdSpy = jest.spyOn(Utils, 'calculateDestAgentId').mockReturnValue(destAgentId);
2002
+ calculateDestTypeSpy = jest.spyOn(Utils, 'calculateDestType').mockReturnValue('dialNumber');
2003
+
2004
+ const mockResponse = {trackingId: 'test-tracking-id-dn', interactionId: taskId};
2005
+ contactMock.consultConference.mockResolvedValue(mockResponse);
2006
+
2007
+ await task.consultConference();
2008
+
2009
+ expect(contactMock.consultConference).toHaveBeenCalledWith({
2010
+ interactionId: taskId,
2011
+ data: {
2012
+ agentId: taskDataMock.agentId,
2013
+ to: destAgentId,
2014
+ destinationType: 'dialNumber',
2015
+ },
2016
+ });
2017
+ });
2018
+
2019
+ it('should use EpDn destination type for entry point dial number participants', async () => {
2020
+ const destAgentId = 'epdn-uuid-456';
2021
+
2022
+ calculateDestAgentIdSpy = jest.spyOn(Utils, 'calculateDestAgentId').mockReturnValue(destAgentId);
2023
+ calculateDestTypeSpy = jest.spyOn(Utils, 'calculateDestType').mockReturnValue('entryPoint');
2024
+
2025
+ const mockResponse = {trackingId: 'test-tracking-id-epdn', interactionId: taskId};
2026
+ contactMock.consultConference.mockResolvedValue(mockResponse);
2027
+
2028
+ await task.consultConference();
2029
+
2030
+ expect(contactMock.consultConference).toHaveBeenCalledWith({
2031
+ interactionId: taskId,
2032
+ data: {
2033
+ agentId: taskDataMock.agentId,
2034
+ to: destAgentId,
2035
+ destinationType: 'entryPoint',
2036
+ },
2037
+ });
2038
+ });
2039
+
2040
+ it('should fall back to task.data.destinationType when calculateDestType returns empty', async () => {
2041
+ const destAgentId = 'consulted-agent-789';
2042
+
2043
+ calculateDestAgentIdSpy = jest.spyOn(Utils, 'calculateDestAgentId').mockReturnValue(destAgentId);
2044
+ calculateDestTypeSpy = jest.spyOn(Utils, 'calculateDestType').mockReturnValue(''); // No type found
2045
+
2046
+ task.data.destinationType = 'EPDN';
2047
+
2048
+ const mockResponse = {trackingId: 'test-tracking-id-fallback', interactionId: taskId};
2049
+ contactMock.consultConference.mockResolvedValue(mockResponse);
2050
+
2051
+ await task.consultConference();
2052
+
2053
+ expect(contactMock.consultConference).toHaveBeenCalledWith({
2054
+ interactionId: taskId,
2055
+ data: {
2056
+ agentId: taskDataMock.agentId,
2057
+ to: destAgentId,
2058
+ destinationType: 'EPDN', // Falls back to task.data.destinationType
2059
+ },
2060
+ });
2061
+ });
2062
+
2063
+ it('should handle CBT scenarios with correct destination type', async () => {
2064
+ const destAgentId = 'agent-cbt-uuid';
2065
+
2066
+ calculateDestAgentIdSpy = jest.spyOn(Utils, 'calculateDestAgentId').mockReturnValue(destAgentId);
2067
+ calculateDestTypeSpy = jest.spyOn(Utils, 'calculateDestType').mockReturnValue('dialNumber');
2068
+
2069
+ const mockResponse = {trackingId: 'test-tracking-id-cbt', interactionId: taskId};
2070
+ contactMock.consultConference.mockResolvedValue(mockResponse);
2071
+
2072
+ await task.consultConference();
2073
+
2074
+ expect(calculateDestTypeSpy).toHaveBeenCalledWith(
2075
+ task.data.interaction,
2076
+ taskDataMock.agentId
2077
+ );
2078
+
2079
+ expect(contactMock.consultConference).toHaveBeenCalledWith({
2080
+ interactionId: taskId,
2081
+ data: {
2082
+ agentId: taskDataMock.agentId,
2083
+ to: destAgentId,
2084
+ destinationType: 'dialNumber', // dialNumber for CBT scenarios
2085
+ },
2086
+ });
2087
+ });
2088
+ });
2089
+
2090
+ describe('exitConference', () => {
2091
+ it('should successfully end conference and emit event', async () => {
2092
+ const mockResponse = {
2093
+ trackingId: 'test-tracking-id-end',
2094
+ interactionId: taskId,
2095
+ };
2096
+ contactMock.exitConference.mockResolvedValue(mockResponse);
2097
+
2098
+ const result = await task.exitConference();
2099
+
2100
+ expect(contactMock.exitConference).toHaveBeenCalledWith({
2101
+ interactionId: taskId,
2102
+ });
2103
+ expect(result).toEqual(mockResponse);
2104
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Exiting consult conference', {
2105
+ module: TASK_FILE,
2106
+ method: 'exitConference',
2107
+ interactionId: taskId,
2108
+ });
2109
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Consult conference exited successfully', {
2110
+ module: TASK_FILE,
2111
+ method: 'exitConference',
2112
+ interactionId: taskId,
2113
+ });
2114
+ });
2115
+
2116
+ it('should throw error for invalid interaction ID', async () => {
2117
+ task.data.interactionId = '';
2118
+
2119
+ await expect(task.exitConference()).rejects.toThrow('Error while performing exitConference');
2120
+ expect(contactMock.exitConference).not.toHaveBeenCalled();
2121
+ });
2122
+
2123
+ it('should handle and rethrow contact method errors', async () => {
2124
+ const mockError = new Error('Conference end failed');
2125
+ contactMock.exitConference.mockRejectedValue(mockError);
2126
+ generateTaskErrorObjectSpy.mockReturnValue(mockError);
2127
+
2128
+ await expect(task.exitConference()).rejects.toThrow('Conference end failed');
2129
+ expect(LoggerProxy.error).toHaveBeenCalledWith('Failed to exit consult conference', {
2130
+ module: TASK_FILE,
2131
+ method: 'exitConference',
2132
+ interactionId: taskId,
2133
+ });
2134
+ });
2135
+ });
2136
+
2137
+ describe('transferConference', () => {
2138
+ it('should successfully transfer conference', async () => {
2139
+ const mockResponse = {
2140
+ trackingId: 'test-tracking-id-transfer',
2141
+ interactionId: taskId,
2142
+ };
2143
+ contactMock.conferenceTransfer.mockResolvedValue(mockResponse);
2144
+
2145
+ const result = await task.transferConference();
2146
+
2147
+ expect(contactMock.conferenceTransfer).toHaveBeenCalledWith({
2148
+ interactionId: taskId,
2149
+ });
2150
+ expect(result).toEqual(mockResponse);
2151
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Transferring conference', {
2152
+ module: TASK_FILE,
2153
+ method: 'transferConference',
2154
+ interactionId: taskId,
2155
+ });
2156
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Conference transferred successfully', {
2157
+ module: TASK_FILE,
2158
+ method: 'transferConference',
2159
+ interactionId: taskId,
2160
+ });
2161
+ });
2162
+
2163
+ it('should throw error for invalid interaction ID', async () => {
2164
+ task.data.interactionId = '';
2165
+
2166
+ await expect(task.transferConference()).rejects.toThrow('Error while performing transferConference');
2167
+ expect(contactMock.conferenceTransfer).not.toHaveBeenCalled();
2168
+ });
2169
+
2170
+ it('should handle and rethrow contact method errors', async () => {
2171
+ const mockError = new Error('Conference transfer failed');
2172
+ contactMock.conferenceTransfer.mockRejectedValue(mockError);
2173
+ generateTaskErrorObjectSpy.mockReturnValue(mockError);
2174
+
2175
+ await expect(task.transferConference()).rejects.toThrow('Conference transfer failed');
2176
+ expect(LoggerProxy.error).toHaveBeenCalledWith('Failed to transfer conference', {
2177
+ module: TASK_FILE,
2178
+ method: 'transferConference',
2179
+ interactionId: taskId,
2180
+ });
2181
+ });
2182
+ });
2183
+ });
1578
2184
  });