@webex/contact-center 3.12.0-task-refactor.10 → 3.12.0-task-refactor.12

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 (110) hide show
  1. package/dist/cc.js +345 -82
  2. package/dist/cc.js.map +1 -1
  3. package/dist/config.js +6 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/constants.js +3 -0
  6. package/dist/constants.js.map +1 -1
  7. package/dist/metrics/behavioral-events.js +39 -0
  8. package/dist/metrics/behavioral-events.js.map +1 -1
  9. package/dist/metrics/constants.js +8 -1
  10. package/dist/metrics/constants.js.map +1 -1
  11. package/dist/services/ApiAiAssistant.js +3 -0
  12. package/dist/services/ApiAiAssistant.js.map +1 -1
  13. package/dist/services/config/Util.js +1 -1
  14. package/dist/services/config/Util.js.map +1 -1
  15. package/dist/services/config/types.js +10 -0
  16. package/dist/services/config/types.js.map +1 -1
  17. package/dist/services/constants.js +1 -0
  18. package/dist/services/constants.js.map +1 -1
  19. package/dist/services/core/Err.js.map +1 -1
  20. package/dist/services/core/Utils.js +57 -6
  21. package/dist/services/core/Utils.js.map +1 -1
  22. package/dist/services/core/aqm-reqs.js +92 -17
  23. package/dist/services/core/aqm-reqs.js.map +1 -1
  24. package/dist/services/core/websocket/WebSocketManager.js +24 -3
  25. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  26. package/dist/services/index.js +1 -0
  27. package/dist/services/index.js.map +1 -1
  28. package/dist/services/task/Task.js +16 -4
  29. package/dist/services/task/Task.js.map +1 -1
  30. package/dist/services/task/TaskManager.js +158 -6
  31. package/dist/services/task/TaskManager.js.map +1 -1
  32. package/dist/services/task/TaskUtils.js +39 -11
  33. package/dist/services/task/TaskUtils.js.map +1 -1
  34. package/dist/services/task/constants.js +7 -1
  35. package/dist/services/task/constants.js.map +1 -1
  36. package/dist/services/task/dialer.js +129 -0
  37. package/dist/services/task/dialer.js.map +1 -1
  38. package/dist/services/task/state-machine/TaskStateMachine.js +30 -3
  39. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  40. package/dist/services/task/state-machine/actions.js +4 -0
  41. package/dist/services/task/state-machine/actions.js.map +1 -1
  42. package/dist/services/task/state-machine/constants.js +3 -0
  43. package/dist/services/task/state-machine/constants.js.map +1 -1
  44. package/dist/services/task/state-machine/guards.js +23 -0
  45. package/dist/services/task/state-machine/guards.js.map +1 -1
  46. package/dist/services/task/state-machine/types.js.map +1 -1
  47. package/dist/services/task/state-machine/uiControlsComputer.js +1 -1
  48. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  49. package/dist/services/task/types.js +69 -0
  50. package/dist/services/task/types.js.map +1 -1
  51. package/dist/types/cc.d.ts +93 -2
  52. package/dist/types/config.d.ts +6 -0
  53. package/dist/types/constants.d.ts +3 -0
  54. package/dist/types/metrics/constants.d.ts +6 -0
  55. package/dist/types/services/config/types.d.ts +22 -1
  56. package/dist/types/services/core/Err.d.ts +6 -0
  57. package/dist/types/services/core/Utils.d.ts +20 -2
  58. package/dist/types/services/core/aqm-reqs.d.ts +49 -0
  59. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
  60. package/dist/types/services/core/websocket/connection-service.d.ts +1 -0
  61. package/dist/types/services/task/Task.d.ts +1 -0
  62. package/dist/types/services/task/TaskUtils.d.ts +13 -0
  63. package/dist/types/services/task/constants.d.ts +6 -0
  64. package/dist/types/services/task/dialer.d.ts +45 -0
  65. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +36 -8
  66. package/dist/types/services/task/state-machine/constants.d.ts +3 -0
  67. package/dist/types/services/task/state-machine/guards.d.ts +2 -0
  68. package/dist/types/services/task/state-machine/types.d.ts +11 -0
  69. package/dist/types/services/task/types.d.ts +85 -1
  70. package/dist/types/types.d.ts +2 -0
  71. package/dist/types.js.map +1 -1
  72. package/dist/webex.js +1 -1
  73. package/package.json +2 -2
  74. package/src/cc.ts +452 -102
  75. package/src/config.ts +6 -0
  76. package/src/constants.ts +3 -0
  77. package/src/metrics/behavioral-events.ts +42 -0
  78. package/src/metrics/constants.ts +9 -1
  79. package/src/services/ApiAiAssistant.ts +3 -0
  80. package/src/services/config/Util.ts +1 -1
  81. package/src/services/config/types.ts +12 -1
  82. package/src/services/constants.ts +1 -0
  83. package/src/services/core/Err.ts +3 -0
  84. package/src/services/core/Utils.ts +68 -5
  85. package/src/services/core/aqm-reqs.ts +100 -22
  86. package/src/services/core/websocket/WebSocketManager.ts +23 -2
  87. package/src/services/index.ts +1 -0
  88. package/src/services/task/Task.ts +22 -4
  89. package/src/services/task/TaskManager.ts +175 -7
  90. package/src/services/task/TaskUtils.ts +33 -1
  91. package/src/services/task/constants.ts +6 -0
  92. package/src/services/task/dialer.ts +136 -1
  93. package/src/services/task/state-machine/TaskStateMachine.ts +37 -5
  94. package/src/services/task/state-machine/actions.ts +4 -0
  95. package/src/services/task/state-machine/constants.ts +3 -0
  96. package/src/services/task/state-machine/guards.ts +30 -0
  97. package/src/services/task/state-machine/types.ts +14 -1
  98. package/src/services/task/state-machine/uiControlsComputer.ts +1 -1
  99. package/src/services/task/types.ts +91 -0
  100. package/src/types.ts +6 -1
  101. package/test/unit/spec/cc.ts +432 -26
  102. package/test/unit/spec/services/config/index.ts +30 -2
  103. package/test/unit/spec/services/core/Utils.ts +367 -1
  104. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +90 -1
  105. package/test/unit/spec/services/core/websocket/connection-service.ts +4 -5
  106. package/test/unit/spec/services/task/TaskManager.ts +2334 -145
  107. package/test/unit/spec/services/task/dialer.ts +372 -96
  108. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +47 -0
  109. package/umd/contact-center.min.js +2 -2
  110. package/umd/contact-center.min.js.map +1 -1
@@ -92,6 +92,10 @@ describe('TaskManager', () => {
92
92
  }
93
93
  }
94
94
 
95
+ if ([TaskEvent.ASSIGN, TaskEvent.CONSULTING_ACTIVE].includes(event.type as TaskEvent)) {
96
+ task.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, task);
97
+ }
98
+
95
99
  // Auto-answer is now handled at the Task layer (triggered by state machine actions)
96
100
  if (
97
101
  [TaskEvent.TASK_OFFERED, TaskEvent.OFFER_CONSULT].includes(event.type as TaskEvent) &&
@@ -160,7 +164,19 @@ describe('TaskManager', () => {
160
164
  beforeEach(() => {
161
165
  contactMock = contact;
162
166
  webSocketManagerMock = new EventEmitter();
167
+ <<<<<<< HEAD
163
168
  rtdWebSocketManagerMock = new EventEmitter();
169
+ =======
170
+ mockApiAIAssistant = {
171
+ sendEvent: jest.fn().mockResolvedValue({}),
172
+ setAIFeatureFlags: jest.fn(),
173
+ aiFeature: {
174
+ realtimeTranscripts: {
175
+ enable: true,
176
+ },
177
+ },
178
+ };
179
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
164
180
 
165
181
  webex = {
166
182
  logger: {
@@ -190,6 +206,7 @@ describe('TaskManager', () => {
190
206
  onSpy = jest.spyOn(webCallingService, 'on');
191
207
  offSpy = jest.spyOn(webCallingService, 'off');
192
208
 
209
+ <<<<<<< HEAD
193
210
  mockApiAIAssistant = {
194
211
  sendEvent: jest.fn().mockResolvedValue({}),
195
212
  };
@@ -203,6 +220,26 @@ describe('TaskManager', () => {
203
220
  );
204
221
  taskManager.taskCollection[taskId] = createMockTask(taskDataMock);
205
222
  (taskManager as any).setupTaskListeners?.(taskManager.taskCollection[taskId]);
223
+ =======
224
+ taskManager = new TaskManager(
225
+ mockApiAIAssistant,
226
+ contactMock,
227
+ webCallingService,
228
+ webSocketManagerMock
229
+ );
230
+ const taskMock = {
231
+ emit: jest.fn(),
232
+ accept: jest.fn(),
233
+ decline: jest.fn(),
234
+ updateTaskData: jest.fn().mockImplementation((updatedData) => {
235
+ taskMock.data = {...taskMock.data, ...updatedData};
236
+ return taskMock;
237
+ }),
238
+ data: taskDataMock,
239
+ };
240
+ taskManager.taskCollection[taskId] = taskMock;
241
+ taskManager.agentId = 'test-agent-id';
242
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
206
243
  taskManager.call = mockCall;
207
244
  taskManager.setAgentId('test-agent-id');
208
245
 
@@ -229,8 +266,15 @@ describe('TaskManager', () => {
229
266
 
230
267
  incomingCallCb(mockCall);
231
268
 
269
+ <<<<<<< HEAD
232
270
  expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(taskId));
233
271
  taskManager.off(TASK_EVENTS.TASK_INCOMING, incomingHandler);
272
+ =======
273
+ expect(taskEmitSpy).toHaveBeenCalledWith(
274
+ TASK_EVENTS.TASK_INCOMING,
275
+ taskManager.getTask(taskId)
276
+ );
277
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
234
278
  });
235
279
 
236
280
  it('should re-emit task related events', () => {
@@ -428,6 +472,106 @@ describe('TaskManager', () => {
428
472
  expect(existingTaskEmitSpy).not.toHaveBeenCalled();
429
473
  });
430
474
 
475
+ it('should invoke sendEvent for configured start/stop backend events', () => {
476
+ const message = (type: CC_EVENTS) =>
477
+ JSON.stringify({
478
+ data: {
479
+ ...taskDataMock,
480
+ taskId,
481
+ type,
482
+ },
483
+ });
484
+
485
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONTACT_ASSIGNED));
486
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
487
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_CONFERENCED));
488
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_ENDED));
489
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
490
+ webSocketManagerMock.emit('message', message(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE));
491
+
492
+ expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledTimes(6);
493
+ expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledWith(
494
+ 'test-agent-id',
495
+ taskId,
496
+ 'CUSTOM_EVENT',
497
+ 'GET_TRANSCRIPTS',
498
+ 'START'
499
+ );
500
+ expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledWith(
501
+ 'test-agent-id',
502
+ taskId,
503
+ 'CUSTOM_EVENT',
504
+ 'GET_TRANSCRIPTS',
505
+ 'STOP'
506
+ );
507
+ });
508
+
509
+ it('should not invoke sendEvent for transcript events when realtime transcript feature is disabled', () => {
510
+ mockApiAIAssistant.aiFeature = {
511
+ realtimeTranscripts: {
512
+ enable: false,
513
+ },
514
+ };
515
+ mockApiAIAssistant.setAIFeatureFlags(mockApiAIAssistant.aiFeature);
516
+
517
+ const message = (type: CC_EVENTS) =>
518
+ JSON.stringify({
519
+ data: {
520
+ ...taskDataMock,
521
+ taskId,
522
+ type,
523
+ },
524
+ });
525
+
526
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONTACT_ASSIGNED));
527
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
528
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_CONFERENCED));
529
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_ENDED));
530
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
531
+ webSocketManagerMock.emit('message', message(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE));
532
+
533
+ expect(mockApiAIAssistant.sendEvent).not.toHaveBeenCalled();
534
+ });
535
+
536
+ it('should emit REAL_TIME_TRANSCRIPTION from task object', () => {
537
+ const task = taskManager.getTask(taskId);
538
+ const taskEmitSpy = jest.spyOn(task, 'emit');
539
+ const realtimePayload = {
540
+ data: {
541
+ ...taskDataMock,
542
+ type: CC_EVENTS.REAL_TIME_TRANSCRIPTION,
543
+ data: {
544
+ content: 'hello from transcript',
545
+ },
546
+ },
547
+ };
548
+
549
+ webSocketManagerMock.emit('message', JSON.stringify(realtimePayload));
550
+
551
+ expect(taskEmitSpy).toHaveBeenCalledWith(
552
+ CC_EVENTS.REAL_TIME_TRANSCRIPTION,
553
+ realtimePayload.data
554
+ );
555
+ });
556
+
557
+ it('should emit REAL_TIME_TRANSCRIPTION from RTD websocket payload on task object', () => {
558
+ const task = taskManager.getTask(taskId);
559
+ const taskEmitSpy = jest.spyOn(task, 'emit');
560
+ const realtimePayload = {
561
+ data: {
562
+ notifType: CC_EVENTS.REAL_TIME_TRANSCRIPTION,
563
+ data: {
564
+ conversationId: taskId,
565
+ content: 'hello from rtd websocket',
566
+ },
567
+ },
568
+ };
569
+
570
+ taskManager.handleRealtimeWebsocketEvent(JSON.stringify(realtimePayload));
571
+
572
+ expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.REAL_TIME_TRANSCRIPTION, realtimePayload.data);
573
+ });
574
+
431
575
  it('should not re-emit agent related events', () => {
432
576
  const dummyPayload = {
433
577
  data: {
@@ -494,6 +638,7 @@ describe('TaskManager', () => {
494
638
  taskManager.getTask(payload.data.interactionId),
495
639
  'emit'
496
640
  );
641
+ const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
497
642
 
498
643
  webSocketManagerMock.emit('message', JSON.stringify(assignedPayload));
499
644
 
@@ -501,6 +646,10 @@ describe('TaskManager', () => {
501
646
  TASK_EVENTS.TASK_ASSIGNED,
502
647
  taskManager.getTask(taskId)
503
648
  );
649
+ expect(taskManagerEmitSpy).toHaveBeenCalledWith(
650
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
651
+ taskManager.getTask(taskId)
652
+ );
504
653
  });
505
654
 
506
655
  it('should handle WebSocket message for AGENT_CONTACT_RESERVED and emit task:incoming for extension case', () => {
@@ -664,6 +813,7 @@ describe('TaskManager', () => {
664
813
  it('test call listeners being switched off on call end', () => {
665
814
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
666
815
 
816
+ <<<<<<< HEAD
667
817
  const webrtcTask = new WebRTC(contactMock, webCallingService, taskDataMock, {
668
818
  isEndTaskEnabled: true,
669
819
  isEndConsultEnabled: true,
@@ -686,6 +836,13 @@ describe('TaskManager', () => {
686
836
  });
687
837
 
688
838
  const webCallListenerSpy = jest.spyOn(task, 'unregisterWebCallListeners');
839
+ =======
840
+ const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
841
+ const webCallListenerSpy = jest.spyOn(
842
+ taskManager.getTask(taskId),
843
+ 'unregisterWebCallListeners'
844
+ );
845
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
689
846
  const callOffSpy = jest.spyOn(mockCall, 'off');
690
847
  const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
691
848
  const payload = {
@@ -718,12 +875,18 @@ describe('TaskManager', () => {
718
875
  webSocketManagerMock.emit('message', JSON.stringify(hydratePayload));
719
876
 
720
877
  taskManager.getTask(taskId).data = payload.data;
878
+ <<<<<<< HEAD
721
879
  webSocketManagerMock.emit('message', JSON.stringify(payload));
722
880
  const stateMachineEvent = expectLastStateMachineEvent(
723
881
  sendStateMachineEventSpy,
724
882
  TaskEvent.CONTACT_ENDED
725
883
  );
726
884
  expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(false);
885
+ =======
886
+ const task = taskManager.getTask(taskId);
887
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
888
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
889
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
727
890
  expect(webCallListenerSpy).toHaveBeenCalledWith();
728
891
  expect(callOffSpy).toHaveBeenCalledWith(
729
892
  CALL_EVENT_KEYS.REMOTE_MEDIA,
@@ -760,19 +923,29 @@ describe('TaskManager', () => {
760
923
 
761
924
  task.updateTaskData(payload.data);
762
925
  webSocketManagerMock.emit('message', JSON.stringify(payload));
926
+ <<<<<<< HEAD
763
927
  const stateMachineEvent = expectLastStateMachineEvent(
764
928
  sendStateMachineEventSpy,
765
929
  TaskEvent.CONTACT_ENDED
766
930
  );
767
931
  expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(true);
768
932
  sendStateMachineEventSpy.mockRestore();
933
+ =======
934
+ expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.CONTACT_ENDED, {...payload.data});
935
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, taskManager.getTask(taskId));
936
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
769
937
  });
770
938
 
771
939
  it('should emit TASK_REJECT event on AGENT_INVITE_FAILED event', () => {
772
940
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
773
941
 
942
+ <<<<<<< HEAD
774
943
  const task = taskManager.getTask(taskId);
775
944
  const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
945
+ =======
946
+ const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
947
+ const metricsTrackSpy = jest.spyOn(taskManager.metricsManager, 'trackEvent');
948
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
776
949
  const payload = {
777
950
  data: {
778
951
  type: CC_EVENTS.AGENT_INVITE_FAILED,
@@ -791,6 +964,7 @@ describe('TaskManager', () => {
791
964
  },
792
965
  };
793
966
 
967
+ <<<<<<< HEAD
794
968
  task.updateTaskData(payload.data);
795
969
  webSocketManagerMock.emit('message', JSON.stringify(payload));
796
970
  const stateMachineEvent = expectLastStateMachineEvent(
@@ -802,6 +976,18 @@ describe('TaskManager', () => {
802
976
  });
803
977
 
804
978
  it('should emit TASK_HYDRATE even if task is already present in taskManager', () => {
979
+ =======
980
+ taskManager.getTask(taskId).updateTaskData(payload.data);
981
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
982
+ expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.AGENT_INVITE_FAILED, {...payload.data});
983
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_REJECT, payload.data.reason);
984
+ // Verify the correct metric event name is used for AGENT_INVITE_FAILED
985
+ expect(metricsTrackSpy).toHaveBeenCalled();
986
+ expect(metricsTrackSpy.mock.calls[0][0]).toBe('Agent Invite Failed');
987
+ });
988
+
989
+ it('should not emit TASK_HYDRATE if task is already present in taskManager', () => {
990
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
805
991
  const payload = {
806
992
  data: {
807
993
  ...initalPayload.data,
@@ -938,7 +1124,11 @@ describe('TaskManager', () => {
938
1124
  expect(createdTask.data.isConferenceInProgress).toBe(false);
939
1125
  });
940
1126
 
1127
+ <<<<<<< HEAD
941
1128
  it('should emit TASK_WRAPUP event on AGENT_WRAPUP event', () => {
1129
+ =======
1130
+ it('should emit TASK_END event on AGENT_WRAPUP event', () => {
1131
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
942
1132
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
943
1133
 
944
1134
  const wrapupPayload = {
@@ -1093,8 +1283,13 @@ describe('TaskManager', () => {
1093
1283
 
1094
1284
  const task = taskManager.getTask(taskId);
1095
1285
  const taskEmitSpy = jest.spyOn(task, 'emit');
1286
+ <<<<<<< HEAD
1096
1287
  const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
1097
1288
  const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1289
+ =======
1290
+ const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
1291
+ const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
1292
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1098
1293
 
1099
1294
  // Step 2: Trigger AGENT_OFFER_CONTACT with auto-answer
1100
1295
  const autoAnswerPayload = {
@@ -1118,6 +1313,7 @@ describe('TaskManager', () => {
1118
1313
  // Verify accept was called
1119
1314
  expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
1120
1315
 
1316
+ <<<<<<< HEAD
1121
1317
  const stateMachineEvent = expectLastStateMachineEvent(
1122
1318
  sendStateMachineEventSpy,
1123
1319
  TaskEvent.TASK_OFFERED
@@ -1126,6 +1322,11 @@ describe('TaskManager', () => {
1126
1322
  // Verify task auto-answer event was emitted
1127
1323
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
1128
1324
  sendStateMachineEventSpy.mockRestore();
1325
+ =======
1326
+ // Verify BOTH events were emitted
1327
+ expect(taskManagerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OFFER_CONTACT, task);
1328
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
1329
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1129
1330
  });
1130
1331
 
1131
1332
  it('should NOT emit TASK_AUTO_ANSWERED event when auto-answer fails', async () => {
@@ -1171,7 +1372,10 @@ describe('TaskManager', () => {
1171
1372
  const task = taskManager.getTask(taskId);
1172
1373
  const taskEmitSpy = jest.spyOn(task, 'emit');
1173
1374
  const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
1375
+ <<<<<<< HEAD
1174
1376
  const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1377
+ =======
1378
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1175
1379
 
1176
1380
  // Step 2: Trigger AGENT_OFFER_CONSULT with auto-answer
1177
1381
  const consultAutoAnswerPayload = {
@@ -1196,6 +1400,7 @@ describe('TaskManager', () => {
1196
1400
  // Verify accept was called
1197
1401
  expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
1198
1402
 
1403
+ <<<<<<< HEAD
1199
1404
  const stateMachineEvent = expectLastStateMachineEvent(
1200
1405
  sendStateMachineEventSpy,
1201
1406
  TaskEvent.OFFER_CONSULT
@@ -1205,11 +1410,18 @@ describe('TaskManager', () => {
1205
1410
  isConsulted: true,
1206
1411
  });
1207
1412
  // Verify task auto-answer event was emitted
1413
+ =======
1414
+ // Verify BOTH events were emitted
1415
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OFFER_CONSULT, task);
1416
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1208
1417
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
1209
1418
 
1210
1419
  // Verify isConsulted flag is set correctly
1211
1420
  expect(task.data.isConsulted).toBe(true);
1421
+ <<<<<<< HEAD
1212
1422
  sendStateMachineEventSpy.mockRestore();
1423
+ =======
1424
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1213
1425
  });
1214
1426
 
1215
1427
  it('should NOT emit TASK_AUTO_ANSWERED when isAutoAnswering is false', async () => {
@@ -1250,6 +1462,7 @@ describe('TaskManager', () => {
1250
1462
  });
1251
1463
  });
1252
1464
 
1465
+ <<<<<<< HEAD
1253
1466
  it('should remove OUTDIAL task from taskCollection on AGENT_OUTBOUND_FAILED when terminated', () => {
1254
1467
  const task = taskManager.getTask(taskId);
1255
1468
  Object.assign(task.data, {
@@ -1264,6 +1477,26 @@ describe('TaskManager', () => {
1264
1477
  task.unregisterWebCallListeners = jest.fn();
1265
1478
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1266
1479
  const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1480
+ =======
1481
+ it('should NOT remove OUTDIAL task from taskCollection on AGENT_OUTBOUND_FAILED when terminated (wrap-up flow)', () => {
1482
+ const task = taskManager.getTask(taskId);
1483
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1484
+ task.data = {
1485
+ ...task.data,
1486
+ ...newData,
1487
+ interaction: {
1488
+ ...task.data.interaction,
1489
+ ...newData.interaction,
1490
+ outboundType: 'OUTDIAL',
1491
+ state: 'new',
1492
+ isTerminated: true,
1493
+ },
1494
+ };
1495
+ return task;
1496
+ });
1497
+ task.unregisterWebCallListeners = jest.fn();
1498
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1499
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1267
1500
 
1268
1501
  const payload = {
1269
1502
  data: {
@@ -1276,7 +1509,10 @@ describe('TaskManager', () => {
1276
1509
  state: 'new',
1277
1510
  isTerminated: true,
1278
1511
  },
1512
+ <<<<<<< HEAD
1279
1513
  agentsPendingWrapUp: ['agent-123'],
1514
+ =======
1515
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1280
1516
  interactionId: taskId,
1281
1517
  orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
1282
1518
  trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
@@ -1290,62 +1526,26 @@ describe('TaskManager', () => {
1290
1526
  };
1291
1527
 
1292
1528
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1529
+ <<<<<<< HEAD
1530
+ =======
1293
1531
 
1294
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1295
-
1296
- expect(taskManager.getTask(taskId)).toBeUndefined();
1297
- expect(removeTaskSpy).toHaveBeenCalled();
1298
- const stateMachineEvent = expectLastStateMachineEvent(
1299
- sendStateMachineEventSpy,
1300
- TaskEvent.OUTBOUND_FAILED
1301
- );
1302
- expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
1303
- sendStateMachineEventSpy.mockRestore();
1532
+ expect(taskManager.getTask(taskId)).toBeDefined();
1533
+ expect(removeTaskSpy).not.toHaveBeenCalled();
1304
1534
  });
1305
1535
 
1306
1536
  it('should emit TASK_OUTDIAL_FAILED event on AGENT_OUTBOUND_FAILED', () => {
1307
1537
  const task = taskManager.getTask(taskId);
1308
- const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1309
- const payload = {
1310
- data: {
1311
- type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
1312
- interactionId: taskId,
1313
- reason: 'CUSTOMER_BUSY',
1314
- },
1315
- };
1316
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1317
- const stateMachineEvent = expectLastStateMachineEvent(
1318
- sendStateMachineEventSpy,
1319
- TaskEvent.OUTBOUND_FAILED
1320
- );
1321
- expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
1322
- sendStateMachineEventSpy.mockRestore();
1323
- });
1324
-
1325
- it('should pass taskData in OUTBOUND_FAILED event for shouldWrapUp guard evaluation', () => {
1326
- const task = taskManager.getTask(taskId);
1327
- const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1538
+ task.updateTaskData = jest.fn().mockReturnValue(task);
1539
+ const taskEmitSpy = jest.spyOn(task, 'emit');
1328
1540
  const payload = {
1329
1541
  data: {
1330
1542
  type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
1331
1543
  interactionId: taskId,
1332
1544
  reason: 'CUSTOMER_BUSY',
1333
- agentsPendingWrapUp: ['agent-123'],
1334
- interaction: {
1335
- outboundType: 'OUTDIAL',
1336
- isTerminated: true,
1337
- },
1338
1545
  },
1339
1546
  };
1340
1547
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1341
- const stateMachineEvent = expectLastStateMachineEvent(
1342
- sendStateMachineEventSpy,
1343
- TaskEvent.OUTBOUND_FAILED
1344
- );
1345
- expect(stateMachineEvent?.taskData).toBeDefined();
1346
- expect(stateMachineEvent?.taskData?.agentsPendingWrapUp).toEqual(['agent-123']);
1347
- expect(stateMachineEvent?.taskData?.interaction?.outboundType).toBe('OUTDIAL');
1348
- sendStateMachineEventSpy.mockRestore();
1548
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OUTDIAL_FAILED, 'CUSTOMER_BUSY');
1349
1549
  });
1350
1550
 
1351
1551
  it('should handle AGENT_OUTBOUND_FAILED gracefully when task is undefined', () => {
@@ -1363,15 +1563,23 @@ describe('TaskManager', () => {
1363
1563
  });
1364
1564
 
1365
1565
  it('should NOT remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp exists', () => {
1566
+ const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
1567
+ taskManager.setAgentId(agentId);
1568
+
1366
1569
  const task = taskManager.getTask(taskId);
1367
- Object.assign(task.data, {
1368
- interaction: {
1369
- ...task.data.interaction,
1370
- outboundType: 'OUTDIAL',
1371
- state: 'new',
1372
- mediaType: 'telephony',
1373
- },
1374
- agentsPendingWrapUp: ['agent-123'],
1570
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1571
+ task.data = {
1572
+ ...task.data,
1573
+ ...newData,
1574
+ interaction: {
1575
+ ...task.data.interaction,
1576
+ outboundType: 'OUTDIAL',
1577
+ state: 'new',
1578
+ mediaType: 'telephony',
1579
+ },
1580
+ agentsPendingWrapUp: [agentId],
1581
+ };
1582
+ return task;
1375
1583
  });
1376
1584
  task.unregisterWebCallListeners = jest.fn();
1377
1585
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -1385,7 +1593,7 @@ describe('TaskManager', () => {
1385
1593
  state: 'new',
1386
1594
  mediaType: 'telephony',
1387
1595
  },
1388
- agentsPendingWrapUp: ['agent-123'],
1596
+ agentsPendingWrapUp: [agentId],
1389
1597
  },
1390
1598
  };
1391
1599
 
@@ -1397,14 +1605,19 @@ describe('TaskManager', () => {
1397
1605
 
1398
1606
  it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is empty', () => {
1399
1607
  const task = taskManager.getTask(taskId);
1400
- Object.assign(task.data, {
1401
- interaction: {
1402
- ...task.data.interaction,
1403
- outboundType: 'OUTDIAL',
1404
- state: 'new',
1405
- mediaType: 'telephony',
1406
- },
1407
- agentsPendingWrapUp: [],
1608
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1609
+ task.data = {
1610
+ ...task.data,
1611
+ ...newData,
1612
+ interaction: {
1613
+ ...task.data.interaction,
1614
+ outboundType: 'OUTDIAL',
1615
+ state: 'new',
1616
+ mediaType: 'telephony',
1617
+ },
1618
+ agentsPendingWrapUp: [],
1619
+ };
1620
+ return task;
1408
1621
  });
1409
1622
  task.unregisterWebCallListeners = jest.fn();
1410
1623
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -1429,13 +1642,19 @@ describe('TaskManager', () => {
1429
1642
 
1430
1643
  it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is undefined', () => {
1431
1644
  const task = taskManager.getTask(taskId);
1432
- Object.assign(task.data, {
1433
- interaction: {
1434
- ...task.data.interaction,
1435
- outboundType: 'OUTDIAL',
1436
- state: 'new',
1437
- mediaType: 'telephony',
1438
- },
1645
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1646
+ task.data = {
1647
+ ...task.data,
1648
+ ...newData,
1649
+ interaction: {
1650
+ ...task.data.interaction,
1651
+ outboundType: 'OUTDIAL',
1652
+ state: 'new',
1653
+ mediaType: 'telephony',
1654
+ },
1655
+ // agentsPendingWrapUp is undefined
1656
+ };
1657
+ return task;
1439
1658
  });
1440
1659
  task.unregisterWebCallListeners = jest.fn();
1441
1660
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -1474,34 +1693,338 @@ describe('TaskManager', () => {
1474
1693
  }).not.toThrow();
1475
1694
  });
1476
1695
 
1477
- it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
1478
- const task = taskManager.getTask(taskId);
1479
- Object.assign(task.data, {
1480
- interaction: {
1481
- ...task.data.interaction,
1482
- outboundType: 'OUTDIAL',
1483
- state: 'new',
1484
- isTerminated: false,
1485
- },
1696
+ describe('wrapUpRequired logic in CONTACT_ENDED event', () => {
1697
+ const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
1698
+
1699
+ beforeEach(() => {
1700
+ // Set the agent ID on taskManager
1701
+ taskManager.setAgentId(agentId);
1486
1702
  });
1487
- task.unregisterWebCallListeners = jest.fn();
1488
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1489
1703
 
1490
- const payload = {
1491
- data: {
1492
- type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
1493
- agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
1494
- eventTime: 1733211616959,
1495
- eventType: 'RoutingMessage',
1496
- interaction: {
1497
- outboundType: 'OUTDIAL',
1498
- state: 'new',
1499
- isTerminated: false,
1704
+ it('should set wrapUpRequired to true when agent is in agentsPendingWrapUp array for non-campaign tasks', () => {
1705
+ const task = taskManager.getTask(taskId);
1706
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1707
+ task.data = {
1708
+ ...task.data,
1709
+ ...newData,
1710
+ };
1711
+ return task;
1712
+ });
1713
+ task.unregisterWebCallListeners = jest.fn();
1714
+
1715
+ const payload = {
1716
+ data: {
1717
+ type: CC_EVENTS.CONTACT_ENDED,
1718
+ interactionId: taskId,
1719
+ interaction: {
1720
+ state: 'connected',
1721
+ mediaType: 'telephony',
1722
+ },
1723
+ agentsPendingWrapUp: [agentId, 'other-agent-id'],
1500
1724
  },
1501
- interactionId: taskId,
1502
- orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
1503
- trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
1504
- mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
1725
+ };
1726
+
1727
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1728
+
1729
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1730
+ expect.objectContaining({
1731
+ wrapUpRequired: true,
1732
+ })
1733
+ );
1734
+ });
1735
+
1736
+ it('should set wrapUpRequired to false when agent is not in agentsPendingWrapUp array', () => {
1737
+ const task = taskManager.getTask(taskId);
1738
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1739
+ task.data = {
1740
+ ...task.data,
1741
+ ...newData,
1742
+ };
1743
+ return task;
1744
+ });
1745
+ task.unregisterWebCallListeners = jest.fn();
1746
+
1747
+ const payload = {
1748
+ data: {
1749
+ type: CC_EVENTS.CONTACT_ENDED,
1750
+ interactionId: taskId,
1751
+ interaction: {
1752
+ state: 'connected',
1753
+ mediaType: 'telephony',
1754
+ },
1755
+ agentsPendingWrapUp: ['other-agent-id', 'another-agent-id'],
1756
+ },
1757
+ };
1758
+
1759
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1760
+
1761
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1762
+ expect.objectContaining({
1763
+ wrapUpRequired: false,
1764
+ })
1765
+ );
1766
+ });
1767
+
1768
+ it('should set wrapUpRequired to false when agentsPendingWrapUp is an empty array', () => {
1769
+ const task = taskManager.getTask(taskId);
1770
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1771
+ task.data = {
1772
+ ...task.data,
1773
+ ...newData,
1774
+ };
1775
+ return task;
1776
+ });
1777
+ task.unregisterWebCallListeners = jest.fn();
1778
+
1779
+ const payload = {
1780
+ data: {
1781
+ type: CC_EVENTS.CONTACT_ENDED,
1782
+ interactionId: taskId,
1783
+ interaction: {
1784
+ state: 'connected',
1785
+ mediaType: 'telephony',
1786
+ },
1787
+ agentsPendingWrapUp: [],
1788
+ },
1789
+ };
1790
+
1791
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1792
+
1793
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1794
+ expect.objectContaining({
1795
+ wrapUpRequired: false,
1796
+ })
1797
+ );
1798
+ });
1799
+
1800
+ it('should set wrapUpRequired to false when agentsPendingWrapUp is undefined', () => {
1801
+ const task = taskManager.getTask(taskId);
1802
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1803
+ task.data = {
1804
+ ...task.data,
1805
+ ...newData,
1806
+ };
1807
+ return task;
1808
+ });
1809
+ task.unregisterWebCallListeners = jest.fn();
1810
+
1811
+ const payload = {
1812
+ data: {
1813
+ type: CC_EVENTS.CONTACT_ENDED,
1814
+ interactionId: taskId,
1815
+ interaction: {
1816
+ state: 'connected',
1817
+ mediaType: 'telephony',
1818
+ },
1819
+ // agentsPendingWrapUp is not defined
1820
+ },
1821
+ };
1822
+
1823
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1824
+
1825
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1826
+ expect.objectContaining({
1827
+ wrapUpRequired: false,
1828
+ })
1829
+ );
1830
+ });
1831
+
1832
+ it('should set wrapUpRequired to false when agentsPendingWrapUp is null', () => {
1833
+ const task = taskManager.getTask(taskId);
1834
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1835
+ task.data = {
1836
+ ...task.data,
1837
+ ...newData,
1838
+ };
1839
+ return task;
1840
+ });
1841
+ task.unregisterWebCallListeners = jest.fn();
1842
+
1843
+ const payload = {
1844
+ data: {
1845
+ type: CC_EVENTS.CONTACT_ENDED,
1846
+ interactionId: taskId,
1847
+ interaction: {
1848
+ state: 'connected',
1849
+ mediaType: 'telephony',
1850
+ },
1851
+ agentsPendingWrapUp: null,
1852
+ },
1853
+ };
1854
+
1855
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1856
+
1857
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1858
+ expect.objectContaining({
1859
+ wrapUpRequired: false,
1860
+ })
1861
+ );
1862
+ });
1863
+
1864
+ it('should set wrapUpRequired to true when agent is the only one in agentsPendingWrapUp for non-campaign tasks', () => {
1865
+ const task = taskManager.getTask(taskId);
1866
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1867
+ task.data = {
1868
+ ...task.data,
1869
+ ...newData,
1870
+ };
1871
+ return task;
1872
+ });
1873
+ task.unregisterWebCallListeners = jest.fn();
1874
+
1875
+ const payload = {
1876
+ data: {
1877
+ type: CC_EVENTS.CONTACT_ENDED,
1878
+ interactionId: taskId,
1879
+ interaction: {
1880
+ state: 'connected',
1881
+ mediaType: 'telephony',
1882
+ },
1883
+ agentsPendingWrapUp: [agentId],
1884
+ },
1885
+ };
1886
+
1887
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1888
+
1889
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1890
+ expect.objectContaining({
1891
+ wrapUpRequired: true,
1892
+ })
1893
+ );
1894
+ });
1895
+
1896
+ it('should set wrapUpRequired to true for different interaction states when agent is in agentsPendingWrapUp for non-campaign tasks', () => {
1897
+ const task = taskManager.getTask(taskId);
1898
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1899
+ task.data = {
1900
+ ...task.data,
1901
+ ...newData,
1902
+ interaction: {
1903
+ ...task.data.interaction,
1904
+ ...newData.interaction,
1905
+ },
1906
+ };
1907
+ return task;
1908
+ });
1909
+ task.unregisterWebCallListeners = jest.fn();
1910
+
1911
+ // Test with 'connected' state
1912
+ const payloadConnected = {
1913
+ data: {
1914
+ type: CC_EVENTS.CONTACT_ENDED,
1915
+ interactionId: taskId,
1916
+ interaction: {
1917
+ state: 'connected',
1918
+ mediaType: 'telephony',
1919
+ },
1920
+ agentsPendingWrapUp: [agentId],
1921
+ },
1922
+ };
1923
+
1924
+ webSocketManagerMock.emit('message', JSON.stringify(payloadConnected));
1925
+
1926
+ expect(task.updateTaskData).toHaveBeenNthCalledWith(
1927
+ 1,
1928
+ expect.objectContaining({
1929
+ wrapUpRequired: true,
1930
+ })
1931
+ );
1932
+
1933
+ // Test with 'held' state to verify it still works regardless of state
1934
+ const payloadHeld = {
1935
+ data: {
1936
+ type: CC_EVENTS.CONTACT_ENDED,
1937
+ interactionId: taskId,
1938
+ interaction: {
1939
+ state: 'held',
1940
+ mediaType: 'telephony',
1941
+ },
1942
+ agentsPendingWrapUp: [agentId],
1943
+ },
1944
+ };
1945
+
1946
+ webSocketManagerMock.emit('message', JSON.stringify(payloadHeld));
1947
+
1948
+ expect(task.updateTaskData).toHaveBeenNthCalledWith(
1949
+ 2,
1950
+ expect.objectContaining({
1951
+ wrapUpRequired: true,
1952
+ })
1953
+ );
1954
+ });
1955
+
1956
+ it('should set wrapUpRequired to false for campaign preview tasks even when agent is in agentsPendingWrapUp', () => {
1957
+ const task = taskManager.getTask(taskId);
1958
+ // Set up task as a campaign preview task via outboundType
1959
+ task.data.interaction = {
1960
+ ...task.data.interaction,
1961
+ outboundType: 'STANDARD_PREVIEW_CAMPAIGN',
1962
+ };
1963
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1964
+ task.data = {
1965
+ ...task.data,
1966
+ ...newData,
1967
+ };
1968
+ return task;
1969
+ });
1970
+ task.unregisterWebCallListeners = jest.fn();
1971
+
1972
+ const payload = {
1973
+ data: {
1974
+ type: CC_EVENTS.CONTACT_ENDED,
1975
+ interactionId: taskId,
1976
+ interaction: {
1977
+ state: 'connected',
1978
+ mediaType: 'telephony',
1979
+ },
1980
+ agentsPendingWrapUp: [agentId],
1981
+ },
1982
+ };
1983
+
1984
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1985
+
1986
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1987
+ expect.objectContaining({
1988
+ wrapUpRequired: false,
1989
+ })
1990
+ );
1991
+ });
1992
+ });
1993
+
1994
+ it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
1995
+ const task = taskManager.getTask(taskId);
1996
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
1997
+ task.data = {
1998
+ ...task.data,
1999
+ ...newData,
2000
+ interaction: {
2001
+ ...task.data.interaction,
2002
+ ...newData.interaction,
2003
+ outboundType: 'OUTDIAL',
2004
+ state: 'new',
2005
+ isTerminated: false,
2006
+ },
2007
+ };
2008
+ return task;
2009
+ });
2010
+ task.unregisterWebCallListeners = jest.fn();
2011
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2012
+
2013
+ const payload = {
2014
+ data: {
2015
+ type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
2016
+ agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
2017
+ eventTime: 1733211616959,
2018
+ eventType: 'RoutingMessage',
2019
+ interaction: {
2020
+ outboundType: 'OUTDIAL',
2021
+ state: 'new',
2022
+ isTerminated: false,
2023
+ },
2024
+ interactionId: taskId,
2025
+ orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
2026
+ trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
2027
+ mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
1505
2028
  destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
1506
2029
  owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
1507
2030
  queueMgr: 'aqm',
@@ -1509,91 +2032,322 @@ describe('TaskManager', () => {
1509
2032
  reasonCode: 156,
1510
2033
  },
1511
2034
  };
2035
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1512
2036
 
1513
2037
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1514
2038
 
1515
2039
  expect(taskManager.getTask(taskId)).toBeUndefined();
1516
2040
  expect(removeTaskSpy).toHaveBeenCalled();
1517
- });
1518
-
1519
- it('handle AGENT_OFFER_CONSULT event', () => {
1520
- const payload = {
1521
- data: {
1522
- ...initalPayload.data,
1523
- type: CC_EVENTS.AGENT_OFFER_CONSULT,
1524
- },
1525
- };
1526
-
1527
- webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1528
- const task = taskManager.getTask(taskId);
1529
- const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1530
-
1531
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1532
-
2041
+ <<<<<<< HEAD
1533
2042
  const stateMachineEvent = expectLastStateMachineEvent(
1534
2043
  sendStateMachineEventSpy,
1535
- TaskEvent.OFFER_CONSULT
2044
+ TaskEvent.OUTBOUND_FAILED
1536
2045
  );
1537
- expect(stateMachineEvent?.taskData).toEqual({...payload.data, isConsulted: true});
2046
+ expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
1538
2047
  sendStateMachineEventSpy.mockRestore();
1539
2048
  });
1540
2049
 
1541
- it('should emit TASK_CONSULT_ACCEPTED event on AGENT_CONSULTING event', () => {
1542
- const initialConsultingPayload = {
1543
- data: {
1544
- ...initalPayload.data,
1545
- type: CC_EVENTS.AGENT_OFFER_CONSULT,
1546
- },
1547
- };
1548
-
1549
- const consultingPayload = {
2050
+ it('should emit TASK_OUTDIAL_FAILED event on AGENT_OUTBOUND_FAILED', () => {
2051
+ const task = taskManager.getTask(taskId);
2052
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
2053
+ const payload = {
1550
2054
  data: {
1551
- ...initalPayload.data,
1552
- type: CC_EVENTS.AGENT_CONSULTING,
2055
+ type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
2056
+ interactionId: taskId,
2057
+ reason: 'CUSTOMER_BUSY',
1553
2058
  },
1554
2059
  };
1555
-
1556
- webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1557
-
1558
- const task = taskManager.getTask(taskId);
1559
- const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1560
-
1561
- webSocketManagerMock.emit('message', JSON.stringify(initialConsultingPayload));
1562
- webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
2060
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1563
2061
  const stateMachineEvent = expectLastStateMachineEvent(
1564
2062
  sendStateMachineEventSpy,
1565
- TaskEvent.CONSULTING_ACTIVE
2063
+ TaskEvent.OUTBOUND_FAILED
1566
2064
  );
1567
- expect(stateMachineEvent?.taskData).toEqual(consultingPayload.data);
2065
+ expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
1568
2066
  sendStateMachineEventSpy.mockRestore();
1569
2067
  });
1570
2068
 
1571
- it('should emit TASK_CONSULT_ENDED event on AGENT_CONSULT_ENDED event', () => {
2069
+ it('should pass taskData in OUTBOUND_FAILED event for shouldWrapUp guard evaluation', () => {
2070
+ const task = taskManager.getTask(taskId);
2071
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1572
2072
  const payload = {
1573
2073
  data: {
1574
- ...initalPayload.data,
1575
- type: CC_EVENTS.AGENT_CONSULT_ENDED,
2074
+ type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
2075
+ interactionId: taskId,
2076
+ reason: 'CUSTOMER_BUSY',
2077
+ agentsPendingWrapUp: ['agent-123'],
2078
+ interaction: {
2079
+ outboundType: 'OUTDIAL',
2080
+ isTerminated: true,
2081
+ },
1576
2082
  },
1577
2083
  };
1578
-
1579
- webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1580
- taskManager.getTask(taskId).data.isConsulted = true;
1581
- const task = taskManager.getTask(taskId);
1582
- const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1583
2084
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1584
2085
  const stateMachineEvent = expectLastStateMachineEvent(
1585
2086
  sendStateMachineEventSpy,
1586
- TaskEvent.CONSULT_END
2087
+ TaskEvent.OUTBOUND_FAILED
1587
2088
  );
1588
- expect(stateMachineEvent?.taskData).toEqual(payload.data);
2089
+ expect(stateMachineEvent?.taskData).toBeDefined();
2090
+ expect(stateMachineEvent?.taskData?.agentsPendingWrapUp).toEqual(['agent-123']);
2091
+ expect(stateMachineEvent?.taskData?.interaction?.outboundType).toBe('OUTDIAL');
1589
2092
  sendStateMachineEventSpy.mockRestore();
1590
2093
  });
1591
2094
 
1592
- it('should emit TASK_CONSULT_ENDED event and remove currentTask when on AGENT_CONSULT_ENDED event when requested for a consult', () => {
2095
+ it('should handle AGENT_OUTBOUND_FAILED gracefully when task is undefined', () => {
1593
2096
  const payload = {
1594
2097
  data: {
1595
- ...initalPayload.data,
1596
- type: CC_EVENTS.AGENT_CONSULT_ENDED,
2098
+ type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
2099
+ interactionId: 'non-existent-task-id',
2100
+ reason: 'CUSTOMER_BUSY',
2101
+ },
2102
+ };
2103
+ // Should not throw error when task doesn't exist
2104
+ expect(() => {
2105
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2106
+ }).not.toThrow();
2107
+ });
2108
+
2109
+ it('should NOT remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp exists', () => {
2110
+ const task = taskManager.getTask(taskId);
2111
+ Object.assign(task.data, {
2112
+ interaction: {
2113
+ ...task.data.interaction,
2114
+ outboundType: 'OUTDIAL',
2115
+ state: 'new',
2116
+ mediaType: 'telephony',
2117
+ },
2118
+ agentsPendingWrapUp: ['agent-123'],
2119
+ });
2120
+ task.unregisterWebCallListeners = jest.fn();
2121
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2122
+
2123
+ const payload = {
2124
+ data: {
2125
+ type: CC_EVENTS.CONTACT_ENDED,
2126
+ interactionId: taskId,
2127
+ interaction: {
2128
+ outboundType: 'OUTDIAL',
2129
+ state: 'new',
2130
+ mediaType: 'telephony',
2131
+ },
2132
+ agentsPendingWrapUp: ['agent-123'],
2133
+ },
2134
+ };
2135
+
2136
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2137
+
2138
+ expect(removeTaskSpy).not.toHaveBeenCalled();
2139
+ expect(taskManager.getTask(taskId)).toBeDefined();
2140
+ });
2141
+
2142
+ it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is empty', () => {
2143
+ const task = taskManager.getTask(taskId);
2144
+ Object.assign(task.data, {
2145
+ interaction: {
2146
+ ...task.data.interaction,
2147
+ outboundType: 'OUTDIAL',
2148
+ state: 'new',
2149
+ mediaType: 'telephony',
2150
+ },
2151
+ agentsPendingWrapUp: [],
2152
+ });
2153
+ task.unregisterWebCallListeners = jest.fn();
2154
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2155
+
2156
+ const payload = {
2157
+ data: {
2158
+ type: CC_EVENTS.CONTACT_ENDED,
2159
+ interactionId: taskId,
2160
+ interaction: {
2161
+ outboundType: 'OUTDIAL',
2162
+ state: 'new',
2163
+ mediaType: 'telephony',
2164
+ },
2165
+ agentsPendingWrapUp: [],
2166
+ },
2167
+ };
2168
+
2169
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2170
+
2171
+ expect(removeTaskSpy).toHaveBeenCalled();
2172
+ });
2173
+
2174
+ it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is undefined', () => {
2175
+ const task = taskManager.getTask(taskId);
2176
+ Object.assign(task.data, {
2177
+ interaction: {
2178
+ ...task.data.interaction,
2179
+ outboundType: 'OUTDIAL',
2180
+ state: 'new',
2181
+ mediaType: 'telephony',
2182
+ },
2183
+ });
2184
+ task.unregisterWebCallListeners = jest.fn();
2185
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2186
+
2187
+ const payload = {
2188
+ data: {
2189
+ type: CC_EVENTS.CONTACT_ENDED,
2190
+ interactionId: taskId,
2191
+ interaction: {
2192
+ outboundType: 'OUTDIAL',
2193
+ state: 'new',
2194
+ mediaType: 'telephony',
2195
+ },
2196
+ // agentsPendingWrapUp not included
2197
+ },
2198
+ };
2199
+
2200
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2201
+
2202
+ expect(removeTaskSpy).toHaveBeenCalled();
2203
+ });
2204
+
2205
+ it('should handle CONTACT_ENDED gracefully when task is undefined', () => {
2206
+ const payload = {
2207
+ data: {
2208
+ type: CC_EVENTS.CONTACT_ENDED,
2209
+ interactionId: 'non-existent-task-id',
2210
+ interaction: {
2211
+ state: 'new',
2212
+ },
2213
+ },
2214
+ };
2215
+ // Should not throw error when task doesn't exist
2216
+ expect(() => {
2217
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2218
+ }).not.toThrow();
2219
+ });
2220
+
2221
+ it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
2222
+ const task = taskManager.getTask(taskId);
2223
+ Object.assign(task.data, {
2224
+ interaction: {
2225
+ ...task.data.interaction,
2226
+ outboundType: 'OUTDIAL',
2227
+ state: 'new',
2228
+ isTerminated: false,
2229
+ },
2230
+ });
2231
+ task.unregisterWebCallListeners = jest.fn();
2232
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2233
+
2234
+ const payload = {
2235
+ data: {
2236
+ type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
2237
+ agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
2238
+ eventTime: 1733211616959,
2239
+ eventType: 'RoutingMessage',
2240
+ interaction: {
2241
+ outboundType: 'OUTDIAL',
2242
+ state: 'new',
2243
+ isTerminated: false,
2244
+ },
2245
+ interactionId: taskId,
2246
+ orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
2247
+ trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
2248
+ mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
2249
+ destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
2250
+ owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
2251
+ queueMgr: 'aqm',
2252
+ reason: 'USER_DECLINED',
2253
+ reasonCode: 156,
2254
+ },
2255
+ };
2256
+
2257
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2258
+
2259
+ expect(taskManager.getTask(taskId)).toBeUndefined();
2260
+ expect(removeTaskSpy).toHaveBeenCalled();
2261
+ =======
2262
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2263
+ });
2264
+
2265
+ it('handle AGENT_OFFER_CONSULT event', () => {
2266
+ const payload = {
2267
+ data: {
2268
+ ...initalPayload.data,
2269
+ type: CC_EVENTS.AGENT_OFFER_CONSULT,
2270
+ },
2271
+ };
2272
+
2273
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
2274
+ const task = taskManager.getTask(taskId);
2275
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
2276
+
2277
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2278
+
2279
+ const stateMachineEvent = expectLastStateMachineEvent(
2280
+ sendStateMachineEventSpy,
2281
+ TaskEvent.OFFER_CONSULT
2282
+ );
2283
+ expect(stateMachineEvent?.taskData).toEqual({...payload.data, isConsulted: true});
2284
+ sendStateMachineEventSpy.mockRestore();
2285
+ });
2286
+
2287
+ it('should emit TASK_CONSULT_ACCEPTED event on AGENT_CONSULTING event', () => {
2288
+ const initialConsultingPayload = {
2289
+ data: {
2290
+ ...initalPayload.data,
2291
+ type: CC_EVENTS.AGENT_OFFER_CONSULT,
2292
+ },
2293
+ };
2294
+
2295
+ const consultingPayload = {
2296
+ data: {
2297
+ ...initalPayload.data,
2298
+ type: CC_EVENTS.AGENT_CONSULTING,
2299
+ },
2300
+ };
2301
+
2302
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
2303
+
2304
+ const task = taskManager.getTask(taskId);
2305
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
2306
+
2307
+ webSocketManagerMock.emit('message', JSON.stringify(initialConsultingPayload));
2308
+ webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
2309
+ const stateMachineEvent = expectLastStateMachineEvent(
2310
+ sendStateMachineEventSpy,
2311
+ TaskEvent.CONSULTING_ACTIVE
2312
+ );
2313
+ expect(stateMachineEvent?.taskData).toEqual(consultingPayload.data);
2314
+ sendStateMachineEventSpy.mockRestore();
2315
+ });
2316
+
2317
+ it('should emit TASK_CONSULT_ENDED event on AGENT_CONSULT_ENDED event', () => {
2318
+ const payload = {
2319
+ data: {
2320
+ ...initalPayload.data,
2321
+ type: CC_EVENTS.AGENT_CONSULT_ENDED,
2322
+ },
2323
+ };
2324
+
2325
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
2326
+ taskManager.getTask(taskId).data.isConsulted = true;
2327
+ const task = taskManager.getTask(taskId);
2328
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
2329
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2330
+ <<<<<<< HEAD
2331
+ const stateMachineEvent = expectLastStateMachineEvent(
2332
+ sendStateMachineEventSpy,
2333
+ TaskEvent.CONSULT_END
2334
+ );
2335
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
2336
+ sendStateMachineEventSpy.mockRestore();
2337
+ =======
2338
+ expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
2339
+ expect(taskEmitSpy).toHaveBeenCalledWith(
2340
+ TASK_EVENTS.TASK_CONSULT_END,
2341
+ taskManager.getTask(taskId)
2342
+ );
2343
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2344
+ });
2345
+
2346
+ it('should emit TASK_CONSULT_ENDED event and remove currentTask when on AGENT_CONSULT_ENDED event when requested for a consult', () => {
2347
+ const payload = {
2348
+ data: {
2349
+ ...initalPayload.data,
2350
+ type: CC_EVENTS.AGENT_CONSULT_ENDED,
1597
2351
  },
1598
2352
  };
1599
2353
 
@@ -1766,12 +2520,23 @@ describe('TaskManager', () => {
1766
2520
 
1767
2521
  webSocketManagerMock.emit('message', JSON.stringify(assignFailedPayload));
1768
2522
 
2523
+ <<<<<<< HEAD
1769
2524
  const stateMachineEvent = expectLastStateMachineEvent(
1770
2525
  sendStateMachineEventSpy,
1771
2526
  TaskEvent.ASSIGN_FAILED
1772
2527
  );
1773
2528
  expect(stateMachineEvent?.reason).toBe(assignFailedPayload.data.reason);
1774
2529
  sendStateMachineEventSpy.mockRestore();
2530
+ =======
2531
+ expect(taskUpdateDataSpy).toHaveBeenCalledWith(assignFailedPayload.data);
2532
+ expect(taskEmitSpy).toHaveBeenCalledWith(
2533
+ TASK_EVENTS.TASK_REJECT,
2534
+ assignFailedPayload.data.reason
2535
+ );
2536
+ // Verify the correct metric event name is used for AGENT_CONTACT_ASSIGN_FAILED
2537
+ expect(metricsTrackSpy).toHaveBeenCalled();
2538
+ expect(metricsTrackSpy.mock.calls[0][0]).toBe('Agent Contact Assign Failed');
2539
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1775
2540
  });
1776
2541
 
1777
2542
  it('should remove currentTask from taskCollection on AGENT_WRAPPEDUP event', () => {
@@ -1837,6 +2602,7 @@ describe('TaskManager', () => {
1837
2602
  taskManager.getTask(taskId).data.isConsulted = false;
1838
2603
  const task = taskManager.getTask(taskId);
1839
2604
  const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
2605
+ const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
1840
2606
  const consultingPayload = {
1841
2607
  data: {
1842
2608
  ...initalPayload.data,
@@ -1845,8 +2611,20 @@ describe('TaskManager', () => {
1845
2611
  },
1846
2612
  };
1847
2613
  webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
2614
+ <<<<<<< HEAD
1848
2615
  expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONSULTING_ACTIVE);
2616
+ expect(taskManagerEmitSpy).toHaveBeenCalledWith(
2617
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
2618
+ taskManager.getTask(taskId)
2619
+ );
1849
2620
  sendStateMachineEventSpy.mockRestore();
2621
+ =======
2622
+ expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.AGENT_CONSULTING, consultingPayload.data);
2623
+ expect(taskEmitSpy).toHaveBeenCalledWith(
2624
+ TASK_EVENTS.TASK_CONSULTING,
2625
+ taskManager.getTask(taskId)
2626
+ );
2627
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1850
2628
  });
1851
2629
 
1852
2630
  it('should update task data on AGENT_CONTACT_UNASSIGNED', () => {
@@ -1873,6 +2651,7 @@ describe('TaskManager', () => {
1873
2651
  },
1874
2652
  };
1875
2653
  webSocketManagerMock.emit('message', JSON.stringify(unassignedPayload));
2654
+ <<<<<<< HEAD
1876
2655
  expect(sendStateMachineEventSpy).not.toHaveBeenCalled();
1877
2656
  expect(updateTaskDataSpy).toHaveBeenCalledWith(unassignedPayload.data);
1878
2657
  });
@@ -1943,6 +2722,13 @@ describe('TaskManager', () => {
1943
2722
  destinationType: null,
1944
2723
  })
1945
2724
  );
2725
+ =======
2726
+ expect(taskEmitSpy).toHaveBeenCalledWith(
2727
+ CC_EVENTS.AGENT_CONTACT_UNASSIGNED,
2728
+ unassignedPayload.data
2729
+ );
2730
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, taskManager.getTask(taskId));
2731
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1946
2732
  });
1947
2733
 
1948
2734
  it('should handle chat interaction and emit TASK_INCOMING immediately', () => {
@@ -1954,6 +2740,11 @@ describe('TaskManager', () => {
1954
2740
  },
1955
2741
  };
1956
2742
 
2743
+ <<<<<<< HEAD
2744
+ =======
2745
+ const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
2746
+
2747
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1957
2748
  // Simulate receiving a chat task
1958
2749
  webSocketManagerMock.emit('message', JSON.stringify(chatPayload));
1959
2750
 
@@ -1976,6 +2767,11 @@ describe('TaskManager', () => {
1976
2767
  },
1977
2768
  };
1978
2769
 
2770
+ <<<<<<< HEAD
2771
+ =======
2772
+ const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
2773
+
2774
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1979
2775
  // Simulate receiving an email task
1980
2776
  webSocketManagerMock.emit('message', JSON.stringify(emailPayload));
1981
2777
 
@@ -1999,11 +2795,21 @@ describe('TaskManager', () => {
1999
2795
  },
2000
2796
  };
2001
2797
 
2798
+ <<<<<<< HEAD
2002
2799
  webSocketManagerMock.emit('message', JSON.stringify(chatReservedPayload));
2003
2800
  const task = taskManager.getTask(chatReservedPayload.data.interactionId);
2004
2801
  const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
2005
2802
 
2006
2803
  expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TASK_INCOMING);
2804
+ =======
2805
+ const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
2806
+ webSocketManagerMock.emit('message', JSON.stringify(chatReservedPayload));
2807
+
2808
+ expect(taskIncomingSpy).toHaveBeenCalledWith(
2809
+ TASK_EVENTS.TASK_INCOMING,
2810
+ taskManager.getTask(chatReservedPayload.data.interactionId)
2811
+ );
2812
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2007
2813
 
2008
2814
  // 2. Chat task is assigned
2009
2815
  const chatAssignedPayload = {
@@ -2013,17 +2819,29 @@ describe('TaskManager', () => {
2013
2819
  },
2014
2820
  };
2015
2821
 
2822
+ <<<<<<< HEAD
2016
2823
  webSocketManagerMock.emit('message', JSON.stringify(chatAssignedPayload));
2017
2824
 
2018
2825
  expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.ASSIGN);
2826
+ =======
2827
+ const task = taskManager.getTask(chatReservedPayload.data.interactionId);
2828
+ const taskEmitSpy = jest.spyOn(task, 'emit');
2019
2829
 
2020
- // 3. Chat task is ended with state 'new' to trigger cleanup
2830
+ webSocketManagerMock.emit('message', JSON.stringify(chatAssignedPayload));
2831
+
2832
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, task);
2833
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2834
+
2835
+ // 3. Chat task is ended with state 'new' to trigger cleanup
2021
2836
  const chatEndedPayload = {
2022
2837
  data: {
2023
2838
  ...chatReservedPayload.data,
2024
2839
  type: CC_EVENTS.CONTACT_ENDED,
2025
2840
  interaction: {mediaType: 'chat', state: 'new'}, // Change to 'new' state
2841
+ <<<<<<< HEAD
2026
2842
  wrapUpRequired: false,
2843
+ =======
2844
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2027
2845
  },
2028
2846
  };
2029
2847
 
@@ -2120,8 +2938,18 @@ describe('TaskManager', () => {
2120
2938
  expect(taskManager.getAllTasks()).toHaveProperty(task2Payload.data.interactionId);
2121
2939
  expect(taskManager.getAllTasks()).toHaveProperty(task3Payload.data.interactionId);
2122
2940
 
2941
+ <<<<<<< HEAD
2123
2942
  const task2 = taskManager.getTask(task2Payload.data.interactionId);
2124
2943
  const task2SendStateMachineEventSpy = task2.sendStateMachineEvent as jest.Mock;
2944
+ =======
2945
+ // Create spies for all tasks
2946
+ const task1EmitSpy = jest.spyOn(taskManager.getTask(task1Payload.data.interactionId), 'emit');
2947
+ const task2EmitSpy = jest.spyOn(taskManager.getTask(task2Payload.data.interactionId), 'emit');
2948
+ const task3EmitSpy = jest.spyOn(taskManager.getTask(task3Payload.data.interactionId), 'emit');
2949
+
2950
+ // Store reference to task2 before it gets removed
2951
+ const task2 = taskManager.getTask(task2Payload.data.interactionId);
2952
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2125
2953
 
2126
2954
  // End only the second task (chat task)
2127
2955
  const chatEndedPayload = {
@@ -2129,6 +2957,7 @@ describe('TaskManager', () => {
2129
2957
  ...task2Payload.data,
2130
2958
  type: CC_EVENTS.CONTACT_ENDED,
2131
2959
  interaction: {mediaType: 'chat', state: 'new'}, // Using 'new' to trigger cleanup
2960
+ <<<<<<< HEAD
2132
2961
  wrapUpRequired: false,
2133
2962
  },
2134
2963
  };
@@ -2141,6 +2970,17 @@ describe('TaskManager', () => {
2141
2970
  TaskEvent.CONTACT_ENDED
2142
2971
  );
2143
2972
  expect(firstEndEvent?.taskData).toEqual(chatEndedPayload.data);
2973
+ =======
2974
+ },
2975
+ };
2976
+
2977
+ webSocketManagerMock.emit('message', JSON.stringify(chatEndedPayload));
2978
+
2979
+ // Verify only task2 emitted TASK_END
2980
+ expect(task1EmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END);
2981
+ expect(task2EmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task2);
2982
+ expect(task3EmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END);
2983
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2144
2984
 
2145
2985
  // Verify task2 was removed from collection (since state was 'new')
2146
2986
  expect(taskManager.getTask(task2Payload.data.interactionId)).toBeUndefined();
@@ -2151,7 +2991,10 @@ describe('TaskManager', () => {
2151
2991
 
2152
2992
  // Store reference to task3 before we end it
2153
2993
  const task3 = taskManager.getTask(task3Payload.data.interactionId);
2994
+ <<<<<<< HEAD
2154
2995
  const task3SendStateMachineEventSpy = task3.sendStateMachineEvent as jest.Mock;
2996
+ =======
2997
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2155
2998
 
2156
2999
  // Now end task3 with a state that doesn't trigger cleanup
2157
3000
  const emailEndedPayload = {
@@ -2159,6 +3002,7 @@ describe('TaskManager', () => {
2159
3002
  ...task3Payload.data,
2160
3003
  type: CC_EVENTS.CONTACT_ENDED,
2161
3004
  interaction: {mediaType: 'email', state: 'connected'}, // Using 'connected' to NOT trigger cleanup
3005
+ <<<<<<< HEAD
2162
3006
  wrapUpRequired: true,
2163
3007
  },
2164
3008
  };
@@ -2171,6 +3015,15 @@ describe('TaskManager', () => {
2171
3015
  TaskEvent.CONTACT_ENDED
2172
3016
  );
2173
3017
  expect(secondEndEvent?.taskData).toEqual(emailEndedPayload.data);
3018
+ =======
3019
+ },
3020
+ };
3021
+
3022
+ webSocketManagerMock.emit('message', JSON.stringify(emailEndedPayload));
3023
+
3024
+ // Verify task3 emitted TASK_END
3025
+ expect(task3EmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task3);
3026
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2174
3027
 
2175
3028
  // Verify task3 is still in collection (since state was 'connected')
2176
3029
  expect(taskManager.getTask(task3Payload.data.interactionId)).toBeDefined();
@@ -2182,8 +3035,17 @@ describe('TaskManager', () => {
2182
3035
  it('should emit TRANSFER_SUCCESS event on AGENT_VTEAM_TRANSFERRED event', () => {
2183
3036
  // First create a task by emitting the initial payload
2184
3037
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
3038
+ <<<<<<< HEAD
2185
3039
  const task = taskManager.getTask(taskId);
2186
3040
  const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
3041
+ =======
3042
+
3043
+ // Get a reference to the task from taskCollection
3044
+ const task = taskManager.getTask(taskId);
3045
+
3046
+ // Now spy on the task's emit method
3047
+ const taskEmitSpy = jest.spyOn(task, 'emit');
3048
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2187
3049
 
2188
3050
  const vteamTransferredPayload = {
2189
3051
  data: {
@@ -2207,9 +3069,14 @@ describe('TaskManager', () => {
2207
3069
 
2208
3070
  webSocketManagerMock.emit('message', JSON.stringify(vteamTransferredPayload));
2209
3071
 
3072
+ <<<<<<< HEAD
2210
3073
  // Check that the state machine received the END event
2211
3074
  expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TRANSFER_SUCCESS);
2212
3075
  sendStateMachineEventSpy.mockRestore();
3076
+ =======
3077
+ // Check that task.emit was called with TASK_END event
3078
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
3079
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2213
3080
 
2214
3081
  // The task should still exist in the collection based on current implementation
2215
3082
  expect(taskManager.getTask(taskId)).toBeDefined();
@@ -2224,7 +3091,14 @@ describe('TaskManager', () => {
2224
3091
  },
2225
3092
  };
2226
3093
  const task = taskManager.getTask(taskId);
3094
+ <<<<<<< HEAD
2227
3095
  const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
3096
+ =======
3097
+ const updateSpy = jest.spyOn(task, 'updateTaskData').mockImplementation((data) => {
3098
+ task.data = {...(task.data || {}), ...(data || {})};
3099
+ return task;
3100
+ });
3101
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2228
3102
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2229
3103
  expect(sendStateMachineEventSpy).toHaveBeenCalled();
2230
3104
  const stateMachineEvent = sendStateMachineEventSpy.mock.calls.at(-1)?.[0];
@@ -2345,6 +3219,7 @@ describe('TaskManager', () => {
2345
3219
 
2346
3220
  describe('Conference event handling', () => {
2347
3221
  let task;
3222
+ <<<<<<< HEAD
2348
3223
 
2349
3224
  beforeEach(() => {
2350
3225
  task = {
@@ -2352,6 +3227,22 @@ describe('TaskManager', () => {
2352
3227
  emit: jest.fn(),
2353
3228
  updateTaskData: jest.fn(),
2354
3229
  sendStateMachineEvent: jest.fn(),
3230
+ =======
3231
+ const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
3232
+
3233
+ beforeEach(() => {
3234
+ // Set the agentId on taskManager before tests run
3235
+ taskManager.setAgentId(agentId);
3236
+
3237
+ task = {
3238
+ data: {interactionId: taskId},
3239
+ emit: jest.fn(),
3240
+ updateTaskData: jest.fn().mockImplementation((updatedData) => {
3241
+ // Mock the updateTaskData method to actually update task.data
3242
+ task.data = {...task.data, ...updatedData};
3243
+ return task;
3244
+ }),
3245
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2355
3246
  };
2356
3247
  taskManager.taskCollection[taskId] = task as any;
2357
3248
  });
@@ -2388,7 +3279,28 @@ describe('TaskManager', () => {
2388
3279
 
2389
3280
  it('sends PARTICIPANT_LEFT_CONFERENCE to state machine as PARTICIPANT_LEAVE', () => {
2390
3281
  const payload = {
3282
+ <<<<<<< HEAD
2391
3283
  data: {type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, interactionId: taskId},
3284
+ =======
3285
+ data: {
3286
+ type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE,
3287
+ interactionId: taskId,
3288
+ participantId: 'new-participant-123',
3289
+ participantType: 'agent',
3290
+ interaction: {
3291
+ participants: {
3292
+ [agentId]: {pType: 'Agent', hasLeft: false},
3293
+ 'new-participant-123': {pType: 'Agent', hasLeft: false},
3294
+ },
3295
+ media: {
3296
+ [taskId]: {
3297
+ mType: 'mainCall',
3298
+ participants: [agentId, 'new-participant-123'],
3299
+ },
3300
+ },
3301
+ },
3302
+ },
3303
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2392
3304
  };
2393
3305
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2394
3306
  expect(task.sendStateMachineEvent).toHaveBeenCalled();
@@ -2396,7 +3308,346 @@ describe('TaskManager', () => {
2396
3308
  expect(call.type).toBe(TaskEvent.PARTICIPANT_LEAVE);
2397
3309
  });
2398
3310
 
3311
+ <<<<<<< HEAD
2399
3312
  it('handles AGENT_CONSULT_CONFERENCING event without errors', () => {
3313
+ =======
3314
+ it('should call updateTaskData only once for PARTICIPANT_JOINED_CONFERENCE with pre-calculated isConferenceInProgress', () => {
3315
+ const payload = {
3316
+ data: {
3317
+ type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE,
3318
+ interactionId: taskId,
3319
+ participantId: 'new-agent-789',
3320
+ interaction: {
3321
+ participants: {
3322
+ [agentId]: {pType: 'Agent', hasLeft: false},
3323
+ 'agent-2': {pType: 'Agent', hasLeft: false},
3324
+ 'new-agent-789': {pType: 'Agent', hasLeft: false},
3325
+ 'customer-1': {pType: 'Customer', hasLeft: false},
3326
+ },
3327
+ media: {
3328
+ [taskId]: {
3329
+ mType: 'mainCall',
3330
+ participants: [agentId, 'agent-2', 'new-agent-789', 'customer-1'],
3331
+ },
3332
+ },
3333
+ },
3334
+ },
3335
+ };
3336
+
3337
+ const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
3338
+
3339
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3340
+
3341
+ // Verify updateTaskData was called exactly once
3342
+ expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
3343
+
3344
+ // Verify it was called with isConferenceInProgress already calculated
3345
+ expect(updateTaskDataSpy).toHaveBeenCalledWith(
3346
+ expect.objectContaining({
3347
+ participantId: 'new-agent-789',
3348
+ isConferenceInProgress: true, // 3 active agents
3349
+ })
3350
+ );
3351
+
3352
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
3353
+ });
3354
+
3355
+ describe('PARTICIPANT_LEFT_CONFERENCE event handling', () => {
3356
+ it('should call updateTaskData only once for PARTICIPANT_LEFT_CONFERENCE with pre-calculated isConferenceInProgress', () => {
3357
+ const payload = {
3358
+ data: {
3359
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
3360
+ interactionId: taskId,
3361
+ interaction: {
3362
+ participants: {
3363
+ [agentId]: {pType: 'Agent', hasLeft: false},
3364
+ 'agent-2': {pType: 'Agent', hasLeft: true}, // This agent left
3365
+ 'customer-1': {pType: 'Customer', hasLeft: false},
3366
+ },
3367
+ media: {
3368
+ [taskId]: {
3369
+ mType: 'mainCall',
3370
+ participants: [agentId, 'customer-1'], // agent-2 removed from participants
3371
+ },
3372
+ },
3373
+ },
3374
+ },
3375
+ };
3376
+
3377
+ const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
3378
+
3379
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3380
+
3381
+ // Verify updateTaskData was called exactly once
3382
+ expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
3383
+
3384
+ // Verify it was called with isConferenceInProgress already calculated
3385
+ expect(updateTaskDataSpy).toHaveBeenCalledWith(
3386
+ expect.objectContaining({
3387
+ isConferenceInProgress: false, // Only 1 active agent remains
3388
+ })
3389
+ );
3390
+
3391
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
3392
+ });
3393
+
3394
+ it('should emit TASK_PARTICIPANT_LEFT event when participant leaves conference', () => {
3395
+ const payload = {
3396
+ data: {
3397
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
3398
+ interactionId: taskId,
3399
+ interaction: {
3400
+ participants: {
3401
+ [agentId]: {
3402
+ hasLeft: false,
3403
+ },
3404
+ },
3405
+ },
3406
+ },
3407
+ };
3408
+
3409
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3410
+
3411
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
3412
+ });
3413
+
3414
+ it('should NOT remove task when agent is still in interaction', () => {
3415
+ const payload = {
3416
+ data: {
3417
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
3418
+ interactionId: taskId,
3419
+ interaction: {
3420
+ participants: {
3421
+ [agentId]: {
3422
+ hasLeft: false,
3423
+ },
3424
+ },
3425
+ },
3426
+ },
3427
+ };
3428
+
3429
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3430
+
3431
+ // Task should still exist in collection
3432
+ expect(taskManager.getTask(taskId)).toBeDefined();
3433
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
3434
+ });
3435
+
3436
+ it('should NOT remove task when agent left but is in main interaction', () => {
3437
+ const payload = {
3438
+ data: {
3439
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
3440
+ interactionId: taskId,
3441
+ interaction: {
3442
+ participants: {
3443
+ [agentId]: {
3444
+ hasLeft: true,
3445
+ },
3446
+ },
3447
+ media: {
3448
+ [taskId]: {
3449
+ mType: 'mainCall',
3450
+ participants: [agentId],
3451
+ },
3452
+ },
3453
+ },
3454
+ },
3455
+ };
3456
+
3457
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
3458
+
3459
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3460
+
3461
+ // Task should still exist - not removed
3462
+ expect(removeTaskSpy).not.toHaveBeenCalled();
3463
+ expect(taskManager.getTask(taskId)).toBeDefined();
3464
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
3465
+ });
3466
+
3467
+ it('should NOT remove task when agent left but is primary (owner)', () => {
3468
+ const payload = {
3469
+ data: {
3470
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
3471
+ interactionId: taskId,
3472
+ interaction: {
3473
+ participants: {
3474
+ [agentId]: {
3475
+ hasLeft: true,
3476
+ },
3477
+ },
3478
+ owner: agentId,
3479
+ media: {
3480
+ [taskId]: {
3481
+ mType: 'consultCall',
3482
+ participants: ['other-agent'],
3483
+ },
3484
+ },
3485
+ },
3486
+ },
3487
+ };
3488
+
3489
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
3490
+
3491
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3492
+
3493
+ // Task should still exist - not removed because agent is primary
3494
+ expect(removeTaskSpy).not.toHaveBeenCalled();
3495
+ expect(taskManager.getTask(taskId)).toBeDefined();
3496
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
3497
+ });
3498
+
3499
+ it('should remove task when agent left and is NOT in main interaction and is NOT primary', () => {
3500
+ const payload = {
3501
+ data: {
3502
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
3503
+ interactionId: taskId,
3504
+ interaction: {
3505
+ participants: {
3506
+ [agentId]: {
3507
+ hasLeft: true,
3508
+ },
3509
+ },
3510
+ owner: 'another-agent-id',
3511
+ media: {
3512
+ [taskId]: {
3513
+ mType: 'mainCall',
3514
+ participants: ['another-agent-id'],
3515
+ },
3516
+ },
3517
+ },
3518
+ },
3519
+ };
3520
+
3521
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
3522
+
3523
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3524
+
3525
+ // Task should be removed
3526
+ expect(removeTaskSpy).toHaveBeenCalled();
3527
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
3528
+ });
3529
+
3530
+ it('should remove task when agent is not in participants list', () => {
3531
+ const payload = {
3532
+ data: {
3533
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
3534
+ interactionId: taskId,
3535
+ interaction: {
3536
+ participants: {
3537
+ 'other-agent-id': {
3538
+ hasLeft: false,
3539
+ },
3540
+ },
3541
+ owner: 'another-agent-id',
3542
+ },
3543
+ },
3544
+ };
3545
+
3546
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
3547
+
3548
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3549
+
3550
+ // Task should be removed because agent is not in participants
3551
+ expect(removeTaskSpy).toHaveBeenCalled();
3552
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
3553
+ });
3554
+
3555
+ it('should update isConferenceInProgress based on remaining active agents', () => {
3556
+ const payload = {
3557
+ data: {
3558
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
3559
+ interactionId: taskId,
3560
+ interaction: {
3561
+ participants: {
3562
+ [agentId]: {
3563
+ hasLeft: false,
3564
+ pType: 'Agent',
3565
+ },
3566
+ 'agent-2': {
3567
+ hasLeft: false,
3568
+ pType: 'Agent',
3569
+ },
3570
+ 'customer-1': {
3571
+ hasLeft: false,
3572
+ pType: 'Customer',
3573
+ },
3574
+ },
3575
+ media: {
3576
+ [taskId]: {
3577
+ mType: 'mainCall',
3578
+ participants: [agentId, 'agent-2', 'customer-1'],
3579
+ },
3580
+ },
3581
+ },
3582
+ },
3583
+ };
3584
+
3585
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3586
+
3587
+ // isConferenceInProgress should be true (2 active agents)
3588
+ expect(task.data.isConferenceInProgress).toBe(true);
3589
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
3590
+ });
3591
+
3592
+ it('should set isConferenceInProgress to false when only one agent remains', () => {
3593
+ const payload = {
3594
+ data: {
3595
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
3596
+ interactionId: taskId,
3597
+ interaction: {
3598
+ participants: {
3599
+ [agentId]: {
3600
+ hasLeft: false,
3601
+ pType: 'Agent',
3602
+ },
3603
+ 'agent-2': {
3604
+ hasLeft: true,
3605
+ pType: 'Agent',
3606
+ },
3607
+ 'customer-1': {
3608
+ hasLeft: false,
3609
+ pType: 'Customer',
3610
+ },
3611
+ },
3612
+ media: {
3613
+ [taskId]: {
3614
+ mType: 'mainCall',
3615
+ participants: [agentId, 'customer-1'],
3616
+ },
3617
+ },
3618
+ },
3619
+ },
3620
+ };
3621
+
3622
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3623
+
3624
+ // isConferenceInProgress should be false (only 1 active agent)
3625
+ expect(task.data.isConferenceInProgress).toBe(false);
3626
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
3627
+ });
3628
+
3629
+ it('should handle participant left when no participants data exists', () => {
3630
+ const payload = {
3631
+ data: {
3632
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
3633
+ interactionId: taskId,
3634
+ interaction: {},
3635
+ },
3636
+ };
3637
+
3638
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
3639
+
3640
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3641
+
3642
+ // When no participants data exists, checkParticipantNotInInteraction returns true
3643
+ // Since agent won't be in main interaction either, task should be removed
3644
+ expect(removeTaskSpy).toHaveBeenCalled();
3645
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
3646
+ });
3647
+ });
3648
+
3649
+ it('should handle PARTICIPANT_LEFT_CONFERENCE_FAILED event', () => {
3650
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2400
3651
  const payload = {
2401
3652
  data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCING, interactionId: taskId},
2402
3653
  };
@@ -2416,7 +3667,11 @@ describe('TaskManager', () => {
2416
3667
 
2417
3668
  it('only routes conference events to matching tasks', () => {
2418
3669
  const otherTaskId = 'other-task-id';
3670
+ <<<<<<< HEAD
2419
3671
  const otherTask: any = {
3672
+ =======
3673
+ const otherTask = {
3674
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2420
3675
  data: {interactionId: otherTaskId},
2421
3676
  emit: jest.fn(),
2422
3677
  updateTaskData: jest.fn(),
@@ -2429,6 +3684,7 @@ describe('TaskManager', () => {
2429
3684
  };
2430
3685
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2431
3686
 
3687
+ <<<<<<< HEAD
2432
3688
  expect(task.sendStateMachineEvent).toHaveBeenCalled();
2433
3689
  expect(otherTask.sendStateMachineEvent).not.toHaveBeenCalled();
2434
3690
  });
@@ -2460,6 +3716,939 @@ describe('TaskManager', () => {
2460
3716
  });
2461
3717
 
2462
3718
  sendStateMachineEventSpy.mockRestore();
3719
+ =======
3720
+ // Only the matching task should be updated
3721
+ expect(task.data.isConferencing).toBe(true);
3722
+ expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
3723
+
3724
+ // Other task should not be affected
3725
+ expect(otherTask.data.isConferencing).toBeUndefined();
3726
+ expect(otherTask.emit).not.toHaveBeenCalled();
3727
+ });
3728
+ });
3729
+
3730
+ describe('handleTaskCleanup - stage changes', () => {
3731
+ const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
3732
+
3733
+ beforeEach(() => {
3734
+ taskManager.setAgentId(agentId);
3735
+ });
3736
+
3737
+ it('should remove OUTDIAL task on CONTACT_ENDED when current agent is NOT in agentsPendingWrapUp', () => {
3738
+ const task = taskManager.getTask(taskId);
3739
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
3740
+ task.data = {
3741
+ ...task.data,
3742
+ ...newData,
3743
+ interaction: {
3744
+ ...task.data.interaction,
3745
+ outboundType: 'OUTDIAL',
3746
+ state: 'new',
3747
+ mediaType: 'telephony',
3748
+ },
3749
+ agentsPendingWrapUp: ['different-agent-123'], // Current agent not in the list
3750
+ };
3751
+ return task;
3752
+ });
3753
+ task.unregisterWebCallListeners = jest.fn();
3754
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
3755
+
3756
+ const payload = {
3757
+ data: {
3758
+ type: CC_EVENTS.CONTACT_ENDED,
3759
+ interactionId: taskId,
3760
+ interaction: {
3761
+ outboundType: 'OUTDIAL',
3762
+ state: 'new',
3763
+ mediaType: 'telephony',
3764
+ },
3765
+ agentsPendingWrapUp: ['different-agent-123'], // Current agent not in the list
3766
+ },
3767
+ };
3768
+
3769
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3770
+
3771
+ expect(removeTaskSpy).toHaveBeenCalled();
3772
+ expect(taskManager.getTask(taskId)).toBeUndefined();
3773
+ });
3774
+
3775
+ it('should NOT remove OUTDIAL task on CONTACT_ENDED when current agent IS in agentsPendingWrapUp', () => {
3776
+ const task = taskManager.getTask(taskId);
3777
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
3778
+ task.data = {
3779
+ ...task.data,
3780
+ ...newData,
3781
+ interaction: {
3782
+ ...task.data.interaction,
3783
+ outboundType: 'OUTDIAL',
3784
+ state: 'new',
3785
+ mediaType: 'telephony',
3786
+ },
3787
+ agentsPendingWrapUp: [agentId, 'other-agent-456'], // Current agent IS in the list
3788
+ };
3789
+ return task;
3790
+ });
3791
+ task.unregisterWebCallListeners = jest.fn();
3792
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
3793
+
3794
+ const payload = {
3795
+ data: {
3796
+ type: CC_EVENTS.CONTACT_ENDED,
3797
+ interactionId: taskId,
3798
+ interaction: {
3799
+ outboundType: 'OUTDIAL',
3800
+ state: 'new',
3801
+ mediaType: 'telephony',
3802
+ },
3803
+ agentsPendingWrapUp: [agentId, 'other-agent-456'], // Current agent IS in the list
3804
+ },
3805
+ };
3806
+
3807
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3808
+
3809
+ expect(removeTaskSpy).not.toHaveBeenCalled();
3810
+ expect(taskManager.getTask(taskId)).toBeDefined();
3811
+ });
3812
+
3813
+ it('should remove OUTDIAL task when needsWrapUp is false and task is outdial', () => {
3814
+ const task = taskManager.getTask(taskId);
3815
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
3816
+ task.data = {
3817
+ ...task.data,
3818
+ ...newData,
3819
+ interaction: {
3820
+ ...task.data.interaction,
3821
+ outboundType: 'OUTDIAL',
3822
+ state: 'WRAPUP', // Not 'new' state
3823
+ mediaType: 'telephony',
3824
+ },
3825
+ agentsPendingWrapUp: [], // No agents pending wrap-up
3826
+ };
3827
+ return task;
3828
+ });
3829
+ task.unregisterWebCallListeners = jest.fn();
3830
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
3831
+
3832
+ const payload = {
3833
+ data: {
3834
+ type: CC_EVENTS.CONTACT_ENDED,
3835
+ interactionId: taskId,
3836
+ interaction: {
3837
+ outboundType: 'OUTDIAL',
3838
+ state: 'WRAPUP', // Not 'new' state
3839
+ mediaType: 'telephony',
3840
+ },
3841
+ agentsPendingWrapUp: [], // No agents pending wrap-up
3842
+ },
3843
+ };
3844
+
3845
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3846
+
3847
+ expect(removeTaskSpy).toHaveBeenCalled();
3848
+ expect(taskManager.getTask(taskId)).toBeUndefined();
3849
+ });
3850
+
3851
+ it('should remove OUTDIAL task when needsWrapUp is false (agentsPendingWrapUp is undefined)', () => {
3852
+ const task = taskManager.getTask(taskId);
3853
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
3854
+ task.data = {
3855
+ ...task.data,
3856
+ ...newData,
3857
+ interaction: {
3858
+ ...task.data.interaction,
3859
+ outboundType: 'OUTDIAL',
3860
+ state: 'WRAPUP',
3861
+ mediaType: 'telephony',
3862
+ },
3863
+ agentsPendingWrapUp: undefined, // No agentsPendingWrapUp field
3864
+ };
3865
+ return task;
3866
+ });
3867
+ task.unregisterWebCallListeners = jest.fn();
3868
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
3869
+
3870
+ const payload = {
3871
+ data: {
3872
+ type: CC_EVENTS.CONTACT_ENDED,
3873
+ interactionId: taskId,
3874
+ interaction: {
3875
+ outboundType: 'OUTDIAL',
3876
+ state: 'WRAPUP',
3877
+ mediaType: 'telephony',
3878
+ },
3879
+ // agentsPendingWrapUp not included
3880
+ },
3881
+ };
3882
+
3883
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3884
+
3885
+ expect(removeTaskSpy).toHaveBeenCalled();
3886
+ expect(taskManager.getTask(taskId)).toBeUndefined();
3887
+ });
3888
+
3889
+ it('should NOT remove OUTDIAL task when needsWrapUp is true (current agent in agentsPendingWrapUp) even if state is WRAPUP', () => {
3890
+ const task = taskManager.getTask(taskId);
3891
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
3892
+ task.data = {
3893
+ ...task.data,
3894
+ ...newData,
3895
+ interaction: {
3896
+ ...task.data.interaction,
3897
+ outboundType: 'OUTDIAL',
3898
+ state: 'WRAPUP',
3899
+ mediaType: 'telephony',
3900
+ },
3901
+ agentsPendingWrapUp: [agentId], // Current agent needs wrap-up
3902
+ };
3903
+ return task;
3904
+ });
3905
+ task.unregisterWebCallListeners = jest.fn();
3906
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
3907
+
3908
+ const payload = {
3909
+ data: {
3910
+ type: CC_EVENTS.CONTACT_ENDED,
3911
+ interactionId: taskId,
3912
+ interaction: {
3913
+ outboundType: 'OUTDIAL',
3914
+ state: 'WRAPUP',
3915
+ mediaType: 'telephony',
3916
+ },
3917
+ agentsPendingWrapUp: [agentId], // Current agent needs wrap-up
3918
+ },
3919
+ };
3920
+
3921
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3922
+
3923
+ expect(removeTaskSpy).not.toHaveBeenCalled();
3924
+ expect(taskManager.getTask(taskId)).toBeDefined();
3925
+ });
3926
+
3927
+ it('should remove non-OUTDIAL task when state is new regardless of agentsPendingWrapUp', () => {
3928
+ const task = taskManager.getTask(taskId);
3929
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
3930
+ task.data = {
3931
+ ...task.data,
3932
+ ...newData,
3933
+ interaction: {
3934
+ ...task.data.interaction,
3935
+ outboundType: 'PREVIEW', // Not OUTDIAL
3936
+ state: 'new',
3937
+ mediaType: 'telephony',
3938
+ },
3939
+ agentsPendingWrapUp: [agentId],
3940
+ };
3941
+ return task;
3942
+ });
3943
+ task.unregisterWebCallListeners = jest.fn();
3944
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
3945
+
3946
+ const payload = {
3947
+ data: {
3948
+ type: CC_EVENTS.CONTACT_ENDED,
3949
+ interactionId: taskId,
3950
+ interaction: {
3951
+ outboundType: 'PREVIEW',
3952
+ state: 'new',
3953
+ mediaType: 'telephony',
3954
+ },
3955
+ agentsPendingWrapUp: [agentId],
3956
+ },
3957
+ };
3958
+
3959
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3960
+
3961
+ expect(removeTaskSpy).toHaveBeenCalled();
3962
+ expect(taskManager.getTask(taskId)).toBeUndefined();
3963
+ });
3964
+
3965
+ it('should handle agentsPendingWrapUp with multiple agents correctly - remove if current agent not in list', () => {
3966
+ const task = taskManager.getTask(taskId);
3967
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
3968
+ task.data = {
3969
+ ...task.data,
3970
+ ...newData,
3971
+ interaction: {
3972
+ ...task.data.interaction,
3973
+ outboundType: 'OUTDIAL',
3974
+ state: 'new',
3975
+ mediaType: 'telephony',
3976
+ },
3977
+ agentsPendingWrapUp: ['agent-1', 'agent-2', 'agent-3'], // Current agent not in the list
3978
+ };
3979
+ return task;
3980
+ });
3981
+ task.unregisterWebCallListeners = jest.fn();
3982
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
3983
+
3984
+ const payload = {
3985
+ data: {
3986
+ type: CC_EVENTS.CONTACT_ENDED,
3987
+ interactionId: taskId,
3988
+ interaction: {
3989
+ outboundType: 'OUTDIAL',
3990
+ state: 'new',
3991
+ mediaType: 'telephony',
3992
+ },
3993
+ agentsPendingWrapUp: ['agent-1', 'agent-2', 'agent-3'],
3994
+ },
3995
+ };
3996
+
3997
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
3998
+
3999
+ expect(removeTaskSpy).toHaveBeenCalled();
4000
+ expect(taskManager.getTask(taskId)).toBeUndefined();
4001
+ });
4002
+
4003
+ it('should handle agentsPendingWrapUp with multiple agents correctly - keep if current agent is in list', () => {
4004
+ const task = taskManager.getTask(taskId);
4005
+ task.updateTaskData = jest.fn().mockImplementation((newData) => {
4006
+ task.data = {
4007
+ ...task.data,
4008
+ ...newData,
4009
+ interaction: {
4010
+ ...task.data.interaction,
4011
+ outboundType: 'OUTDIAL',
4012
+ state: 'new',
4013
+ mediaType: 'telephony',
4014
+ },
4015
+ agentsPendingWrapUp: ['agent-1', agentId, 'agent-3'], // Current agent IS in the list
4016
+ };
4017
+ return task;
4018
+ });
4019
+ task.unregisterWebCallListeners = jest.fn();
4020
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
4021
+
4022
+ const payload = {
4023
+ data: {
4024
+ type: CC_EVENTS.CONTACT_ENDED,
4025
+ interactionId: taskId,
4026
+ interaction: {
4027
+ outboundType: 'OUTDIAL',
4028
+ state: 'new',
4029
+ mediaType: 'telephony',
4030
+ },
4031
+ agentsPendingWrapUp: ['agent-1', agentId, 'agent-3'],
4032
+ },
4033
+ };
4034
+
4035
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
4036
+
4037
+ expect(removeTaskSpy).not.toHaveBeenCalled();
4038
+ expect(taskManager.getTask(taskId)).toBeDefined();
4039
+ });
4040
+ });
4041
+
4042
+ describe('CONTACT_MERGED event handling', () => {
4043
+ let task;
4044
+ let taskEmitSpy;
4045
+ let managerEmitSpy;
4046
+
4047
+ beforeEach(() => {
4048
+ // Create initial task
4049
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
4050
+ task = taskManager.getTask(taskId);
4051
+ taskEmitSpy = jest.spyOn(task, 'emit');
4052
+ managerEmitSpy = jest.spyOn(taskManager, 'emit');
4053
+ });
4054
+
4055
+ it('should update existing task data and emit TASK_MERGED event when CONTACT_MERGED is received', () => {
4056
+ const mergedPayload = {
4057
+ data: {
4058
+ type: CC_EVENTS.CONTACT_MERGED,
4059
+ interactionId: taskId,
4060
+ agentId: taskDataMock.agentId,
4061
+ interaction: {
4062
+ ...taskDataMock.interaction,
4063
+ state: 'merged',
4064
+ customField: 'updated-value',
4065
+ },
4066
+ },
4067
+ };
4068
+
4069
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
4070
+
4071
+ const updatedTask = taskManager.getTask(taskId);
4072
+ expect(updatedTask).toBeDefined();
4073
+ expect(updatedTask.data.interaction.customField).toBe('updated-value');
4074
+ expect(updatedTask.data.interaction.state).toBe('merged');
4075
+ expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, updatedTask);
4076
+ });
4077
+
4078
+ it('should create new task when CONTACT_MERGED is received for non-existing task', () => {
4079
+ const newMergedTaskId = 'new-merged-task-id';
4080
+ const mergedPayload = {
4081
+ data: {
4082
+ type: CC_EVENTS.CONTACT_MERGED,
4083
+ interactionId: newMergedTaskId,
4084
+ agentId: taskDataMock.agentId,
4085
+ interaction: {
4086
+ mediaType: 'telephony',
4087
+ state: 'merged',
4088
+ participants: {
4089
+ [taskDataMock.agentId]: {
4090
+ isWrapUp: false,
4091
+ hasJoined: true,
4092
+ },
4093
+ },
4094
+ },
4095
+ },
4096
+ };
4097
+
4098
+ // Verify task doesn't exist before
4099
+ expect(taskManager.getTask(newMergedTaskId)).toBeUndefined();
4100
+
4101
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
4102
+
4103
+ // Verify task was created
4104
+ const newTask = taskManager.getTask(newMergedTaskId);
4105
+ expect(newTask).toBeDefined();
4106
+ expect(newTask.data.interactionId).toBe(newMergedTaskId);
4107
+ expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, newTask);
4108
+ });
4109
+
4110
+ it('should remove child task when childInteractionId is present in CONTACT_MERGED', () => {
4111
+ const childTaskId = 'child-task-id';
4112
+ const parentTaskId = 'parent-task-id';
4113
+
4114
+ // Create child task
4115
+ const childPayload = {
4116
+ data: {
4117
+ type: CC_EVENTS.AGENT_CONTACT_RESERVED,
4118
+ interactionId: childTaskId,
4119
+ agentId: taskDataMock.agentId,
4120
+ interaction: {mediaType: 'telephony'},
4121
+ },
4122
+ };
4123
+ webSocketManagerMock.emit('message', JSON.stringify(childPayload));
4124
+
4125
+ // Verify child task exists
4126
+ expect(taskManager.getTask(childTaskId)).toBeDefined();
4127
+
4128
+ // Create parent task
4129
+ const parentPayload = {
4130
+ data: {
4131
+ type: CC_EVENTS.AGENT_CONTACT_RESERVED,
4132
+ interactionId: parentTaskId,
4133
+ agentId: taskDataMock.agentId,
4134
+ interaction: {mediaType: 'telephony'},
4135
+ },
4136
+ };
4137
+ webSocketManagerMock.emit('message', JSON.stringify(parentPayload));
4138
+
4139
+ // Send CONTACT_MERGED with childInteractionId
4140
+ const mergedPayload = {
4141
+ data: {
4142
+ type: CC_EVENTS.CONTACT_MERGED,
4143
+ interactionId: parentTaskId,
4144
+ childInteractionId: childTaskId,
4145
+ agentId: taskDataMock.agentId,
4146
+ interaction: {
4147
+ mediaType: 'telephony',
4148
+ state: 'merged',
4149
+ },
4150
+ },
4151
+ };
4152
+
4153
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
4154
+
4155
+ // Verify child task was removed
4156
+ expect(taskManager.getTask(childTaskId)).toBeUndefined();
4157
+
4158
+ // Verify parent task still exists
4159
+ expect(taskManager.getTask(parentTaskId)).toBeDefined();
4160
+
4161
+ // Verify TASK_MERGED event was emitted
4162
+ expect(managerEmitSpy).toHaveBeenCalledWith(
4163
+ TASK_EVENTS.TASK_MERGED,
4164
+ expect.objectContaining({
4165
+ data: expect.objectContaining({
4166
+ interactionId: parentTaskId,
4167
+ }),
4168
+ })
4169
+ );
4170
+ });
4171
+
4172
+ it('should handle CONTACT_MERGED with EP-DN participant correctly', () => {
4173
+ const epdnTaskId = 'epdn-merged-task';
4174
+ const mergedPayload = {
4175
+ data: {
4176
+ type: CC_EVENTS.CONTACT_MERGED,
4177
+ interactionId: epdnTaskId,
4178
+ agentId: taskDataMock.agentId,
4179
+ interaction: {
4180
+ mediaType: 'telephony',
4181
+ state: 'merged',
4182
+ participants: {
4183
+ [taskDataMock.agentId]: {
4184
+ type: 'Agent',
4185
+ isWrapUp: false,
4186
+ hasJoined: true,
4187
+ },
4188
+ 'epdn-participant': {
4189
+ type: 'EpDn',
4190
+ epId: 'entry-point-123',
4191
+ isWrapUp: false,
4192
+ },
4193
+ },
4194
+ },
4195
+ },
4196
+ };
4197
+
4198
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
4199
+
4200
+ const mergedTask = taskManager.getTask(epdnTaskId);
4201
+ expect(mergedTask).toBeDefined();
4202
+ expect(mergedTask.data.interaction.participants['epdn-participant']).toBeDefined();
4203
+ expect(mergedTask.data.interaction.participants['epdn-participant'].type).toBe('EpDn');
4204
+ expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, mergedTask);
4205
+ });
4206
+
4207
+ it('should not affect other tasks when CONTACT_MERGED is received', () => {
4208
+ const otherTaskId = 'other-unrelated-task';
4209
+ const otherPayload = {
4210
+ data: {
4211
+ type: CC_EVENTS.AGENT_CONTACT_RESERVED,
4212
+ interactionId: otherTaskId,
4213
+ agentId: taskDataMock.agentId,
4214
+ interaction: {mediaType: 'chat'},
4215
+ },
4216
+ };
4217
+ webSocketManagerMock.emit('message', JSON.stringify(otherPayload));
4218
+
4219
+ const otherTask = taskManager.getTask(otherTaskId);
4220
+ const otherTaskEmitSpy = jest.spyOn(otherTask, 'emit');
4221
+
4222
+ // Send CONTACT_MERGED for the original task
4223
+ const mergedPayload = {
4224
+ data: {
4225
+ type: CC_EVENTS.CONTACT_MERGED,
4226
+ interactionId: taskId,
4227
+ agentId: taskDataMock.agentId,
4228
+ interaction: {
4229
+ mediaType: 'telephony',
4230
+ state: 'merged',
4231
+ },
4232
+ },
4233
+ };
4234
+
4235
+ webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
4236
+
4237
+ // Verify other task was not affected
4238
+ expect(otherTaskEmitSpy).not.toHaveBeenCalled();
4239
+ expect(otherTask.data.interaction.mediaType).toBe('chat');
4240
+
4241
+ // Verify original task was updated
4242
+ expect(managerEmitSpy).toHaveBeenCalledWith(
4243
+ TASK_EVENTS.TASK_MERGED,
4244
+ expect.objectContaining({
4245
+ data: expect.objectContaining({
4246
+ interactionId: taskId,
4247
+ }),
4248
+ })
4249
+ );
4250
+ });
4251
+ });
4252
+
4253
+ describe('Campaign Preview Reservation', () => {
4254
+ it('should create a task and emit TASK_CAMPAIGN_PREVIEW_RESERVATION when AgentOfferCampaignReservation is received', () => {
4255
+ const campaignPayload = {
4256
+ data: {
4257
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
4258
+ interactionId: 'campaign-interaction-123',
4259
+ agentId: taskDataMock.agentId,
4260
+ orgId: taskDataMock.orgId,
4261
+ trackingId: 'campaign-tracking-456',
4262
+ interaction: {
4263
+ mediaType: 'telephony',
4264
+ callProcessingDetails: {
4265
+ campaignId: 'campaign-789',
4266
+ },
4267
+ },
4268
+ },
4269
+ };
4270
+
4271
+ const managerEmitSpy = jest.spyOn(taskManager, 'emit');
4272
+
4273
+ webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
4274
+
4275
+ // Should emit with a task object (not raw data)
4276
+ expect(managerEmitSpy).toHaveBeenCalledWith(
4277
+ TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION,
4278
+ expect.objectContaining({
4279
+ data: expect.objectContaining({
4280
+ interactionId: 'campaign-interaction-123',
4281
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
4282
+ wrapUpRequired: false,
4283
+ isAutoAnswering: false,
4284
+ }),
4285
+ })
4286
+ );
4287
+
4288
+ // Task should be in the collection so subsequent events (e.g. AGENT_CONTACT_ASSIGNED) can find it
4289
+ expect(taskManager['taskCollection']['campaign-interaction-123']).toBeDefined();
4290
+ });
4291
+
4292
+ it('should re-key task and emit TASK_ASSIGNED when AgentContactAssigned uses a new interactionId', () => {
4293
+ const reservationInteractionId = 'campaign-reservation-id';
4294
+ const assignedInteractionId = 'campaign-assigned-id';
4295
+
4296
+ webSocketManagerMock.emit(
4297
+ 'message',
4298
+ JSON.stringify({
4299
+ data: {
4300
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
4301
+ interactionId: reservationInteractionId,
4302
+ agentId: taskDataMock.agentId,
4303
+ orgId: taskDataMock.orgId,
4304
+ trackingId: 'campaign-tracking-456',
4305
+ interaction: {
4306
+ mediaType: 'telephony',
4307
+ callProcessingDetails: {campaignId: 'campaign-789'},
4308
+ },
4309
+ },
4310
+ })
4311
+ );
4312
+
4313
+ const taskBeforeAssign = taskManager['taskCollection'][reservationInteractionId];
4314
+ expect(taskBeforeAssign).toBeDefined();
4315
+
4316
+ const taskEmitSpy = jest.spyOn(taskBeforeAssign, 'emit');
4317
+
4318
+ webSocketManagerMock.emit(
4319
+ 'message',
4320
+ JSON.stringify({
4321
+ data: {
4322
+ type: CC_EVENTS.AGENT_CONTACT_ASSIGNED,
4323
+ interactionId: assignedInteractionId,
4324
+ reservationInteractionId,
4325
+ agentId: taskDataMock.agentId,
4326
+ orgId: taskDataMock.orgId,
4327
+ trackingId: 'campaign-tracking-assigned',
4328
+ interaction: {mediaType: 'telephony', state: 'connected'},
4329
+ },
4330
+ })
4331
+ );
4332
+
4333
+ expect(taskManager['taskCollection'][reservationInteractionId]).toBeUndefined();
4334
+ expect(taskManager['taskCollection'][assignedInteractionId]).toBe(taskBeforeAssign);
4335
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, taskBeforeAssign);
4336
+
4337
+ taskEmitSpy.mockRestore();
4338
+ });
4339
+
4340
+ it('should not emit TASK_INCOMING for campaign preview reservation when incoming WebRTC call arrives', () => {
4341
+ const campaignPayload = {
4342
+ data: {
4343
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
4344
+ interactionId: 'campaign-interaction-123',
4345
+ agentId: taskDataMock.agentId,
4346
+ orgId: taskDataMock.orgId,
4347
+ trackingId: 'campaign-tracking-456',
4348
+ interaction: {
4349
+ mediaType: 'telephony',
4350
+ callProcessingDetails: {
4351
+ campaignId: 'campaign-789',
4352
+ },
4353
+ },
4354
+ },
4355
+ };
4356
+
4357
+ // Remove the default task so only the campaign preview task is in the collection
4358
+ delete taskManager['taskCollection'][taskId];
4359
+
4360
+ // Create campaign preview task via the reservation event
4361
+ webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
4362
+
4363
+ const managerEmitSpy = jest.spyOn(taskManager, 'emit');
4364
+
4365
+ // Simulate an incoming WebRTC call
4366
+ const incomingCallCb = onSpy.mock.calls[0][1];
4367
+ incomingCallCb(mockCall);
4368
+
4369
+ // TASK_INCOMING should NOT be emitted because the only telephony task is a campaign preview
4370
+ expect(managerEmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, expect.anything());
4371
+ });
4372
+
4373
+ it('should update existing task when AgentOfferCampaignReservation is received for known interactionId', () => {
4374
+ const campaignPayload = {
4375
+ data: {
4376
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
4377
+ interactionId: 'campaign-interaction-123',
4378
+ agentId: taskDataMock.agentId,
4379
+ orgId: taskDataMock.orgId,
4380
+ trackingId: 'campaign-tracking-456',
4381
+ interaction: {
4382
+ mediaType: 'telephony',
4383
+ callProcessingDetails: {
4384
+ campaignId: 'campaign-789',
4385
+ },
4386
+ },
4387
+ },
4388
+ };
4389
+
4390
+ // Send the first reservation to create the task
4391
+ webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
4392
+
4393
+ const managerEmitSpy = jest.spyOn(taskManager, 'emit');
4394
+
4395
+ // Send a second reservation for the same interactionId
4396
+ webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
4397
+
4398
+ expect(managerEmitSpy).toHaveBeenCalledWith(
4399
+ TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION,
4400
+ expect.objectContaining({
4401
+ data: expect.objectContaining({
4402
+ interactionId: 'campaign-interaction-123',
4403
+ }),
4404
+ })
4405
+ );
4406
+ });
4407
+
4408
+ it('should emit TASK_CAMPAIGN_CONTACT_UPDATED and NOT remove task when CampaignContactUpdated is received', () => {
4409
+ const campaignInteractionId = 'campaign-interaction-123';
4410
+
4411
+ // First create a campaign preview task
4412
+ const reservationPayload = {
4413
+ data: {
4414
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
4415
+ interactionId: campaignInteractionId,
4416
+ agentId: taskDataMock.agentId,
4417
+ orgId: taskDataMock.orgId,
4418
+ trackingId: 'campaign-tracking-456',
4419
+ interaction: {
4420
+ mediaType: 'telephony',
4421
+ callProcessingDetails: {
4422
+ campaignId: 'campaign-789',
4423
+ },
4424
+ },
4425
+ },
4426
+ };
4427
+
4428
+ webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
4429
+
4430
+ // Verify task exists in collection
4431
+ const task = taskManager['taskCollection'][campaignInteractionId];
4432
+ expect(task).toBeDefined();
4433
+
4434
+ const taskEmitSpy = jest.spyOn(task, 'emit');
4435
+
4436
+ // Now send CampaignContactUpdated
4437
+ const campaignContactUpdatedPayload = {
4438
+ data: {
4439
+ type: CC_EVENTS.CAMPAIGN_CONTACT_UPDATED,
4440
+ interactionId: campaignInteractionId,
4441
+ agentId: taskDataMock.agentId,
4442
+ orgId: taskDataMock.orgId,
4443
+ interaction: {
4444
+ mediaType: 'telephony',
4445
+ state: 'new',
4446
+ callProcessingDetails: {
4447
+ campaignId: 'campaign-789',
4448
+ },
4449
+ },
4450
+ },
4451
+ };
4452
+
4453
+ webSocketManagerMock.emit('message', JSON.stringify(campaignContactUpdatedPayload));
4454
+
4455
+ // Task should still exist in collection (not removed — non-terminal event)
4456
+ expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
4457
+
4458
+ // TASK_CAMPAIGN_CONTACT_UPDATED should have been emitted
4459
+ expect(taskEmitSpy).toHaveBeenCalledWith(
4460
+ TASK_EVENTS.TASK_CAMPAIGN_CONTACT_UPDATED,
4461
+ expect.objectContaining({
4462
+ data: expect.objectContaining({
4463
+ interactionId: campaignInteractionId,
4464
+ }),
4465
+ })
4466
+ );
4467
+
4468
+ // TASK_END should NOT have been emitted
4469
+ expect(taskEmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.anything());
4470
+ });
4471
+
4472
+ it('should emit TASK_CAMPAIGN_PREVIEW_ACCEPT_FAILED when CampaignPreviewAcceptFailed is received', () => {
4473
+ const campaignInteractionId = 'campaign-interaction-123';
4474
+
4475
+ // First create a campaign preview task
4476
+ const reservationPayload = {
4477
+ data: {
4478
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
4479
+ interactionId: campaignInteractionId,
4480
+ agentId: taskDataMock.agentId,
4481
+ orgId: taskDataMock.orgId,
4482
+ trackingId: 'campaign-tracking-456',
4483
+ interaction: {
4484
+ mediaType: 'telephony',
4485
+ callProcessingDetails: {
4486
+ campaignId: 'campaign-789',
4487
+ },
4488
+ },
4489
+ },
4490
+ };
4491
+
4492
+ webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
4493
+
4494
+ const task = taskManager['taskCollection'][campaignInteractionId];
4495
+ expect(task).toBeDefined();
4496
+
4497
+ const taskEmitSpy = jest.spyOn(task, 'emit');
4498
+
4499
+ const failPayload = {
4500
+ data: {
4501
+ type: CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED,
4502
+ interactionId: campaignInteractionId,
4503
+ campaignId: 'campaign-789',
4504
+ reason: 'INTERNAL_ERROR',
4505
+ },
4506
+ };
4507
+
4508
+ webSocketManagerMock.emit('message', JSON.stringify(failPayload));
4509
+
4510
+ expect(taskEmitSpy).toHaveBeenCalledWith(
4511
+ TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_ACCEPT_FAILED,
4512
+ expect.objectContaining({
4513
+ data: expect.objectContaining({interactionId: campaignInteractionId}),
4514
+ })
4515
+ );
4516
+ // Task should still exist (failure is non-terminal)
4517
+ expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
4518
+
4519
+ // Failure payload (reason, campaignId) should be merged into task.data
4520
+ expect(task.data.reason).toBe('INTERNAL_ERROR');
4521
+
4522
+ // Original reservation data must be preserved
4523
+ expect(task.data.interaction).toBeDefined();
4524
+ expect(task.data.interaction.callProcessingDetails.campaignId).toBe('campaign-789');
4525
+ });
4526
+
4527
+ it('should emit TASK_CAMPAIGN_PREVIEW_SKIP_FAILED when CampaignPreviewSkipFailed is received', () => {
4528
+ const campaignInteractionId = 'campaign-interaction-123';
4529
+
4530
+ const reservationPayload = {
4531
+ data: {
4532
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
4533
+ interactionId: campaignInteractionId,
4534
+ agentId: taskDataMock.agentId,
4535
+ orgId: taskDataMock.orgId,
4536
+ trackingId: 'campaign-tracking-456',
4537
+ interaction: {
4538
+ mediaType: 'telephony',
4539
+ callProcessingDetails: {
4540
+ campaignId: 'campaign-789',
4541
+ },
4542
+ },
4543
+ },
4544
+ };
4545
+
4546
+ webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
4547
+
4548
+ const task = taskManager['taskCollection'][campaignInteractionId];
4549
+ expect(task).toBeDefined();
4550
+
4551
+ const taskEmitSpy = jest.spyOn(task, 'emit');
4552
+
4553
+ const failPayload = {
4554
+ data: {
4555
+ type: CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED,
4556
+ interactionId: campaignInteractionId,
4557
+ campaignId: 'campaign-789',
4558
+ reason: 'INTERNAL_ERROR',
4559
+ },
4560
+ };
4561
+
4562
+ webSocketManagerMock.emit('message', JSON.stringify(failPayload));
4563
+
4564
+ expect(taskEmitSpy).toHaveBeenCalledWith(
4565
+ TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_SKIP_FAILED,
4566
+ expect.objectContaining({
4567
+ data: expect.objectContaining({interactionId: campaignInteractionId}),
4568
+ })
4569
+ );
4570
+ expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
4571
+
4572
+ // Failure payload (reason) should be merged into task.data
4573
+ expect(task.data.reason).toBe('INTERNAL_ERROR');
4574
+
4575
+ // Original reservation data must be preserved
4576
+ expect(task.data.interaction).toBeDefined();
4577
+ expect(task.data.interaction.callProcessingDetails.campaignId).toBe('campaign-789');
4578
+ });
4579
+
4580
+ it('should emit TASK_CAMPAIGN_PREVIEW_REMOVE_FAILED when CampaignPreviewRemoveFailed is received', () => {
4581
+ const campaignInteractionId = 'campaign-interaction-123';
4582
+
4583
+ const reservationPayload = {
4584
+ data: {
4585
+ type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
4586
+ interactionId: campaignInteractionId,
4587
+ agentId: taskDataMock.agentId,
4588
+ orgId: taskDataMock.orgId,
4589
+ trackingId: 'campaign-tracking-456',
4590
+ interaction: {
4591
+ mediaType: 'telephony',
4592
+ callProcessingDetails: {
4593
+ campaignId: 'campaign-789',
4594
+ },
4595
+ },
4596
+ },
4597
+ };
4598
+
4599
+ webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
4600
+
4601
+ const task = taskManager['taskCollection'][campaignInteractionId];
4602
+ expect(task).toBeDefined();
4603
+
4604
+ const taskEmitSpy = jest.spyOn(task, 'emit');
4605
+
4606
+ const failPayload = {
4607
+ data: {
4608
+ type: CC_EVENTS.CAMPAIGN_PREVIEW_REMOVE_FAILED,
4609
+ interactionId: campaignInteractionId,
4610
+ campaignId: 'campaign-789',
4611
+ reason: 'INTERNAL_ERROR',
4612
+ },
4613
+ };
4614
+
4615
+ webSocketManagerMock.emit('message', JSON.stringify(failPayload));
4616
+
4617
+ expect(taskEmitSpy).toHaveBeenCalledWith(
4618
+ TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_REMOVE_FAILED,
4619
+ expect.objectContaining({
4620
+ data: expect.objectContaining({interactionId: campaignInteractionId}),
4621
+ })
4622
+ );
4623
+ expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
4624
+
4625
+ // Failure payload (reason) should be merged into task.data
4626
+ expect(task.data.reason).toBe('INTERNAL_ERROR');
4627
+
4628
+ // Original reservation data must be preserved
4629
+ expect(task.data.interaction).toBeDefined();
4630
+ expect(task.data.interaction.callProcessingDetails.campaignId).toBe('campaign-789');
4631
+ });
4632
+
4633
+ it('should not emit campaign preview failure events when task does not exist', () => {
4634
+ const nonExistentId = 'non-existent-interaction';
4635
+
4636
+ const failPayload = {
4637
+ data: {
4638
+ type: CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED,
4639
+ interactionId: nonExistentId,
4640
+ campaignId: 'campaign-789',
4641
+ reason: 'INTERNAL_ERROR',
4642
+ },
4643
+ };
4644
+
4645
+ // Should not throw when task is not found
4646
+ expect(() => {
4647
+ webSocketManagerMock.emit('message', JSON.stringify(failPayload));
4648
+ }).not.toThrow();
4649
+
4650
+ expect(taskManager['taskCollection'][nonExistentId]).toBeUndefined();
4651
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2463
4652
  });
2464
4653
  });
2465
4654
  });