@webex/contact-center 3.11.0 → 3.12.0-mobius-socket.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 (126) hide show
  1. package/dist/cc.js +121 -28
  2. package/dist/cc.js.map +1 -1
  3. package/dist/constants.js +5 -1
  4. package/dist/constants.js.map +1 -1
  5. package/dist/index.js +7 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/metrics/behavioral-events.js +13 -0
  8. package/dist/metrics/behavioral-events.js.map +1 -1
  9. package/dist/metrics/constants.js +9 -1
  10. package/dist/metrics/constants.js.map +1 -1
  11. package/dist/services/ApiAiAssistant.js +173 -0
  12. package/dist/services/ApiAiAssistant.js.map +1 -0
  13. package/dist/services/agent/types.js.map +1 -1
  14. package/dist/services/config/Util.js +6 -2
  15. package/dist/services/config/Util.js.map +1 -1
  16. package/dist/services/config/constants.js +12 -0
  17. package/dist/services/config/constants.js.map +1 -1
  18. package/dist/services/config/index.js +41 -2
  19. package/dist/services/config/index.js.map +1 -1
  20. package/dist/services/config/types.js +19 -1
  21. package/dist/services/config/types.js.map +1 -1
  22. package/dist/services/constants.js +27 -1
  23. package/dist/services/constants.js.map +1 -1
  24. package/dist/services/core/Err.js.map +1 -1
  25. package/dist/services/core/Utils.js +28 -6
  26. package/dist/services/core/Utils.js.map +1 -1
  27. package/dist/services/core/aqm-reqs.js +92 -17
  28. package/dist/services/core/aqm-reqs.js.map +1 -1
  29. package/dist/services/core/websocket/WebSocketManager.js +20 -5
  30. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  31. package/dist/services/core/websocket/connection-service.js +3 -1
  32. package/dist/services/core/websocket/connection-service.js.map +1 -1
  33. package/dist/services/index.js +6 -0
  34. package/dist/services/index.js.map +1 -1
  35. package/dist/services/task/TaskManager.js +117 -24
  36. package/dist/services/task/TaskManager.js.map +1 -1
  37. package/dist/services/task/TaskUtils.js +16 -3
  38. package/dist/services/task/TaskUtils.js.map +1 -1
  39. package/dist/services/task/constants.js +15 -1
  40. package/dist/services/task/constants.js.map +1 -1
  41. package/dist/services/task/dialer.js +51 -0
  42. package/dist/services/task/dialer.js.map +1 -1
  43. package/dist/services/task/types.js +15 -0
  44. package/dist/services/task/types.js.map +1 -1
  45. package/dist/types/cc.d.ts +801 -0
  46. package/dist/types/config.d.ts +66 -0
  47. package/dist/types/constants.d.ts +50 -0
  48. package/dist/types/index.d.ts +184 -0
  49. package/dist/types/logger-proxy.d.ts +71 -0
  50. package/dist/types/metrics/MetricsManager.d.ts +223 -0
  51. package/dist/types/metrics/behavioral-events.d.ts +29 -0
  52. package/dist/types/metrics/constants.d.ts +161 -0
  53. package/dist/types/services/AddressBook.d.ts +74 -0
  54. package/dist/types/services/ApiAiAssistant.d.ts +31 -0
  55. package/dist/types/services/EntryPoint.d.ts +67 -0
  56. package/dist/types/services/Queue.d.ts +76 -0
  57. package/dist/types/services/WebCallingService.d.ts +1 -0
  58. package/dist/types/services/agent/index.d.ts +46 -0
  59. package/dist/types/services/agent/types.d.ts +413 -0
  60. package/dist/types/services/config/Util.d.ts +20 -0
  61. package/dist/types/services/config/constants.d.ts +249 -0
  62. package/dist/types/services/config/index.d.ts +177 -0
  63. package/dist/types/services/config/types.d.ts +1207 -0
  64. package/dist/types/services/constants.d.ts +110 -0
  65. package/dist/types/services/core/Err.d.ts +121 -0
  66. package/dist/types/services/core/GlobalTypes.d.ts +58 -0
  67. package/dist/types/services/core/Utils.d.ts +101 -0
  68. package/dist/types/services/core/WebexRequest.d.ts +22 -0
  69. package/dist/types/services/core/aqm-reqs.d.ts +65 -0
  70. package/dist/types/services/core/constants.d.ts +99 -0
  71. package/dist/types/services/core/types.d.ts +47 -0
  72. package/dist/types/services/core/websocket/WebSocketManager.d.ts +35 -0
  73. package/dist/types/services/core/websocket/connection-service.d.ts +27 -0
  74. package/dist/types/services/core/websocket/keepalive.worker.d.ts +2 -0
  75. package/dist/types/services/core/websocket/types.d.ts +37 -0
  76. package/dist/types/services/index.d.ts +54 -0
  77. package/dist/types/services/task/AutoWrapup.d.ts +40 -0
  78. package/dist/types/services/task/TaskManager.d.ts +1 -0
  79. package/dist/types/services/task/TaskUtils.d.ts +92 -0
  80. package/dist/types/services/task/constants.d.ts +84 -0
  81. package/dist/types/services/task/contact.d.ts +69 -0
  82. package/dist/types/services/task/dialer.d.ts +43 -0
  83. package/dist/types/services/task/index.d.ts +650 -0
  84. package/dist/types/services/task/types.d.ts +1319 -0
  85. package/dist/types/types.d.ts +643 -0
  86. package/dist/types/utils/PageCache.d.ts +173 -0
  87. package/dist/types/webex-config.d.ts +53 -0
  88. package/dist/types/webex.d.ts +7 -0
  89. package/dist/types.js +14 -1
  90. package/dist/types.js.map +1 -1
  91. package/dist/webex.js +1 -1
  92. package/package.json +9 -9
  93. package/src/cc.ts +157 -30
  94. package/src/constants.ts +4 -0
  95. package/src/index.ts +1 -0
  96. package/src/metrics/behavioral-events.ts +14 -0
  97. package/src/metrics/constants.ts +11 -0
  98. package/src/services/ApiAiAssistant.ts +217 -0
  99. package/src/services/agent/types.ts +1 -1
  100. package/src/services/config/Util.ts +8 -0
  101. package/src/services/config/constants.ts +12 -0
  102. package/src/services/config/index.ts +45 -1
  103. package/src/services/config/types.ts +67 -0
  104. package/src/services/constants.ts +29 -0
  105. package/src/services/core/Err.ts +1 -0
  106. package/src/services/core/Utils.ts +32 -5
  107. package/src/services/core/aqm-reqs.ts +100 -22
  108. package/src/services/core/websocket/WebSocketManager.ts +21 -6
  109. package/src/services/core/websocket/connection-service.ts +5 -1
  110. package/src/services/index.ts +4 -0
  111. package/src/services/task/TaskManager.ts +174 -27
  112. package/src/services/task/TaskUtils.ts +12 -0
  113. package/src/services/task/constants.ts +16 -0
  114. package/src/services/task/dialer.ts +56 -1
  115. package/src/services/task/types.ts +24 -0
  116. package/src/types.ts +40 -1
  117. package/test/unit/spec/cc.ts +163 -23
  118. package/test/unit/spec/services/ApiAiAssistant.ts +115 -0
  119. package/test/unit/spec/services/config/index.ts +56 -0
  120. package/test/unit/spec/services/core/Utils.ts +63 -1
  121. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +82 -12
  122. package/test/unit/spec/services/core/websocket/connection-service.ts +3 -1
  123. package/test/unit/spec/services/task/TaskManager.ts +1119 -251
  124. package/test/unit/spec/services/task/dialer.ts +198 -112
  125. package/umd/contact-center.min.js +2 -2
  126. package/umd/contact-center.min.js.map +1 -1
@@ -13,6 +13,7 @@ import {CC_TASK_EVENTS} from '../../../../../src/services/config/types';
13
13
 
14
14
  describe('TaskManager', () => {
15
15
  let mockCall;
16
+ let mockApiAIAssistant;
16
17
  let webSocketManagerMock;
17
18
  let onSpy;
18
19
  let offSpy;
@@ -45,6 +46,15 @@ describe('TaskManager', () => {
45
46
  beforeEach(() => {
46
47
  contactMock = contact;
47
48
  webSocketManagerMock = new EventEmitter();
49
+ mockApiAIAssistant = {
50
+ sendEvent: jest.fn().mockResolvedValue({}),
51
+ setAIFeatureFlags: jest.fn(),
52
+ aiFeature: {
53
+ realtimeTranscripts: {
54
+ enable: true,
55
+ },
56
+ },
57
+ };
48
58
 
49
59
  webex = {
50
60
  logger: {
@@ -74,14 +84,19 @@ describe('TaskManager', () => {
74
84
  onSpy = jest.spyOn(webCallingService, 'on');
75
85
  offSpy = jest.spyOn(webCallingService, 'off');
76
86
 
77
- taskManager = new TaskManager(contactMock, webCallingService, webSocketManagerMock);
78
- taskManager.taskCollection[taskId] = {
87
+ taskManager = new TaskManager(mockApiAIAssistant, contactMock, webCallingService, webSocketManagerMock);
88
+ const taskMock = {
79
89
  emit: jest.fn(),
80
90
  accept: jest.fn(),
81
91
  decline: jest.fn(),
82
- updateTaskData: jest.fn(),
92
+ updateTaskData: jest.fn().mockImplementation((updatedData) => {
93
+ taskMock.data = {...taskMock.data, ...updatedData};
94
+ return taskMock;
95
+ }),
83
96
  data: taskDataMock,
84
97
  };
98
+ taskManager.taskCollection[taskId] = taskMock;
99
+ taskManager.agentId = 'test-agent-id';
85
100
  taskManager.call = mockCall;
86
101
  });
87
102
 
@@ -102,14 +117,15 @@ describe('TaskManager', () => {
102
117
 
103
118
  incomingCallCb(mockCall);
104
119
 
105
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, taskManager.getTask(taskId));
120
+ expect(taskEmitSpy).toHaveBeenCalledWith(
121
+ TASK_EVENTS.TASK_INCOMING,
122
+ taskManager.getTask(taskId)
123
+ );
106
124
  });
107
125
 
108
126
  it('should re-emit task related events', () => {
109
127
  const dummyPayload = {
110
- data: {...taskDataMock,
111
- type: CC_TASK_EVENTS.AGENT_CONSULTING,
112
- },
128
+ data: {...taskDataMock, type: CC_TASK_EVENTS.AGENT_CONSULTING},
113
129
  };
114
130
  webSocketManagerMock.emit('message', JSON.stringify({data: taskDataMock}));
115
131
  const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
@@ -123,6 +139,106 @@ describe('TaskManager', () => {
123
139
  expect(taskEmitSpy).toHaveBeenCalledWith(dummyPayload.data.type, dummyPayload.data);
124
140
  });
125
141
 
142
+ it('should invoke sendEvent for configured start/stop backend events', () => {
143
+ const message = (type: CC_EVENTS) =>
144
+ JSON.stringify({
145
+ data: {
146
+ ...taskDataMock,
147
+ taskId,
148
+ type,
149
+ },
150
+ });
151
+
152
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONTACT_ASSIGNED));
153
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
154
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_CONFERENCED));
155
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_ENDED));
156
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
157
+ webSocketManagerMock.emit('message', message(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE));
158
+
159
+ expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledTimes(6);
160
+ expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledWith(
161
+ 'test-agent-id',
162
+ taskId,
163
+ 'CUSTOM_EVENT',
164
+ 'GET_TRANSCRIPTS',
165
+ 'START'
166
+ );
167
+ expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledWith(
168
+ 'test-agent-id',
169
+ taskId,
170
+ 'CUSTOM_EVENT',
171
+ 'GET_TRANSCRIPTS',
172
+ 'STOP'
173
+ );
174
+ });
175
+
176
+ it('should not invoke sendEvent for transcript events when realtime transcript feature is disabled', () => {
177
+ mockApiAIAssistant.aiFeature = {
178
+ realtimeTranscripts: {
179
+ enable: false,
180
+ },
181
+ };
182
+ mockApiAIAssistant.setAIFeatureFlags(mockApiAIAssistant.aiFeature);
183
+
184
+ const message = (type: CC_EVENTS) =>
185
+ JSON.stringify({
186
+ data: {
187
+ ...taskDataMock,
188
+ taskId,
189
+ type,
190
+ },
191
+ });
192
+
193
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONTACT_ASSIGNED));
194
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
195
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_CONFERENCED));
196
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_ENDED));
197
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
198
+ webSocketManagerMock.emit('message', message(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE));
199
+
200
+ expect(mockApiAIAssistant.sendEvent).not.toHaveBeenCalled();
201
+ });
202
+
203
+ it('should emit REAL_TIME_TRANSCRIPTION from task object', () => {
204
+ const task = taskManager.getTask(taskId);
205
+ const taskEmitSpy = jest.spyOn(task, 'emit');
206
+ const realtimePayload = {
207
+ data: {
208
+ ...taskDataMock,
209
+ type: CC_EVENTS.REAL_TIME_TRANSCRIPTION,
210
+ data: {
211
+ content: 'hello from transcript',
212
+ },
213
+ },
214
+ };
215
+
216
+ webSocketManagerMock.emit('message', JSON.stringify(realtimePayload));
217
+
218
+ expect(taskEmitSpy).toHaveBeenCalledWith(
219
+ CC_EVENTS.REAL_TIME_TRANSCRIPTION,
220
+ realtimePayload.data
221
+ );
222
+ });
223
+
224
+ it('should emit REAL_TIME_TRANSCRIPTION from RTD websocket payload on task object', () => {
225
+ const task = taskManager.getTask(taskId);
226
+ const taskEmitSpy = jest.spyOn(task, 'emit');
227
+ const realtimePayload = {
228
+ data: {
229
+ notifType: CC_EVENTS.REAL_TIME_TRANSCRIPTION,
230
+ data: {
231
+ conversationId: taskId,
232
+ content: 'hello from rtd websocket',
233
+ },
234
+ },
235
+ };
236
+
237
+ taskManager.handleRealtimeWebsocketEvent(JSON.stringify(realtimePayload));
238
+
239
+ expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.REAL_TIME_TRANSCRIPTION, realtimePayload.data);
240
+ });
241
+
126
242
  it('should not re-emit agent related events', () => {
127
243
  const dummyPayload = {
128
244
  data: {
@@ -187,7 +303,10 @@ describe('TaskManager', () => {
187
303
  },
188
304
  };
189
305
 
190
- const currentTaskAssignedSpy = jest.spyOn(taskManager.getTask(payload.data.interactionId), 'emit');
306
+ const currentTaskAssignedSpy = jest.spyOn(
307
+ taskManager.getTask(payload.data.interactionId),
308
+ 'emit'
309
+ );
191
310
 
192
311
  webSocketManagerMock.emit('message', JSON.stringify(assignedPayload));
193
312
 
@@ -311,7 +430,10 @@ describe('TaskManager', () => {
311
430
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
312
431
 
313
432
  const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
314
- const webCallListenerSpy = jest.spyOn(taskManager.getTask(taskId), 'unregisterWebCallListeners');
433
+ const webCallListenerSpy = jest.spyOn(
434
+ taskManager.getTask(taskId),
435
+ 'unregisterWebCallListeners'
436
+ );
315
437
  const callOffSpy = jest.spyOn(mockCall, 'off');
316
438
  const payload = {
317
439
  data: {
@@ -331,11 +453,9 @@ describe('TaskManager', () => {
331
453
  };
332
454
 
333
455
  taskManager.getTask(taskId).data = payload.data;
334
- const task = taskManager.getTask(taskId)
456
+ const task = taskManager.getTask(taskId);
335
457
  webSocketManagerMock.emit('message', JSON.stringify(payload));
336
- expect(taskEmitSpy).toHaveBeenCalledWith(
337
- TASK_EVENTS.TASK_END, task
338
- );
458
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
339
459
  expect(webCallListenerSpy).toHaveBeenCalledWith();
340
460
  expect(callOffSpy).toHaveBeenCalledWith(
341
461
  CALL_EVENT_KEYS.REMOTE_MEDIA,
@@ -371,55 +491,42 @@ describe('TaskManager', () => {
371
491
 
372
492
  taskManager.getTask(taskId).updateTaskData(payload.data);
373
493
  webSocketManagerMock.emit('message', JSON.stringify(payload));
374
- expect(taskEmitSpy).toHaveBeenCalledWith(
375
- CC_EVENTS.CONTACT_ENDED,
376
- { ...payload.data}
377
- );
378
- expect(taskEmitSpy).toHaveBeenCalledWith(
379
- TASK_EVENTS.TASK_END,
380
- taskManager.getTask(taskId)
381
- );
494
+ expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.CONTACT_ENDED, {...payload.data});
495
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, taskManager.getTask(taskId));
382
496
  });
383
497
 
384
498
  it('should emit TASK_REJECT event on AGENT_INVITE_FAILED event', () => {
385
499
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
386
500
 
387
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
388
- const metricsTrackSpy = jest.spyOn(taskManager.metricsManager, 'trackEvent');
389
- const payload = {
390
- data: {
391
- type: CC_EVENTS.AGENT_INVITE_FAILED,
392
- agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
393
- eventTime: 1733211616959,
394
- eventType: 'RoutingMessage',
395
- interaction: {state: 'connected'},
396
- interactionId: taskId,
397
- orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
398
- trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
399
- mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
400
- destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
401
- owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
402
- queueMgr: 'aqm',
403
- reason: 'INVITE_FAILED',
404
- },
405
- };
501
+ const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
502
+ const metricsTrackSpy = jest.spyOn(taskManager.metricsManager, 'trackEvent');
503
+ const payload = {
504
+ data: {
505
+ type: CC_EVENTS.AGENT_INVITE_FAILED,
506
+ agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
507
+ eventTime: 1733211616959,
508
+ eventType: 'RoutingMessage',
509
+ interaction: {state: 'connected'},
510
+ interactionId: taskId,
511
+ orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
512
+ trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
513
+ mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
514
+ destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
515
+ owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
516
+ queueMgr: 'aqm',
517
+ reason: 'INVITE_FAILED',
518
+ },
519
+ };
406
520
 
407
- taskManager.getTask(taskId).updateTaskData(payload.data);
408
- webSocketManagerMock.emit('message', JSON.stringify(payload));
409
- expect(taskEmitSpy).toHaveBeenCalledWith(
410
- CC_EVENTS.AGENT_INVITE_FAILED,
411
- { ...payload.data}
412
- );
413
- expect(taskEmitSpy).toHaveBeenCalledWith(
414
- TASK_EVENTS.TASK_REJECT,
415
- payload.data.reason
416
- );
417
- // Verify the correct metric event name is used for AGENT_INVITE_FAILED
418
- expect(metricsTrackSpy).toHaveBeenCalled();
419
- expect(metricsTrackSpy.mock.calls[0][0]).toBe('Agent Invite Failed');
521
+ taskManager.getTask(taskId).updateTaskData(payload.data);
522
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
523
+ expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.AGENT_INVITE_FAILED, {...payload.data});
524
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_REJECT, payload.data.reason);
525
+ // Verify the correct metric event name is used for AGENT_INVITE_FAILED
526
+ expect(metricsTrackSpy).toHaveBeenCalled();
527
+ expect(metricsTrackSpy.mock.calls[0][0]).toBe('Agent Invite Failed');
420
528
  });
421
529
 
422
-
423
530
  it('should not emit TASK_HYDRATE if task is already present in taskManager', () => {
424
531
  const payload = {
425
532
  data: {
@@ -483,7 +590,7 @@ describe('TaskManager', () => {
483
590
  const testAgentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
484
591
  taskManager.setAgentId(testAgentId);
485
592
  taskManager.taskCollection = [];
486
-
593
+
487
594
  const payload = {
488
595
  data: {
489
596
  ...initalPayload.data,
@@ -492,9 +599,9 @@ describe('TaskManager', () => {
492
599
  mediaType: 'telephony',
493
600
  state: 'conference',
494
601
  participants: {
495
- [testAgentId]: { pType: 'Agent', hasLeft: false },
496
- 'agent-2': { pType: 'Agent', hasLeft: false },
497
- 'customer-1': { pType: 'Customer', hasLeft: false },
602
+ [testAgentId]: {pType: 'Agent', hasLeft: false},
603
+ 'agent-2': {pType: 'Agent', hasLeft: false},
604
+ 'customer-1': {pType: 'Customer', hasLeft: false},
498
605
  },
499
606
  media: {
500
607
  [taskId]: {
@@ -517,7 +624,7 @@ describe('TaskManager', () => {
517
624
  const testAgentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
518
625
  taskManager.setAgentId(testAgentId);
519
626
  taskManager.taskCollection = [];
520
-
627
+
521
628
  const payload = {
522
629
  data: {
523
630
  ...initalPayload.data,
@@ -526,8 +633,8 @@ describe('TaskManager', () => {
526
633
  mediaType: 'telephony',
527
634
  state: 'connected',
528
635
  participants: {
529
- [testAgentId]: { pType: 'Agent', hasLeft: false },
530
- 'customer-1': { pType: 'Customer', hasLeft: false },
636
+ [testAgentId]: {pType: 'Agent', hasLeft: false},
637
+ 'customer-1': {pType: 'Customer', hasLeft: false},
531
638
  },
532
639
  media: {
533
640
  [taskId]: {
@@ -563,7 +670,7 @@ describe('TaskManager', () => {
563
670
  destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
564
671
  owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
565
672
  queueMgr: 'aqm',
566
- wrapUpRequired: true
673
+ wrapUpRequired: true,
567
674
  },
568
675
  };
569
676
 
@@ -716,7 +823,9 @@ describe('TaskManager', () => {
716
823
 
717
824
  const task = taskManager.getTask(taskId);
718
825
  const taskEmitSpy = jest.spyOn(task, 'emit');
719
- const taskAcceptSpy = jest.spyOn(task, 'accept').mockRejectedValue(new Error('Accept failed'));
826
+ const taskAcceptSpy = jest
827
+ .spyOn(task, 'accept')
828
+ .mockRejectedValue(new Error('Accept failed'));
720
829
 
721
830
  // Step 2: Trigger AGENT_OFFER_CONTACT with auto-answer (will fail)
722
831
  const autoAnswerPayload = {
@@ -778,7 +887,7 @@ describe('TaskManager', () => {
778
887
  // Verify BOTH events were emitted
779
888
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OFFER_CONSULT, task);
780
889
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
781
-
890
+
782
891
  // Verify isConsulted flag is set correctly
783
892
  expect(task.data.isConsulted).toBe(true);
784
893
  });
@@ -814,7 +923,10 @@ describe('TaskManager', () => {
814
923
  expect(taskAcceptSpy).not.toHaveBeenCalled();
815
924
 
816
925
  // Verify TASK_AUTO_ANSWERED event was NOT emitted
817
- expect(taskEmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, expect.anything());
926
+ expect(taskEmitSpy).not.toHaveBeenCalledWith(
927
+ TASK_EVENTS.TASK_AUTO_ANSWERED,
928
+ expect.anything()
929
+ );
818
930
  });
819
931
  });
820
932
 
@@ -896,6 +1008,9 @@ describe('TaskManager', () => {
896
1008
  });
897
1009
 
898
1010
  it('should NOT remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp exists', () => {
1011
+ const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
1012
+ taskManager.setAgentId(agentId);
1013
+
899
1014
  const task = taskManager.getTask(taskId);
900
1015
  task.updateTaskData = jest.fn().mockImplementation((newData) => {
901
1016
  task.data = {
@@ -907,7 +1022,7 @@ describe('TaskManager', () => {
907
1022
  state: 'new',
908
1023
  mediaType: 'telephony',
909
1024
  },
910
- agentsPendingWrapUp: ['agent-123'],
1025
+ agentsPendingWrapUp: [agentId],
911
1026
  };
912
1027
  return task;
913
1028
  });
@@ -923,7 +1038,7 @@ describe('TaskManager', () => {
923
1038
  state: 'new',
924
1039
  mediaType: 'telephony',
925
1040
  },
926
- agentsPendingWrapUp: ['agent-123'],
1041
+ agentsPendingWrapUp: [agentId],
927
1042
  },
928
1043
  };
929
1044
 
@@ -1023,107 +1138,370 @@ describe('TaskManager', () => {
1023
1138
  }).not.toThrow();
1024
1139
  });
1025
1140
 
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,
1141
+ describe('wrapUpRequired logic in CONTACT_ENDED event', () => {
1142
+ const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
1143
+
1144
+ beforeEach(() => {
1145
+ // Set the agent ID on taskManager
1146
+ taskManager.setAgentId(agentId);
1147
+ });
1148
+
1149
+ it('should set wrapUpRequired to true when agent is in agentsPendingWrapUp array', () => {
1150
+ const task = taskManager.getTask(taskId);
1151
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1152
+ task.data = {
1153
+ ...task.data,
1154
+ ...newData,
1155
+ };
1156
+ return task;
1157
+ });
1158
+ task.unregisterWebCallListeners = jest.fn();
1159
+
1160
+ const payload = {
1161
+ data: {
1162
+ type: CC_EVENTS.CONTACT_ENDED,
1163
+ interactionId: taskId,
1164
+ interaction: {
1165
+ state: 'connected',
1166
+ mediaType: 'telephony',
1167
+ },
1168
+ agentsPendingWrapUp: [agentId, 'other-agent-id'],
1038
1169
  },
1039
1170
  };
1040
- return task;
1171
+
1172
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1173
+
1174
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1175
+ expect.objectContaining({
1176
+ wrapUpRequired: true,
1177
+ })
1178
+ );
1041
1179
  });
1042
- task.unregisterWebCallListeners = jest.fn();
1043
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1044
1180
 
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,
1181
+ it('should set wrapUpRequired to false when agent is not in agentsPendingWrapUp array', () => {
1182
+ const task = taskManager.getTask(taskId);
1183
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1184
+ task.data = {
1185
+ ...task.data,
1186
+ ...newData,
1187
+ };
1188
+ return task;
1189
+ });
1190
+ task.unregisterWebCallListeners = jest.fn();
1191
+
1192
+ const payload = {
1193
+ data: {
1194
+ type: CC_EVENTS.CONTACT_ENDED,
1195
+ interactionId: taskId,
1196
+ interaction: {
1197
+ state: 'connected',
1198
+ mediaType: 'telephony',
1199
+ },
1200
+ agentsPendingWrapUp: ['other-agent-id', 'another-agent-id'],
1055
1201
  },
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
- };
1202
+ };
1067
1203
 
1068
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1204
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1069
1205
 
1070
- expect(taskManager.getTask(taskId)).toBeUndefined();
1071
- expect(removeTaskSpy).toHaveBeenCalled();
1072
- });
1206
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1207
+ expect.objectContaining({
1208
+ wrapUpRequired: false,
1209
+ })
1210
+ );
1211
+ });
1073
1212
 
1074
- it('handle AGENT_OFFER_CONSULT event', () => {
1075
- const payload = {
1076
- data: {
1077
- ...initalPayload.data,
1078
- type: CC_EVENTS.AGENT_OFFER_CONSULT,
1079
- },
1080
- };
1213
+ it('should set wrapUpRequired to false when agentsPendingWrapUp is an empty array', () => {
1214
+ const task = taskManager.getTask(taskId);
1215
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1216
+ task.data = {
1217
+ ...task.data,
1218
+ ...newData,
1219
+ };
1220
+ return task;
1221
+ });
1222
+ task.unregisterWebCallListeners = jest.fn();
1081
1223
 
1082
- webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1083
- const task = taskManager.getTask(taskId);
1084
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1085
- task.data = {...newData, isConsulted: true};
1086
- return task;
1087
- });
1088
- const taskEmitSpy = jest.spyOn(task, 'emit');
1224
+ const payload = {
1225
+ data: {
1226
+ type: CC_EVENTS.CONTACT_ENDED,
1227
+ interactionId: taskId,
1228
+ interaction: {
1229
+ state: 'connected',
1230
+ mediaType: 'telephony',
1231
+ },
1232
+ agentsPendingWrapUp: [],
1233
+ },
1234
+ };
1089
1235
 
1090
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1236
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1091
1237
 
1092
- expect(task.updateTaskData).toHaveBeenCalledWith({
1093
- ...payload.data,
1094
- isConsulted: true,
1238
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1239
+ expect.objectContaining({
1240
+ wrapUpRequired: false,
1241
+ })
1242
+ );
1095
1243
  });
1096
- expect(task.data.isConsulted).toBe(true);
1097
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OFFER_CONSULT, task);
1098
- });
1099
1244
 
1100
- it('should emit TASK_CONSULT_ACCEPTED event on AGENT_CONSULTING event', () => {
1101
- const initialConsultingPayload = {
1102
- data: {
1103
- ...initalPayload.data,
1104
- type: CC_EVENTS.AGENT_OFFER_CONSULT,
1105
- },
1106
- };
1245
+ it('should set wrapUpRequired to false when agentsPendingWrapUp is undefined', () => {
1246
+ const task = taskManager.getTask(taskId);
1247
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1248
+ task.data = {
1249
+ ...task.data,
1250
+ ...newData,
1251
+ };
1252
+ return task;
1253
+ });
1254
+ task.unregisterWebCallListeners = jest.fn();
1107
1255
 
1108
- const consultingPayload = {
1109
- data: {
1110
- ...initalPayload.data,
1111
- type: CC_EVENTS.AGENT_CONSULTING,
1112
- },
1113
- };
1256
+ const payload = {
1257
+ data: {
1258
+ type: CC_EVENTS.CONTACT_ENDED,
1259
+ interactionId: taskId,
1260
+ interaction: {
1261
+ state: 'connected',
1262
+ mediaType: 'telephony',
1263
+ },
1264
+ // agentsPendingWrapUp is not defined
1265
+ },
1266
+ };
1114
1267
 
1115
- webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1116
- taskManager.getTask(taskId).updateTaskData = jest.fn().mockImplementation((newData) => {
1117
- taskManager.getTask(taskId).data = {...newData, isConsulted: true};
1118
- return taskManager.getTask(taskId);
1268
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1269
+
1270
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1271
+ expect.objectContaining({
1272
+ wrapUpRequired: false,
1273
+ })
1274
+ );
1119
1275
  });
1120
1276
 
1121
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1122
- webSocketManagerMock.emit('message', JSON.stringify(initialConsultingPayload));
1123
- webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
1124
- expect(taskManager.getTask(taskId).data.isConsulted).toBe(true);
1125
- expect(taskEmitSpy).toHaveBeenCalledWith(
1126
- TASK_EVENTS.TASK_CONSULT_ACCEPTED,
1277
+ it('should set wrapUpRequired to false when agentsPendingWrapUp is null', () => {
1278
+ const task = taskManager.getTask(taskId);
1279
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1280
+ task.data = {
1281
+ ...task.data,
1282
+ ...newData,
1283
+ };
1284
+ return task;
1285
+ });
1286
+ task.unregisterWebCallListeners = jest.fn();
1287
+
1288
+ const payload = {
1289
+ data: {
1290
+ type: CC_EVENTS.CONTACT_ENDED,
1291
+ interactionId: taskId,
1292
+ interaction: {
1293
+ state: 'connected',
1294
+ mediaType: 'telephony',
1295
+ },
1296
+ agentsPendingWrapUp: null,
1297
+ },
1298
+ };
1299
+
1300
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1301
+
1302
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1303
+ expect.objectContaining({
1304
+ wrapUpRequired: false,
1305
+ })
1306
+ );
1307
+ });
1308
+
1309
+ it('should set wrapUpRequired correctly when agent is the only one in agentsPendingWrapUp', () => {
1310
+ const task = taskManager.getTask(taskId);
1311
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1312
+ task.data = {
1313
+ ...task.data,
1314
+ ...newData,
1315
+ };
1316
+ return task;
1317
+ });
1318
+ task.unregisterWebCallListeners = jest.fn();
1319
+
1320
+ const payload = {
1321
+ data: {
1322
+ type: CC_EVENTS.CONTACT_ENDED,
1323
+ interactionId: taskId,
1324
+ interaction: {
1325
+ state: 'connected',
1326
+ mediaType: 'telephony',
1327
+ },
1328
+ agentsPendingWrapUp: [agentId],
1329
+ },
1330
+ };
1331
+
1332
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1333
+
1334
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1335
+ expect.objectContaining({
1336
+ wrapUpRequired: true,
1337
+ })
1338
+ );
1339
+ });
1340
+
1341
+ it('should work correctly for different interaction states when agent is in agentsPendingWrapUp', () => {
1342
+ const task = taskManager.getTask(taskId);
1343
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1344
+ task.data = {
1345
+ ...task.data,
1346
+ ...newData,
1347
+ interaction: {
1348
+ ...task.data.interaction,
1349
+ ...newData.interaction,
1350
+ },
1351
+ };
1352
+ return task;
1353
+ });
1354
+ task.unregisterWebCallListeners = jest.fn();
1355
+
1356
+ // Test with 'connected' state
1357
+ const payloadConnected = {
1358
+ data: {
1359
+ type: CC_EVENTS.CONTACT_ENDED,
1360
+ interactionId: taskId,
1361
+ interaction: {
1362
+ state: 'connected',
1363
+ mediaType: 'telephony',
1364
+ },
1365
+ agentsPendingWrapUp: [agentId],
1366
+ },
1367
+ };
1368
+
1369
+ webSocketManagerMock.emit('message', JSON.stringify(payloadConnected));
1370
+
1371
+ // First call should set wrapUpRequired to true
1372
+ expect(task.updateTaskData).toHaveBeenNthCalledWith(
1373
+ 1,
1374
+ expect.objectContaining({
1375
+ wrapUpRequired: true,
1376
+ })
1377
+ );
1378
+
1379
+ // Test with 'held' state to verify it still works regardless of state
1380
+ const payloadHeld = {
1381
+ data: {
1382
+ type: CC_EVENTS.CONTACT_ENDED,
1383
+ interactionId: taskId,
1384
+ interaction: {
1385
+ state: 'held',
1386
+ mediaType: 'telephony',
1387
+ },
1388
+ agentsPendingWrapUp: [agentId],
1389
+ },
1390
+ };
1391
+
1392
+ webSocketManagerMock.emit('message', JSON.stringify(payloadHeld));
1393
+
1394
+ // Second call should also set wrapUpRequired to true
1395
+ expect(task.updateTaskData).toHaveBeenNthCalledWith(
1396
+ 2,
1397
+ expect.objectContaining({
1398
+ wrapUpRequired: true,
1399
+ })
1400
+ );
1401
+ });
1402
+ });
1403
+
1404
+ it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
1405
+ const task = taskManager.getTask(taskId);
1406
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1407
+ task.data = {
1408
+ ...task.data,
1409
+ ...newData,
1410
+ interaction: {
1411
+ ...task.data.interaction,
1412
+ ...newData.interaction,
1413
+ outboundType: 'OUTDIAL',
1414
+ state: 'new',
1415
+ isTerminated: false,
1416
+ },
1417
+ };
1418
+ return task;
1419
+ });
1420
+ task.unregisterWebCallListeners = jest.fn();
1421
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1422
+
1423
+ const payload = {
1424
+ data: {
1425
+ type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
1426
+ agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
1427
+ eventTime: 1733211616959,
1428
+ eventType: 'RoutingMessage',
1429
+ interaction: {
1430
+ outboundType: 'OUTDIAL',
1431
+ state: 'new',
1432
+ isTerminated: false,
1433
+ },
1434
+ interactionId: taskId,
1435
+ orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
1436
+ trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
1437
+ mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
1438
+ destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
1439
+ owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
1440
+ queueMgr: 'aqm',
1441
+ reason: 'USER_DECLINED',
1442
+ reasonCode: 156,
1443
+ },
1444
+ };
1445
+
1446
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1447
+
1448
+ expect(taskManager.getTask(taskId)).toBeUndefined();
1449
+ expect(removeTaskSpy).toHaveBeenCalled();
1450
+ });
1451
+
1452
+ it('handle AGENT_OFFER_CONSULT event', () => {
1453
+ const payload = {
1454
+ data: {
1455
+ ...initalPayload.data,
1456
+ type: CC_EVENTS.AGENT_OFFER_CONSULT,
1457
+ },
1458
+ };
1459
+
1460
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1461
+ const task = taskManager.getTask(taskId);
1462
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1463
+ task.data = {...newData, isConsulted: true};
1464
+ return task;
1465
+ });
1466
+ const taskEmitSpy = jest.spyOn(task, 'emit');
1467
+
1468
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1469
+
1470
+ expect(task.updateTaskData).toHaveBeenCalledWith({
1471
+ ...payload.data,
1472
+ isConsulted: true,
1473
+ });
1474
+ expect(task.data.isConsulted).toBe(true);
1475
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OFFER_CONSULT, task);
1476
+ });
1477
+
1478
+ it('should emit TASK_CONSULT_ACCEPTED event on AGENT_CONSULTING event', () => {
1479
+ const initialConsultingPayload = {
1480
+ data: {
1481
+ ...initalPayload.data,
1482
+ type: CC_EVENTS.AGENT_OFFER_CONSULT,
1483
+ },
1484
+ };
1485
+
1486
+ const consultingPayload = {
1487
+ data: {
1488
+ ...initalPayload.data,
1489
+ type: CC_EVENTS.AGENT_CONSULTING,
1490
+ },
1491
+ };
1492
+
1493
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1494
+ taskManager.getTask(taskId).updateTaskData = jest.fn().mockImplementation((newData) => {
1495
+ taskManager.getTask(taskId).data = {...newData, isConsulted: true};
1496
+ return taskManager.getTask(taskId);
1497
+ });
1498
+
1499
+ const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1500
+ webSocketManagerMock.emit('message', JSON.stringify(initialConsultingPayload));
1501
+ webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
1502
+ expect(taskManager.getTask(taskId).data.isConsulted).toBe(true);
1503
+ expect(taskEmitSpy).toHaveBeenCalledWith(
1504
+ TASK_EVENTS.TASK_CONSULT_ACCEPTED,
1127
1505
  taskManager.getTask(taskId)
1128
1506
  );
1129
1507
  });
@@ -1141,7 +1519,10 @@ describe('TaskManager', () => {
1141
1519
  const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1142
1520
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1143
1521
  expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
1144
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONSULT_END, taskManager.getTask(taskId));
1522
+ expect(taskEmitSpy).toHaveBeenCalledWith(
1523
+ TASK_EVENTS.TASK_CONSULT_END,
1524
+ taskManager.getTask(taskId)
1525
+ );
1145
1526
  });
1146
1527
 
1147
1528
  it('should emit TASK_CONSULT_ENDED event and remove currentTask when on AGENT_CONSULT_ENDED event when requested for a consult', () => {
@@ -1322,7 +1703,10 @@ describe('TaskManager', () => {
1322
1703
  webSocketManagerMock.emit('message', JSON.stringify(assignFailedPayload));
1323
1704
 
1324
1705
  expect(taskUpdateDataSpy).toHaveBeenCalledWith(assignFailedPayload.data);
1325
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_REJECT, assignFailedPayload.data.reason);
1706
+ expect(taskEmitSpy).toHaveBeenCalledWith(
1707
+ TASK_EVENTS.TASK_REJECT,
1708
+ assignFailedPayload.data.reason
1709
+ );
1326
1710
  // Verify the correct metric event name is used for AGENT_CONTACT_ASSIGN_FAILED
1327
1711
  expect(metricsTrackSpy).toHaveBeenCalled();
1328
1712
  expect(metricsTrackSpy.mock.calls[0][0]).toBe('Agent Contact Assign Failed');
@@ -1397,7 +1781,10 @@ describe('TaskManager', () => {
1397
1781
  };
1398
1782
  webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
1399
1783
  expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.AGENT_CONSULTING, consultingPayload.data);
1400
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONSULTING, taskManager.getTask(taskId));
1784
+ expect(taskEmitSpy).toHaveBeenCalledWith(
1785
+ TASK_EVENTS.TASK_CONSULTING,
1786
+ taskManager.getTask(taskId)
1787
+ );
1401
1788
  });
1402
1789
 
1403
1790
  it('should emit TASK_END event on AGENT_CONTACT_UNASSIGNED', () => {
@@ -1420,7 +1807,10 @@ describe('TaskManager', () => {
1420
1807
  },
1421
1808
  };
1422
1809
  webSocketManagerMock.emit('message', JSON.stringify(unassignedPayload));
1423
- expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.AGENT_CONTACT_UNASSIGNED, unassignedPayload.data);
1810
+ expect(taskEmitSpy).toHaveBeenCalledWith(
1811
+ CC_EVENTS.AGENT_CONTACT_UNASSIGNED,
1812
+ unassignedPayload.data
1813
+ );
1424
1814
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, taskManager.getTask(taskId));
1425
1815
  });
1426
1816
 
@@ -1429,12 +1819,12 @@ describe('TaskManager', () => {
1429
1819
  const chatPayload = {
1430
1820
  data: {
1431
1821
  ...initalPayload.data,
1432
- interaction: { mediaType: 'chat' },
1822
+ interaction: {mediaType: 'chat'},
1433
1823
  },
1434
1824
  };
1435
1825
 
1436
1826
  const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
1437
-
1827
+
1438
1828
  // Simulate receiving a chat task
1439
1829
  webSocketManagerMock.emit('message', JSON.stringify(chatPayload));
1440
1830
 
@@ -1451,12 +1841,12 @@ describe('TaskManager', () => {
1451
1841
  const emailPayload = {
1452
1842
  data: {
1453
1843
  ...initalPayload.data,
1454
- interaction: { mediaType: 'email' },
1844
+ interaction: {mediaType: 'email'},
1455
1845
  },
1456
1846
  };
1457
1847
 
1458
1848
  const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
1459
-
1849
+
1460
1850
  // Simulate receiving an email task
1461
1851
  webSocketManagerMock.emit('message', JSON.stringify(emailPayload));
1462
1852
 
@@ -1474,18 +1864,18 @@ describe('TaskManager', () => {
1474
1864
  data: {
1475
1865
  ...initalPayload.data,
1476
1866
  type: CC_EVENTS.AGENT_CONTACT_RESERVED,
1477
- interaction: { mediaType: 'chat' },
1867
+ interaction: {mediaType: 'chat'},
1478
1868
  },
1479
1869
  };
1480
-
1870
+
1481
1871
  const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
1482
1872
  webSocketManagerMock.emit('message', JSON.stringify(chatReservedPayload));
1483
-
1873
+
1484
1874
  expect(taskIncomingSpy).toHaveBeenCalledWith(
1485
1875
  TASK_EVENTS.TASK_INCOMING,
1486
1876
  taskManager.getTask(chatReservedPayload.data.interactionId)
1487
1877
  );
1488
-
1878
+
1489
1879
  // 2. Chat task is assigned
1490
1880
  const chatAssignedPayload = {
1491
1881
  data: {
@@ -1493,20 +1883,20 @@ describe('TaskManager', () => {
1493
1883
  type: CC_EVENTS.AGENT_CONTACT_ASSIGNED,
1494
1884
  },
1495
1885
  };
1496
-
1886
+
1497
1887
  const task = taskManager.getTask(chatReservedPayload.data.interactionId);
1498
1888
  const taskEmitSpy = jest.spyOn(task, 'emit');
1499
-
1889
+
1500
1890
  webSocketManagerMock.emit('message', JSON.stringify(chatAssignedPayload));
1501
-
1891
+
1502
1892
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, task);
1503
-
1893
+
1504
1894
  // 3. Chat task is ended with state 'new' to trigger cleanup
1505
1895
  const chatEndedPayload = {
1506
1896
  data: {
1507
1897
  ...chatReservedPayload.data,
1508
1898
  type: CC_EVENTS.CONTACT_ENDED,
1509
- interaction: { mediaType: 'chat', state: 'new' }, // Change to 'new' state
1899
+ interaction: {mediaType: 'chat', state: 'new'}, // Change to 'new' state
1510
1900
  },
1511
1901
  };
1512
1902
 
@@ -1523,40 +1913,46 @@ describe('TaskManager', () => {
1523
1913
  data: {
1524
1914
  ...initalPayload.data,
1525
1915
  interactionId: 'telephony-task-id',
1526
- interaction: { mediaType: 'telephony' },
1916
+ interaction: {mediaType: 'telephony'},
1527
1917
  },
1528
1918
  };
1529
-
1919
+
1530
1920
  const chatPayload = {
1531
1921
  data: {
1532
1922
  ...initalPayload.data,
1533
1923
  interactionId: 'chat-task-id',
1534
- interaction: { mediaType: 'chat' },
1924
+ interaction: {mediaType: 'chat'},
1535
1925
  },
1536
1926
  };
1537
-
1927
+
1538
1928
  const emailPayload = {
1539
1929
  data: {
1540
1930
  ...initalPayload.data,
1541
1931
  interactionId: 'email-task-id',
1542
- interaction: { mediaType: 'email' },
1932
+ interaction: {mediaType: 'email'},
1543
1933
  },
1544
1934
  };
1545
-
1935
+
1546
1936
  // Simulate receiving tasks of different types
1547
1937
  webSocketManagerMock.emit('message', JSON.stringify(telephonyPayload));
1548
1938
  webSocketManagerMock.emit('message', JSON.stringify(chatPayload));
1549
1939
  webSocketManagerMock.emit('message', JSON.stringify(emailPayload));
1550
-
1940
+
1551
1941
  // Verify all tasks are in the collection
1552
1942
  expect(taskManager.getAllTasks()).toHaveProperty(telephonyPayload.data.interactionId);
1553
1943
  expect(taskManager.getAllTasks()).toHaveProperty(chatPayload.data.interactionId);
1554
1944
  expect(taskManager.getAllTasks()).toHaveProperty(emailPayload.data.interactionId);
1555
-
1945
+
1556
1946
  // Verify the task media types are correctly set
1557
- expect(taskManager.getTask(telephonyPayload.data.interactionId).data.interaction.mediaType).toBe('telephony');
1558
- expect(taskManager.getTask(chatPayload.data.interactionId).data.interaction.mediaType).toBe('chat');
1559
- expect(taskManager.getTask(emailPayload.data.interactionId).data.interaction.mediaType).toBe('email');
1947
+ expect(
1948
+ taskManager.getTask(telephonyPayload.data.interactionId).data.interaction.mediaType
1949
+ ).toBe('telephony');
1950
+ expect(taskManager.getTask(chatPayload.data.interactionId).data.interaction.mediaType).toBe(
1951
+ 'chat'
1952
+ );
1953
+ expect(taskManager.getTask(emailPayload.data.interactionId).data.interaction.mediaType).toBe(
1954
+ 'email'
1955
+ );
1560
1956
  });
1561
1957
 
1562
1958
  it('should properly handle one task ending when multiple tasks are active', () => {
@@ -1565,87 +1961,87 @@ describe('TaskManager', () => {
1565
1961
  data: {
1566
1962
  ...initalPayload.data,
1567
1963
  interactionId: 'task-id-1',
1568
- interaction: { mediaType: 'telephony' },
1964
+ interaction: {mediaType: 'telephony'},
1569
1965
  },
1570
1966
  };
1571
-
1967
+
1572
1968
  const task2Payload = {
1573
1969
  data: {
1574
1970
  ...initalPayload.data,
1575
1971
  interactionId: 'task-id-2',
1576
- interaction: { mediaType: 'chat' },
1972
+ interaction: {mediaType: 'chat'},
1577
1973
  },
1578
1974
  };
1579
-
1975
+
1580
1976
  const task3Payload = {
1581
1977
  data: {
1582
1978
  ...initalPayload.data,
1583
1979
  interactionId: 'task-id-3',
1584
- interaction: { mediaType: 'email' },
1980
+ interaction: {mediaType: 'email'},
1585
1981
  },
1586
1982
  };
1587
-
1983
+
1588
1984
  // Initialize all tasks
1589
1985
  webSocketManagerMock.emit('message', JSON.stringify(task1Payload));
1590
1986
  webSocketManagerMock.emit('message', JSON.stringify(task2Payload));
1591
1987
  webSocketManagerMock.emit('message', JSON.stringify(task3Payload));
1592
-
1988
+
1593
1989
  // Verify all tasks are in the collection
1594
1990
  expect(taskManager.getAllTasks()).toHaveProperty(task1Payload.data.interactionId);
1595
1991
  expect(taskManager.getAllTasks()).toHaveProperty(task2Payload.data.interactionId);
1596
1992
  expect(taskManager.getAllTasks()).toHaveProperty(task3Payload.data.interactionId);
1597
-
1993
+
1598
1994
  // Create spies for all tasks
1599
1995
  const task1EmitSpy = jest.spyOn(taskManager.getTask(task1Payload.data.interactionId), 'emit');
1600
1996
  const task2EmitSpy = jest.spyOn(taskManager.getTask(task2Payload.data.interactionId), 'emit');
1601
1997
  const task3EmitSpy = jest.spyOn(taskManager.getTask(task3Payload.data.interactionId), 'emit');
1602
-
1998
+
1603
1999
  // Store reference to task2 before it gets removed
1604
2000
  const task2 = taskManager.getTask(task2Payload.data.interactionId);
1605
-
2001
+
1606
2002
  // End only the second task (chat task)
1607
2003
  const chatEndedPayload = {
1608
2004
  data: {
1609
2005
  ...task2Payload.data,
1610
2006
  type: CC_EVENTS.CONTACT_ENDED,
1611
- interaction: { mediaType: 'chat', state: 'new' }, // Using 'new' to trigger cleanup
2007
+ interaction: {mediaType: 'chat', state: 'new'}, // Using 'new' to trigger cleanup
1612
2008
  },
1613
2009
  };
1614
-
2010
+
1615
2011
  webSocketManagerMock.emit('message', JSON.stringify(chatEndedPayload));
1616
-
2012
+
1617
2013
  // Verify only task2 emitted TASK_END
1618
2014
  expect(task1EmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END);
1619
2015
  expect(task2EmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task2);
1620
2016
  expect(task3EmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END);
1621
-
2017
+
1622
2018
  // Verify task2 was removed from collection (since state was 'new')
1623
2019
  expect(taskManager.getTask(task2Payload.data.interactionId)).toBeUndefined();
1624
-
2020
+
1625
2021
  // Verify other tasks remain in the collection
1626
2022
  expect(taskManager.getTask(task1Payload.data.interactionId)).toBeDefined();
1627
2023
  expect(taskManager.getTask(task3Payload.data.interactionId)).toBeDefined();
1628
-
2024
+
1629
2025
  // Store reference to task3 before we end it
1630
2026
  const task3 = taskManager.getTask(task3Payload.data.interactionId);
1631
-
2027
+
1632
2028
  // Now end task3 with a state that doesn't trigger cleanup
1633
2029
  const emailEndedPayload = {
1634
2030
  data: {
1635
2031
  ...task3Payload.data,
1636
2032
  type: CC_EVENTS.CONTACT_ENDED,
1637
- interaction: { mediaType: 'email', state: 'connected' }, // Using 'connected' to NOT trigger cleanup
2033
+ interaction: {mediaType: 'email', state: 'connected'}, // Using 'connected' to NOT trigger cleanup
1638
2034
  },
1639
2035
  };
1640
-
2036
+
1641
2037
  webSocketManagerMock.emit('message', JSON.stringify(emailEndedPayload));
1642
-
2038
+
1643
2039
  // Verify task3 emitted TASK_END
1644
2040
  expect(task3EmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task3);
1645
-
2041
+
1646
2042
  // Verify task3 is still in collection (since state was 'connected')
1647
2043
  expect(taskManager.getTask(task3Payload.data.interactionId)).toBeDefined();
1648
-
2044
+
1649
2045
  // Verify task1 remains unaffected
1650
2046
  expect(task1EmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END);
1651
2047
  expect(taskManager.getTask(task1Payload.data.interactionId)).toBeDefined();
@@ -1654,13 +2050,13 @@ describe('TaskManager', () => {
1654
2050
  it('should emit TASK_END event on AGENT_VTEAM_TRANSFERRED event', () => {
1655
2051
  // First create a task by emitting the initial payload
1656
2052
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1657
-
2053
+
1658
2054
  // Get a reference to the task from taskCollection
1659
2055
  const task = taskManager.getTask(taskId);
1660
-
2056
+
1661
2057
  // Now spy on the task's emit method
1662
2058
  const taskEmitSpy = jest.spyOn(task, 'emit');
1663
-
2059
+
1664
2060
  const vteamTransferredPayload = {
1665
2061
  data: {
1666
2062
  type: CC_EVENTS.AGENT_VTEAM_TRANSFERRED,
@@ -1677,31 +2073,31 @@ describe('TaskManager', () => {
1677
2073
  queueMgr: initalPayload.data.queueMgr,
1678
2074
  },
1679
2075
  };
1680
-
2076
+
1681
2077
  // No need to explicitly set the task in the collection as it's already there
1682
2078
  // from the initial message processing
1683
-
2079
+
1684
2080
  webSocketManagerMock.emit('message', JSON.stringify(vteamTransferredPayload));
1685
-
2081
+
1686
2082
  // Check that task.emit was called with TASK_END event
1687
2083
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
1688
-
2084
+
1689
2085
  // The task should still exist in the collection based on current implementation
1690
2086
  expect(taskManager.getTask(taskId)).toBeDefined();
1691
2087
  });
1692
2088
 
1693
2089
  it('should update task data on AGENT_WRAPUP event', () => {
1694
2090
  const payload = {
1695
- data: {
1696
- type: CC_EVENTS.AGENT_WRAPUP,
1697
- interactionId: taskId,
1698
- wrapUpRequired: true,
1699
- },
2091
+ data: {
2092
+ type: CC_EVENTS.AGENT_WRAPUP,
2093
+ interactionId: taskId,
2094
+ wrapUpRequired: true,
2095
+ },
1700
2096
  };
1701
2097
  const task = taskManager.getTask(taskId);
1702
2098
  const updateSpy = jest.spyOn(task, 'updateTaskData').mockImplementation((data) => {
1703
- task.data = { ...(task.data || {}), ...(data || {}) };
1704
- return task;
2099
+ task.data = {...(task.data || {}), ...(data || {})};
2100
+ return task;
1705
2101
  });
1706
2102
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1707
2103
  expect(updateSpy).toHaveBeenCalledWith(payload.data);
@@ -1716,7 +2112,7 @@ describe('TaskManager', () => {
1716
2112
  data: {
1717
2113
  type: CC_EVENTS.AGENT_CONTACT_UNASSIGNED,
1718
2114
  agentId: initalPayload.data.agentId,
1719
- interaction: { mediaType: 'telephony' },
2115
+ interaction: {mediaType: 'telephony'},
1720
2116
  interactionId: initalPayload.data.interactionId,
1721
2117
  orgId: initalPayload.data.orgId,
1722
2118
  trackingId: initalPayload.data.trackingId,
@@ -1735,7 +2131,7 @@ describe('TaskManager', () => {
1735
2131
  data: {
1736
2132
  type: CC_EVENTS.AGENT_WRAPUP,
1737
2133
  interactionId: taskId,
1738
- interaction: { mediaType: 'telephony' },
2134
+ interaction: {mediaType: 'telephony'},
1739
2135
  },
1740
2136
  };
1741
2137
  webSocketManagerMock.emit('message', JSON.stringify(wrapupPayload));
@@ -1752,7 +2148,7 @@ describe('TaskManager', () => {
1752
2148
  data: {
1753
2149
  type: CC_EVENTS.AGENT_VTEAM_TRANSFERRED,
1754
2150
  agentId: initalPayload.data.agentId,
1755
- interaction: { mediaType: 'telephony' },
2151
+ interaction: {mediaType: 'telephony'},
1756
2152
  interactionId: initalPayload.data.interactionId,
1757
2153
  orgId: initalPayload.data.orgId,
1758
2154
  trackingId: initalPayload.data.trackingId,
@@ -1771,7 +2167,7 @@ describe('TaskManager', () => {
1771
2167
  data: {
1772
2168
  type: CC_EVENTS.AGENT_WRAPUP,
1773
2169
  interactionId: taskId,
1774
- interaction: { mediaType: 'telephony' },
2170
+ interaction: {mediaType: 'telephony'},
1775
2171
  },
1776
2172
  };
1777
2173
  webSocketManagerMock.emit('message', JSON.stringify(wrapupPayload));
@@ -1793,22 +2189,22 @@ describe('TaskManager', () => {
1793
2189
  expect(spy).toHaveBeenCalledWith(taskEvent, task);
1794
2190
  });
1795
2191
  });
1796
- });
2192
+ });
1797
2193
 
1798
2194
  describe('Conference event handling', () => {
1799
2195
  let task;
1800
2196
  const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
1801
-
2197
+
1802
2198
  beforeEach(() => {
1803
2199
  // Set the agentId on taskManager before tests run
1804
2200
  taskManager.setAgentId(agentId);
1805
-
2201
+
1806
2202
  task = {
1807
- data: { interactionId: taskId },
2203
+ data: {interactionId: taskId},
1808
2204
  emit: jest.fn(),
1809
2205
  updateTaskData: jest.fn().mockImplementation((updatedData) => {
1810
2206
  // Mock the updateTaskData method to actually update task.data
1811
- task.data = { ...task.data, ...updatedData };
2207
+ task.data = {...task.data, ...updatedData};
1812
2208
  return task;
1813
2209
  }),
1814
2210
  };
@@ -1870,8 +2266,8 @@ describe('TaskManager', () => {
1870
2266
  participantType: 'agent',
1871
2267
  interaction: {
1872
2268
  participants: {
1873
- [agentId]: { pType: 'Agent', hasLeft: false },
1874
- 'new-participant-123': { pType: 'Agent', hasLeft: false },
2269
+ [agentId]: {pType: 'Agent', hasLeft: false},
2270
+ 'new-participant-123': {pType: 'Agent', hasLeft: false},
1875
2271
  },
1876
2272
  media: {
1877
2273
  [taskId]: {
@@ -1898,10 +2294,10 @@ describe('TaskManager', () => {
1898
2294
  participantId: 'new-agent-789',
1899
2295
  interaction: {
1900
2296
  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 },
2297
+ [agentId]: {pType: 'Agent', hasLeft: false},
2298
+ 'agent-2': {pType: 'Agent', hasLeft: false},
2299
+ 'new-agent-789': {pType: 'Agent', hasLeft: false},
2300
+ 'customer-1': {pType: 'Customer', hasLeft: false},
1905
2301
  },
1906
2302
  media: {
1907
2303
  [taskId]: {
@@ -1914,12 +2310,12 @@ describe('TaskManager', () => {
1914
2310
  };
1915
2311
 
1916
2312
  const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
1917
-
2313
+
1918
2314
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1919
2315
 
1920
2316
  // Verify updateTaskData was called exactly once
1921
2317
  expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
1922
-
2318
+
1923
2319
  // Verify it was called with isConferenceInProgress already calculated
1924
2320
  expect(updateTaskDataSpy).toHaveBeenCalledWith(
1925
2321
  expect.objectContaining({
@@ -1927,7 +2323,7 @@ describe('TaskManager', () => {
1927
2323
  isConferenceInProgress: true, // 3 active agents
1928
2324
  })
1929
2325
  );
1930
-
2326
+
1931
2327
  expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
1932
2328
  });
1933
2329
 
@@ -1939,9 +2335,9 @@ describe('TaskManager', () => {
1939
2335
  interactionId: taskId,
1940
2336
  interaction: {
1941
2337
  participants: {
1942
- [agentId]: { pType: 'Agent', hasLeft: false },
1943
- 'agent-2': { pType: 'Agent', hasLeft: true }, // This agent left
1944
- 'customer-1': { pType: 'Customer', hasLeft: false },
2338
+ [agentId]: {pType: 'Agent', hasLeft: false},
2339
+ 'agent-2': {pType: 'Agent', hasLeft: true}, // This agent left
2340
+ 'customer-1': {pType: 'Customer', hasLeft: false},
1945
2341
  },
1946
2342
  media: {
1947
2343
  [taskId]: {
@@ -1954,19 +2350,19 @@ describe('TaskManager', () => {
1954
2350
  };
1955
2351
 
1956
2352
  const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
1957
-
2353
+
1958
2354
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1959
2355
 
1960
2356
  // Verify updateTaskData was called exactly once
1961
2357
  expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
1962
-
2358
+
1963
2359
  // Verify it was called with isConferenceInProgress already calculated
1964
2360
  expect(updateTaskDataSpy).toHaveBeenCalledWith(
1965
2361
  expect.objectContaining({
1966
2362
  isConferenceInProgress: false, // Only 1 active agent remains
1967
2363
  })
1968
2364
  );
1969
-
2365
+
1970
2366
  expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1971
2367
  });
1972
2368
 
@@ -2243,7 +2639,7 @@ describe('TaskManager', () => {
2243
2639
  it('should only update task for matching interactionId', () => {
2244
2640
  const otherTaskId = 'other-task-id';
2245
2641
  const otherTask = {
2246
- data: { interactionId: otherTaskId },
2642
+ data: {interactionId: otherTaskId},
2247
2643
  emit: jest.fn(),
2248
2644
  };
2249
2645
  taskManager.taskCollection[otherTaskId] = otherTask;
@@ -2261,16 +2657,328 @@ describe('TaskManager', () => {
2261
2657
  // Only the matching task should be updated
2262
2658
  expect(task.data.isConferencing).toBe(true);
2263
2659
  expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
2264
-
2660
+
2265
2661
  // Other task should not be affected
2266
2662
  expect(otherTask.data.isConferencing).toBeUndefined();
2267
2663
  expect(otherTask.emit).not.toHaveBeenCalled();
2268
2664
  });
2269
2665
  });
2270
2666
 
2271
- describe('CONTACT_MERGED event handling', () => {
2272
- let task;
2273
- let taskEmitSpy;
2667
+ describe('handleTaskCleanup - stage changes', () => {
2668
+ const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
2669
+
2670
+ beforeEach(() => {
2671
+ taskManager.setAgentId(agentId);
2672
+ });
2673
+
2674
+ it('should remove OUTDIAL task on CONTACT_ENDED when current agent is NOT in agentsPendingWrapUp', () => {
2675
+ const task = taskManager.getTask(taskId);
2676
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
2677
+ task.data = {
2678
+ ...task.data,
2679
+ ...newData,
2680
+ interaction: {
2681
+ ...task.data.interaction,
2682
+ outboundType: 'OUTDIAL',
2683
+ state: 'new',
2684
+ mediaType: 'telephony',
2685
+ },
2686
+ agentsPendingWrapUp: ['different-agent-123'], // Current agent not in the list
2687
+ };
2688
+ return task;
2689
+ });
2690
+ task.unregisterWebCallListeners = jest.fn();
2691
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2692
+
2693
+ const payload = {
2694
+ data: {
2695
+ type: CC_EVENTS.CONTACT_ENDED,
2696
+ interactionId: taskId,
2697
+ interaction: {
2698
+ outboundType: 'OUTDIAL',
2699
+ state: 'new',
2700
+ mediaType: 'telephony',
2701
+ },
2702
+ agentsPendingWrapUp: ['different-agent-123'], // Current agent not in the list
2703
+ },
2704
+ };
2705
+
2706
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2707
+
2708
+ expect(removeTaskSpy).toHaveBeenCalled();
2709
+ expect(taskManager.getTask(taskId)).toBeUndefined();
2710
+ });
2711
+
2712
+ it('should NOT remove OUTDIAL task on CONTACT_ENDED when current agent IS in agentsPendingWrapUp', () => {
2713
+ const task = taskManager.getTask(taskId);
2714
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
2715
+ task.data = {
2716
+ ...task.data,
2717
+ ...newData,
2718
+ interaction: {
2719
+ ...task.data.interaction,
2720
+ outboundType: 'OUTDIAL',
2721
+ state: 'new',
2722
+ mediaType: 'telephony',
2723
+ },
2724
+ agentsPendingWrapUp: [agentId, 'other-agent-456'], // Current agent IS in the list
2725
+ };
2726
+ return task;
2727
+ });
2728
+ task.unregisterWebCallListeners = jest.fn();
2729
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2730
+
2731
+ const payload = {
2732
+ data: {
2733
+ type: CC_EVENTS.CONTACT_ENDED,
2734
+ interactionId: taskId,
2735
+ interaction: {
2736
+ outboundType: 'OUTDIAL',
2737
+ state: 'new',
2738
+ mediaType: 'telephony',
2739
+ },
2740
+ agentsPendingWrapUp: [agentId, 'other-agent-456'], // Current agent IS in the list
2741
+ },
2742
+ };
2743
+
2744
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2745
+
2746
+ expect(removeTaskSpy).not.toHaveBeenCalled();
2747
+ expect(taskManager.getTask(taskId)).toBeDefined();
2748
+ });
2749
+
2750
+ it('should remove OUTDIAL task when needsWrapUp is false and task is outdial', () => {
2751
+ const task = taskManager.getTask(taskId);
2752
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
2753
+ task.data = {
2754
+ ...task.data,
2755
+ ...newData,
2756
+ interaction: {
2757
+ ...task.data.interaction,
2758
+ outboundType: 'OUTDIAL',
2759
+ state: 'WRAPUP', // Not 'new' state
2760
+ mediaType: 'telephony',
2761
+ },
2762
+ agentsPendingWrapUp: [], // No agents pending wrap-up
2763
+ };
2764
+ return task;
2765
+ });
2766
+ task.unregisterWebCallListeners = jest.fn();
2767
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2768
+
2769
+ const payload = {
2770
+ data: {
2771
+ type: CC_EVENTS.CONTACT_ENDED,
2772
+ interactionId: taskId,
2773
+ interaction: {
2774
+ outboundType: 'OUTDIAL',
2775
+ state: 'WRAPUP', // Not 'new' state
2776
+ mediaType: 'telephony',
2777
+ },
2778
+ agentsPendingWrapUp: [], // No agents pending wrap-up
2779
+ },
2780
+ };
2781
+
2782
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2783
+
2784
+ expect(removeTaskSpy).toHaveBeenCalled();
2785
+ expect(taskManager.getTask(taskId)).toBeUndefined();
2786
+ });
2787
+
2788
+ it('should remove OUTDIAL task when needsWrapUp is false (agentsPendingWrapUp is undefined)', () => {
2789
+ const task = taskManager.getTask(taskId);
2790
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
2791
+ task.data = {
2792
+ ...task.data,
2793
+ ...newData,
2794
+ interaction: {
2795
+ ...task.data.interaction,
2796
+ outboundType: 'OUTDIAL',
2797
+ state: 'WRAPUP',
2798
+ mediaType: 'telephony',
2799
+ },
2800
+ agentsPendingWrapUp: undefined, // No agentsPendingWrapUp field
2801
+ };
2802
+ return task;
2803
+ });
2804
+ task.unregisterWebCallListeners = jest.fn();
2805
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2806
+
2807
+ const payload = {
2808
+ data: {
2809
+ type: CC_EVENTS.CONTACT_ENDED,
2810
+ interactionId: taskId,
2811
+ interaction: {
2812
+ outboundType: 'OUTDIAL',
2813
+ state: 'WRAPUP',
2814
+ mediaType: 'telephony',
2815
+ },
2816
+ // agentsPendingWrapUp not included
2817
+ },
2818
+ };
2819
+
2820
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2821
+
2822
+ expect(removeTaskSpy).toHaveBeenCalled();
2823
+ expect(taskManager.getTask(taskId)).toBeUndefined();
2824
+ });
2825
+
2826
+ it('should NOT remove OUTDIAL task when needsWrapUp is true (current agent in agentsPendingWrapUp) even if state is WRAPUP', () => {
2827
+ const task = taskManager.getTask(taskId);
2828
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
2829
+ task.data = {
2830
+ ...task.data,
2831
+ ...newData,
2832
+ interaction: {
2833
+ ...task.data.interaction,
2834
+ outboundType: 'OUTDIAL',
2835
+ state: 'WRAPUP',
2836
+ mediaType: 'telephony',
2837
+ },
2838
+ agentsPendingWrapUp: [agentId], // Current agent needs wrap-up
2839
+ };
2840
+ return task;
2841
+ });
2842
+ task.unregisterWebCallListeners = jest.fn();
2843
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2844
+
2845
+ const payload = {
2846
+ data: {
2847
+ type: CC_EVENTS.CONTACT_ENDED,
2848
+ interactionId: taskId,
2849
+ interaction: {
2850
+ outboundType: 'OUTDIAL',
2851
+ state: 'WRAPUP',
2852
+ mediaType: 'telephony',
2853
+ },
2854
+ agentsPendingWrapUp: [agentId], // Current agent needs wrap-up
2855
+ },
2856
+ };
2857
+
2858
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2859
+
2860
+ expect(removeTaskSpy).not.toHaveBeenCalled();
2861
+ expect(taskManager.getTask(taskId)).toBeDefined();
2862
+ });
2863
+
2864
+ it('should remove non-OUTDIAL task when state is new regardless of agentsPendingWrapUp', () => {
2865
+ const task = taskManager.getTask(taskId);
2866
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
2867
+ task.data = {
2868
+ ...task.data,
2869
+ ...newData,
2870
+ interaction: {
2871
+ ...task.data.interaction,
2872
+ outboundType: 'PREVIEW', // Not OUTDIAL
2873
+ state: 'new',
2874
+ mediaType: 'telephony',
2875
+ },
2876
+ agentsPendingWrapUp: [agentId],
2877
+ };
2878
+ return task;
2879
+ });
2880
+ task.unregisterWebCallListeners = jest.fn();
2881
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2882
+
2883
+ const payload = {
2884
+ data: {
2885
+ type: CC_EVENTS.CONTACT_ENDED,
2886
+ interactionId: taskId,
2887
+ interaction: {
2888
+ outboundType: 'PREVIEW',
2889
+ state: 'new',
2890
+ mediaType: 'telephony',
2891
+ },
2892
+ agentsPendingWrapUp: [agentId],
2893
+ },
2894
+ };
2895
+
2896
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2897
+
2898
+ expect(removeTaskSpy).toHaveBeenCalled();
2899
+ expect(taskManager.getTask(taskId)).toBeUndefined();
2900
+ });
2901
+
2902
+ it('should handle agentsPendingWrapUp with multiple agents correctly - remove if current agent not in list', () => {
2903
+ const task = taskManager.getTask(taskId);
2904
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
2905
+ task.data = {
2906
+ ...task.data,
2907
+ ...newData,
2908
+ interaction: {
2909
+ ...task.data.interaction,
2910
+ outboundType: 'OUTDIAL',
2911
+ state: 'new',
2912
+ mediaType: 'telephony',
2913
+ },
2914
+ agentsPendingWrapUp: ['agent-1', 'agent-2', 'agent-3'], // Current agent not in the list
2915
+ };
2916
+ return task;
2917
+ });
2918
+ task.unregisterWebCallListeners = jest.fn();
2919
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2920
+
2921
+ const payload = {
2922
+ data: {
2923
+ type: CC_EVENTS.CONTACT_ENDED,
2924
+ interactionId: taskId,
2925
+ interaction: {
2926
+ outboundType: 'OUTDIAL',
2927
+ state: 'new',
2928
+ mediaType: 'telephony',
2929
+ },
2930
+ agentsPendingWrapUp: ['agent-1', 'agent-2', 'agent-3'],
2931
+ },
2932
+ };
2933
+
2934
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2935
+
2936
+ expect(removeTaskSpy).toHaveBeenCalled();
2937
+ expect(taskManager.getTask(taskId)).toBeUndefined();
2938
+ });
2939
+
2940
+ it('should handle agentsPendingWrapUp with multiple agents correctly - keep if current agent is in list', () => {
2941
+ const task = taskManager.getTask(taskId);
2942
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
2943
+ task.data = {
2944
+ ...task.data,
2945
+ ...newData,
2946
+ interaction: {
2947
+ ...task.data.interaction,
2948
+ outboundType: 'OUTDIAL',
2949
+ state: 'new',
2950
+ mediaType: 'telephony',
2951
+ },
2952
+ agentsPendingWrapUp: ['agent-1', agentId, 'agent-3'], // Current agent IS in the list
2953
+ };
2954
+ return task;
2955
+ });
2956
+ task.unregisterWebCallListeners = jest.fn();
2957
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2958
+
2959
+ const payload = {
2960
+ data: {
2961
+ type: CC_EVENTS.CONTACT_ENDED,
2962
+ interactionId: taskId,
2963
+ interaction: {
2964
+ outboundType: 'OUTDIAL',
2965
+ state: 'new',
2966
+ mediaType: 'telephony',
2967
+ },
2968
+ agentsPendingWrapUp: ['agent-1', agentId, 'agent-3'],
2969
+ },
2970
+ };
2971
+
2972
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2973
+
2974
+ expect(removeTaskSpy).not.toHaveBeenCalled();
2975
+ expect(taskManager.getTask(taskId)).toBeDefined();
2976
+ });
2977
+ });
2978
+
2979
+ describe('CONTACT_MERGED event handling', () => {
2980
+ let task;
2981
+ let taskEmitSpy;
2274
2982
  let managerEmitSpy;
2275
2983
 
2276
2984
  beforeEach(() => {
@@ -2339,7 +3047,7 @@ describe('TaskManager', () => {
2339
3047
  it('should remove child task when childInteractionId is present in CONTACT_MERGED', () => {
2340
3048
  const childTaskId = 'child-task-id';
2341
3049
  const parentTaskId = 'parent-task-id';
2342
-
3050
+
2343
3051
  // Create child task
2344
3052
  const childPayload = {
2345
3053
  data: {
@@ -2350,7 +3058,7 @@ describe('TaskManager', () => {
2350
3058
  },
2351
3059
  };
2352
3060
  webSocketManagerMock.emit('message', JSON.stringify(childPayload));
2353
-
3061
+
2354
3062
  // Verify child task exists
2355
3063
  expect(taskManager.getTask(childTaskId)).toBeDefined();
2356
3064
 
@@ -2383,10 +3091,10 @@ describe('TaskManager', () => {
2383
3091
 
2384
3092
  // Verify child task was removed
2385
3093
  expect(taskManager.getTask(childTaskId)).toBeUndefined();
2386
-
3094
+
2387
3095
  // Verify parent task still exists
2388
3096
  expect(taskManager.getTask(parentTaskId)).toBeDefined();
2389
-
3097
+
2390
3098
  // Verify TASK_MERGED event was emitted
2391
3099
  expect(managerEmitSpy).toHaveBeenCalledWith(
2392
3100
  TASK_EVENTS.TASK_MERGED,
@@ -2444,7 +3152,7 @@ describe('TaskManager', () => {
2444
3152
  },
2445
3153
  };
2446
3154
  webSocketManagerMock.emit('message', JSON.stringify(otherPayload));
2447
-
3155
+
2448
3156
  const otherTask = taskManager.getTask(otherTaskId);
2449
3157
  const otherTaskEmitSpy = jest.spyOn(otherTask, 'emit');
2450
3158
 
@@ -2466,7 +3174,7 @@ describe('TaskManager', () => {
2466
3174
  // Verify other task was not affected
2467
3175
  expect(otherTaskEmitSpy).not.toHaveBeenCalled();
2468
3176
  expect(otherTask.data.interaction.mediaType).toBe('chat');
2469
-
3177
+
2470
3178
  // Verify original task was updated
2471
3179
  expect(managerEmitSpy).toHaveBeenCalledWith(
2472
3180
  TASK_EVENTS.TASK_MERGED,
@@ -2479,5 +3187,165 @@ describe('TaskManager', () => {
2479
3187
  });
2480
3188
  });
2481
3189
 
2482
- });
3190
+ describe('Campaign Preview Reservation', () => {
3191
+ it('should create a task and emit TASK_CAMPAIGN_PREVIEW_RESERVATION when AgentOfferCampaignReservation is received', () => {
3192
+ const campaignPayload = {
3193
+ data: {
3194
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3195
+ interactionId: 'campaign-interaction-123',
3196
+ agentId: taskDataMock.agentId,
3197
+ orgId: taskDataMock.orgId,
3198
+ trackingId: 'campaign-tracking-456',
3199
+ interaction: {
3200
+ mediaType: 'telephony',
3201
+ callProcessingDetails: {
3202
+ campaignId: 'campaign-789',
3203
+ },
3204
+ },
3205
+ },
3206
+ };
3207
+
3208
+ const managerEmitSpy = jest.spyOn(taskManager, 'emit');
3209
+
3210
+ webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
2483
3211
 
3212
+ // Should emit with a task object (not raw data)
3213
+ expect(managerEmitSpy).toHaveBeenCalledWith(
3214
+ TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION,
3215
+ expect.objectContaining({
3216
+ data: expect.objectContaining({
3217
+ interactionId: 'campaign-interaction-123',
3218
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3219
+ wrapUpRequired: false,
3220
+ isAutoAnswering: false,
3221
+ }),
3222
+ })
3223
+ );
3224
+
3225
+ // Task should be in the collection so subsequent events (e.g. AGENT_CONTACT_ASSIGNED) can find it
3226
+ expect(taskManager['taskCollection']['campaign-interaction-123']).toBeDefined();
3227
+ });
3228
+
3229
+ it('should not emit TASK_INCOMING for campaign preview reservation when incoming WebRTC call arrives', () => {
3230
+ const campaignPayload = {
3231
+ data: {
3232
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3233
+ interactionId: 'campaign-interaction-123',
3234
+ agentId: taskDataMock.agentId,
3235
+ orgId: taskDataMock.orgId,
3236
+ trackingId: 'campaign-tracking-456',
3237
+ interaction: {
3238
+ mediaType: 'telephony',
3239
+ callProcessingDetails: {
3240
+ campaignId: 'campaign-789',
3241
+ },
3242
+ },
3243
+ },
3244
+ };
3245
+
3246
+ // Remove the default task so only the campaign preview task is in the collection
3247
+ delete taskManager['taskCollection'][taskId];
3248
+
3249
+ // Create campaign preview task via the reservation event
3250
+ webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
3251
+
3252
+ const managerEmitSpy = jest.spyOn(taskManager, 'emit');
3253
+
3254
+ // Simulate an incoming WebRTC call
3255
+ const incomingCallCb = onSpy.mock.calls[0][1];
3256
+ incomingCallCb(mockCall);
3257
+
3258
+ // TASK_INCOMING should NOT be emitted because the only telephony task is a campaign preview
3259
+ expect(managerEmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, expect.anything());
3260
+ });
3261
+
3262
+ it('should update existing task when AgentOfferCampaignReservation is received for known interactionId', () => {
3263
+ const campaignPayload = {
3264
+ data: {
3265
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3266
+ interactionId: 'campaign-interaction-123',
3267
+ agentId: taskDataMock.agentId,
3268
+ orgId: taskDataMock.orgId,
3269
+ trackingId: 'campaign-tracking-456',
3270
+ interaction: {
3271
+ mediaType: 'telephony',
3272
+ callProcessingDetails: {
3273
+ campaignId: 'campaign-789',
3274
+ },
3275
+ },
3276
+ },
3277
+ };
3278
+
3279
+ // Send the first reservation to create the task
3280
+ webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
3281
+
3282
+ const managerEmitSpy = jest.spyOn(taskManager, 'emit');
3283
+
3284
+ // Send a second reservation for the same interactionId
3285
+ webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
3286
+
3287
+ expect(managerEmitSpy).toHaveBeenCalledWith(
3288
+ TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION,
3289
+ expect.objectContaining({
3290
+ data: expect.objectContaining({
3291
+ interactionId: 'campaign-interaction-123',
3292
+ }),
3293
+ })
3294
+ );
3295
+ });
3296
+
3297
+ it('should update task data but NOT remove task when CampaignContactUpdated is received', () => {
3298
+ const campaignInteractionId = 'campaign-interaction-123';
3299
+
3300
+ // First create a campaign preview task
3301
+ const reservationPayload = {
3302
+ data: {
3303
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3304
+ interactionId: campaignInteractionId,
3305
+ agentId: taskDataMock.agentId,
3306
+ orgId: taskDataMock.orgId,
3307
+ trackingId: 'campaign-tracking-456',
3308
+ interaction: {
3309
+ mediaType: 'telephony',
3310
+ callProcessingDetails: {
3311
+ campaignId: 'campaign-789',
3312
+ },
3313
+ },
3314
+ },
3315
+ };
3316
+
3317
+ webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
3318
+
3319
+ // Verify task exists in collection
3320
+ const task = taskManager['taskCollection'][campaignInteractionId];
3321
+ expect(task).toBeDefined();
3322
+
3323
+ const taskEmitSpy = jest.spyOn(task, 'emit');
3324
+
3325
+ // Now send CampaignContactUpdated
3326
+ const campaignContactUpdatedPayload = {
3327
+ data: {
3328
+ type: CC_EVENTS.CAMPAIGN_CONTACT_UPDATED,
3329
+ interactionId: campaignInteractionId,
3330
+ agentId: taskDataMock.agentId,
3331
+ orgId: taskDataMock.orgId,
3332
+ interaction: {
3333
+ mediaType: 'telephony',
3334
+ state: 'new',
3335
+ callProcessingDetails: {
3336
+ campaignId: 'campaign-789',
3337
+ },
3338
+ },
3339
+ },
3340
+ };
3341
+
3342
+ webSocketManagerMock.emit('message', JSON.stringify(campaignContactUpdatedPayload));
3343
+
3344
+ // Task should still exist in collection (not removed — non-terminal event)
3345
+ expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
3346
+
3347
+ // TASK_END should NOT have been emitted
3348
+ expect(taskEmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.anything());
3349
+ });
3350
+ });
3351
+ });