@webex/contact-center 3.12.0-task-refactor.11 → 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 (101) hide show
  1. package/dist/cc.js +297 -83
  2. package/dist/cc.js.map +1 -1
  3. package/dist/constants.js +3 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/metrics/behavioral-events.js +39 -0
  6. package/dist/metrics/behavioral-events.js.map +1 -1
  7. package/dist/metrics/constants.js +8 -1
  8. package/dist/metrics/constants.js.map +1 -1
  9. package/dist/services/ApiAiAssistant.js +3 -0
  10. package/dist/services/ApiAiAssistant.js.map +1 -1
  11. package/dist/services/config/Util.js +1 -1
  12. package/dist/services/config/Util.js.map +1 -1
  13. package/dist/services/config/types.js +10 -0
  14. package/dist/services/config/types.js.map +1 -1
  15. package/dist/services/constants.js +1 -0
  16. package/dist/services/constants.js.map +1 -1
  17. package/dist/services/core/Err.js.map +1 -1
  18. package/dist/services/core/Utils.js +57 -6
  19. package/dist/services/core/Utils.js.map +1 -1
  20. package/dist/services/core/aqm-reqs.js +92 -17
  21. package/dist/services/core/aqm-reqs.js.map +1 -1
  22. package/dist/services/core/websocket/WebSocketManager.js +24 -3
  23. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  24. package/dist/services/index.js +1 -0
  25. package/dist/services/index.js.map +1 -1
  26. package/dist/services/task/Task.js +6 -0
  27. package/dist/services/task/Task.js.map +1 -1
  28. package/dist/services/task/TaskManager.js +155 -6
  29. package/dist/services/task/TaskManager.js.map +1 -1
  30. package/dist/services/task/TaskUtils.js +39 -11
  31. package/dist/services/task/TaskUtils.js.map +1 -1
  32. package/dist/services/task/constants.js +7 -1
  33. package/dist/services/task/constants.js.map +1 -1
  34. package/dist/services/task/dialer.js +129 -0
  35. package/dist/services/task/dialer.js.map +1 -1
  36. package/dist/services/task/state-machine/TaskStateMachine.js +30 -3
  37. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  38. package/dist/services/task/state-machine/actions.js +4 -0
  39. package/dist/services/task/state-machine/actions.js.map +1 -1
  40. package/dist/services/task/state-machine/constants.js +3 -0
  41. package/dist/services/task/state-machine/constants.js.map +1 -1
  42. package/dist/services/task/state-machine/guards.js +23 -0
  43. package/dist/services/task/state-machine/guards.js.map +1 -1
  44. package/dist/services/task/state-machine/types.js.map +1 -1
  45. package/dist/services/task/types.js +59 -0
  46. package/dist/services/task/types.js.map +1 -1
  47. package/dist/types/cc.d.ts +76 -2
  48. package/dist/types/constants.d.ts +3 -0
  49. package/dist/types/metrics/constants.d.ts +6 -0
  50. package/dist/types/services/config/types.d.ts +22 -1
  51. package/dist/types/services/core/Err.d.ts +6 -0
  52. package/dist/types/services/core/Utils.d.ts +20 -2
  53. package/dist/types/services/core/aqm-reqs.d.ts +49 -0
  54. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
  55. package/dist/types/services/core/websocket/connection-service.d.ts +1 -0
  56. package/dist/types/services/task/Task.d.ts +1 -0
  57. package/dist/types/services/task/TaskUtils.d.ts +13 -0
  58. package/dist/types/services/task/constants.d.ts +6 -0
  59. package/dist/types/services/task/dialer.d.ts +45 -0
  60. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +36 -8
  61. package/dist/types/services/task/state-machine/constants.d.ts +3 -0
  62. package/dist/types/services/task/state-machine/guards.d.ts +2 -0
  63. package/dist/types/services/task/state-machine/types.d.ts +11 -0
  64. package/dist/types/services/task/types.d.ts +75 -1
  65. package/dist/types.js.map +1 -1
  66. package/dist/webex.js +1 -1
  67. package/package.json +2 -2
  68. package/src/cc.ts +389 -111
  69. package/src/constants.ts +3 -0
  70. package/src/metrics/behavioral-events.ts +42 -0
  71. package/src/metrics/constants.ts +9 -1
  72. package/src/services/ApiAiAssistant.ts +3 -0
  73. package/src/services/config/Util.ts +1 -1
  74. package/src/services/config/types.ts +12 -1
  75. package/src/services/constants.ts +1 -0
  76. package/src/services/core/Err.ts +3 -0
  77. package/src/services/core/Utils.ts +68 -5
  78. package/src/services/core/aqm-reqs.ts +100 -22
  79. package/src/services/core/websocket/WebSocketManager.ts +23 -2
  80. package/src/services/index.ts +1 -0
  81. package/src/services/task/Task.ts +13 -0
  82. package/src/services/task/TaskManager.ts +171 -7
  83. package/src/services/task/TaskUtils.ts +33 -1
  84. package/src/services/task/constants.ts +6 -0
  85. package/src/services/task/dialer.ts +136 -1
  86. package/src/services/task/state-machine/TaskStateMachine.ts +37 -5
  87. package/src/services/task/state-machine/actions.ts +4 -0
  88. package/src/services/task/state-machine/constants.ts +3 -0
  89. package/src/services/task/state-machine/guards.ts +30 -0
  90. package/src/services/task/state-machine/types.ts +14 -1
  91. package/src/services/task/types.ts +80 -0
  92. package/src/types.ts +4 -1
  93. package/test/unit/spec/cc.ts +293 -26
  94. package/test/unit/spec/services/config/index.ts +30 -2
  95. package/test/unit/spec/services/core/Utils.ts +367 -1
  96. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +90 -1
  97. package/test/unit/spec/services/core/websocket/connection-service.ts +4 -5
  98. package/test/unit/spec/services/task/TaskManager.ts +2325 -150
  99. package/test/unit/spec/services/task/dialer.ts +372 -96
  100. package/umd/contact-center.min.js +2 -2
  101. package/umd/contact-center.min.js.map +1 -1
@@ -164,7 +164,19 @@ describe('TaskManager', () => {
164
164
  beforeEach(() => {
165
165
  contactMock = contact;
166
166
  webSocketManagerMock = new EventEmitter();
167
+ <<<<<<< HEAD
167
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
168
180
 
169
181
  webex = {
170
182
  logger: {
@@ -194,6 +206,7 @@ describe('TaskManager', () => {
194
206
  onSpy = jest.spyOn(webCallingService, 'on');
195
207
  offSpy = jest.spyOn(webCallingService, 'off');
196
208
 
209
+ <<<<<<< HEAD
197
210
  mockApiAIAssistant = {
198
211
  sendEvent: jest.fn().mockResolvedValue({}),
199
212
  };
@@ -207,6 +220,26 @@ describe('TaskManager', () => {
207
220
  );
208
221
  taskManager.taskCollection[taskId] = createMockTask(taskDataMock);
209
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
210
243
  taskManager.call = mockCall;
211
244
  taskManager.setAgentId('test-agent-id');
212
245
 
@@ -233,8 +266,15 @@ describe('TaskManager', () => {
233
266
 
234
267
  incomingCallCb(mockCall);
235
268
 
269
+ <<<<<<< HEAD
236
270
  expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(taskId));
237
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
238
278
  });
239
279
 
240
280
  it('should re-emit task related events', () => {
@@ -432,6 +472,106 @@ describe('TaskManager', () => {
432
472
  expect(existingTaskEmitSpy).not.toHaveBeenCalled();
433
473
  });
434
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
+
435
575
  it('should not re-emit agent related events', () => {
436
576
  const dummyPayload = {
437
577
  data: {
@@ -673,6 +813,7 @@ describe('TaskManager', () => {
673
813
  it('test call listeners being switched off on call end', () => {
674
814
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
675
815
 
816
+ <<<<<<< HEAD
676
817
  const webrtcTask = new WebRTC(contactMock, webCallingService, taskDataMock, {
677
818
  isEndTaskEnabled: true,
678
819
  isEndConsultEnabled: true,
@@ -695,6 +836,13 @@ describe('TaskManager', () => {
695
836
  });
696
837
 
697
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
698
846
  const callOffSpy = jest.spyOn(mockCall, 'off');
699
847
  const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
700
848
  const payload = {
@@ -727,12 +875,18 @@ describe('TaskManager', () => {
727
875
  webSocketManagerMock.emit('message', JSON.stringify(hydratePayload));
728
876
 
729
877
  taskManager.getTask(taskId).data = payload.data;
878
+ <<<<<<< HEAD
730
879
  webSocketManagerMock.emit('message', JSON.stringify(payload));
731
880
  const stateMachineEvent = expectLastStateMachineEvent(
732
881
  sendStateMachineEventSpy,
733
882
  TaskEvent.CONTACT_ENDED
734
883
  );
735
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
736
890
  expect(webCallListenerSpy).toHaveBeenCalledWith();
737
891
  expect(callOffSpy).toHaveBeenCalledWith(
738
892
  CALL_EVENT_KEYS.REMOTE_MEDIA,
@@ -769,19 +923,29 @@ describe('TaskManager', () => {
769
923
 
770
924
  task.updateTaskData(payload.data);
771
925
  webSocketManagerMock.emit('message', JSON.stringify(payload));
926
+ <<<<<<< HEAD
772
927
  const stateMachineEvent = expectLastStateMachineEvent(
773
928
  sendStateMachineEventSpy,
774
929
  TaskEvent.CONTACT_ENDED
775
930
  );
776
931
  expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(true);
777
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
778
937
  });
779
938
 
780
939
  it('should emit TASK_REJECT event on AGENT_INVITE_FAILED event', () => {
781
940
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
782
941
 
942
+ <<<<<<< HEAD
783
943
  const task = taskManager.getTask(taskId);
784
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
785
949
  const payload = {
786
950
  data: {
787
951
  type: CC_EVENTS.AGENT_INVITE_FAILED,
@@ -800,6 +964,7 @@ describe('TaskManager', () => {
800
964
  },
801
965
  };
802
966
 
967
+ <<<<<<< HEAD
803
968
  task.updateTaskData(payload.data);
804
969
  webSocketManagerMock.emit('message', JSON.stringify(payload));
805
970
  const stateMachineEvent = expectLastStateMachineEvent(
@@ -811,6 +976,18 @@ describe('TaskManager', () => {
811
976
  });
812
977
 
813
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
814
991
  const payload = {
815
992
  data: {
816
993
  ...initalPayload.data,
@@ -947,7 +1124,11 @@ describe('TaskManager', () => {
947
1124
  expect(createdTask.data.isConferenceInProgress).toBe(false);
948
1125
  });
949
1126
 
1127
+ <<<<<<< HEAD
950
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
951
1132
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
952
1133
 
953
1134
  const wrapupPayload = {
@@ -1102,8 +1283,13 @@ describe('TaskManager', () => {
1102
1283
 
1103
1284
  const task = taskManager.getTask(taskId);
1104
1285
  const taskEmitSpy = jest.spyOn(task, 'emit');
1286
+ <<<<<<< HEAD
1105
1287
  const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
1106
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
1107
1293
 
1108
1294
  // Step 2: Trigger AGENT_OFFER_CONTACT with auto-answer
1109
1295
  const autoAnswerPayload = {
@@ -1127,6 +1313,7 @@ describe('TaskManager', () => {
1127
1313
  // Verify accept was called
1128
1314
  expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
1129
1315
 
1316
+ <<<<<<< HEAD
1130
1317
  const stateMachineEvent = expectLastStateMachineEvent(
1131
1318
  sendStateMachineEventSpy,
1132
1319
  TaskEvent.TASK_OFFERED
@@ -1135,6 +1322,11 @@ describe('TaskManager', () => {
1135
1322
  // Verify task auto-answer event was emitted
1136
1323
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
1137
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
1138
1330
  });
1139
1331
 
1140
1332
  it('should NOT emit TASK_AUTO_ANSWERED event when auto-answer fails', async () => {
@@ -1180,7 +1372,10 @@ describe('TaskManager', () => {
1180
1372
  const task = taskManager.getTask(taskId);
1181
1373
  const taskEmitSpy = jest.spyOn(task, 'emit');
1182
1374
  const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
1375
+ <<<<<<< HEAD
1183
1376
  const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1377
+ =======
1378
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1184
1379
 
1185
1380
  // Step 2: Trigger AGENT_OFFER_CONSULT with auto-answer
1186
1381
  const consultAutoAnswerPayload = {
@@ -1205,6 +1400,7 @@ describe('TaskManager', () => {
1205
1400
  // Verify accept was called
1206
1401
  expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
1207
1402
 
1403
+ <<<<<<< HEAD
1208
1404
  const stateMachineEvent = expectLastStateMachineEvent(
1209
1405
  sendStateMachineEventSpy,
1210
1406
  TaskEvent.OFFER_CONSULT
@@ -1214,11 +1410,18 @@ describe('TaskManager', () => {
1214
1410
  isConsulted: true,
1215
1411
  });
1216
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
1217
1417
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
1218
1418
 
1219
1419
  // Verify isConsulted flag is set correctly
1220
1420
  expect(task.data.isConsulted).toBe(true);
1421
+ <<<<<<< HEAD
1221
1422
  sendStateMachineEventSpy.mockRestore();
1423
+ =======
1424
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1222
1425
  });
1223
1426
 
1224
1427
  it('should NOT emit TASK_AUTO_ANSWERED when isAutoAnswering is false', async () => {
@@ -1259,6 +1462,7 @@ describe('TaskManager', () => {
1259
1462
  });
1260
1463
  });
1261
1464
 
1465
+ <<<<<<< HEAD
1262
1466
  it('should remove OUTDIAL task from taskCollection on AGENT_OUTBOUND_FAILED when terminated', () => {
1263
1467
  const task = taskManager.getTask(taskId);
1264
1468
  Object.assign(task.data, {
@@ -1273,6 +1477,26 @@ describe('TaskManager', () => {
1273
1477
  task.unregisterWebCallListeners = jest.fn();
1274
1478
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1275
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
1276
1500
 
1277
1501
  const payload = {
1278
1502
  data: {
@@ -1285,7 +1509,10 @@ describe('TaskManager', () => {
1285
1509
  state: 'new',
1286
1510
  isTerminated: true,
1287
1511
  },
1512
+ <<<<<<< HEAD
1288
1513
  agentsPendingWrapUp: ['agent-123'],
1514
+ =======
1515
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1289
1516
  interactionId: taskId,
1290
1517
  orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
1291
1518
  trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
@@ -1299,62 +1526,26 @@ describe('TaskManager', () => {
1299
1526
  };
1300
1527
 
1301
1528
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1529
+ <<<<<<< HEAD
1530
+ =======
1302
1531
 
1303
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1304
-
1305
- expect(taskManager.getTask(taskId)).toBeUndefined();
1306
- expect(removeTaskSpy).toHaveBeenCalled();
1307
- const stateMachineEvent = expectLastStateMachineEvent(
1308
- sendStateMachineEventSpy,
1309
- TaskEvent.OUTBOUND_FAILED
1310
- );
1311
- expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
1312
- sendStateMachineEventSpy.mockRestore();
1532
+ expect(taskManager.getTask(taskId)).toBeDefined();
1533
+ expect(removeTaskSpy).not.toHaveBeenCalled();
1313
1534
  });
1314
1535
 
1315
1536
  it('should emit TASK_OUTDIAL_FAILED event on AGENT_OUTBOUND_FAILED', () => {
1316
1537
  const task = taskManager.getTask(taskId);
1317
- const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1318
- const payload = {
1319
- data: {
1320
- type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
1321
- interactionId: taskId,
1322
- reason: 'CUSTOMER_BUSY',
1323
- },
1324
- };
1325
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1326
- const stateMachineEvent = expectLastStateMachineEvent(
1327
- sendStateMachineEventSpy,
1328
- TaskEvent.OUTBOUND_FAILED
1329
- );
1330
- expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
1331
- sendStateMachineEventSpy.mockRestore();
1332
- });
1333
-
1334
- it('should pass taskData in OUTBOUND_FAILED event for shouldWrapUp guard evaluation', () => {
1335
- const task = taskManager.getTask(taskId);
1336
- const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1538
+ task.updateTaskData = jest.fn().mockReturnValue(task);
1539
+ const taskEmitSpy = jest.spyOn(task, 'emit');
1337
1540
  const payload = {
1338
1541
  data: {
1339
1542
  type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
1340
1543
  interactionId: taskId,
1341
1544
  reason: 'CUSTOMER_BUSY',
1342
- agentsPendingWrapUp: ['agent-123'],
1343
- interaction: {
1344
- outboundType: 'OUTDIAL',
1345
- isTerminated: true,
1346
- },
1347
1545
  },
1348
1546
  };
1349
1547
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1350
- const stateMachineEvent = expectLastStateMachineEvent(
1351
- sendStateMachineEventSpy,
1352
- TaskEvent.OUTBOUND_FAILED
1353
- );
1354
- expect(stateMachineEvent?.taskData).toBeDefined();
1355
- expect(stateMachineEvent?.taskData?.agentsPendingWrapUp).toEqual(['agent-123']);
1356
- expect(stateMachineEvent?.taskData?.interaction?.outboundType).toBe('OUTDIAL');
1357
- sendStateMachineEventSpy.mockRestore();
1548
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OUTDIAL_FAILED, 'CUSTOMER_BUSY');
1358
1549
  });
1359
1550
 
1360
1551
  it('should handle AGENT_OUTBOUND_FAILED gracefully when task is undefined', () => {
@@ -1372,15 +1563,23 @@ describe('TaskManager', () => {
1372
1563
  });
1373
1564
 
1374
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
+
1375
1569
  const task = taskManager.getTask(taskId);
1376
- Object.assign(task.data, {
1377
- interaction: {
1378
- ...task.data.interaction,
1379
- outboundType: 'OUTDIAL',
1380
- state: 'new',
1381
- mediaType: 'telephony',
1382
- },
1383
- 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;
1384
1583
  });
1385
1584
  task.unregisterWebCallListeners = jest.fn();
1386
1585
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -1394,7 +1593,7 @@ describe('TaskManager', () => {
1394
1593
  state: 'new',
1395
1594
  mediaType: 'telephony',
1396
1595
  },
1397
- agentsPendingWrapUp: ['agent-123'],
1596
+ agentsPendingWrapUp: [agentId],
1398
1597
  },
1399
1598
  };
1400
1599
 
@@ -1406,14 +1605,19 @@ describe('TaskManager', () => {
1406
1605
 
1407
1606
  it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is empty', () => {
1408
1607
  const task = taskManager.getTask(taskId);
1409
- Object.assign(task.data, {
1410
- interaction: {
1411
- ...task.data.interaction,
1412
- outboundType: 'OUTDIAL',
1413
- state: 'new',
1414
- mediaType: 'telephony',
1415
- },
1416
- 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;
1417
1621
  });
1418
1622
  task.unregisterWebCallListeners = jest.fn();
1419
1623
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -1438,13 +1642,19 @@ describe('TaskManager', () => {
1438
1642
 
1439
1643
  it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is undefined', () => {
1440
1644
  const task = taskManager.getTask(taskId);
1441
- Object.assign(task.data, {
1442
- interaction: {
1443
- ...task.data.interaction,
1444
- outboundType: 'OUTDIAL',
1445
- state: 'new',
1446
- mediaType: 'telephony',
1447
- },
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;
1448
1658
  });
1449
1659
  task.unregisterWebCallListeners = jest.fn();
1450
1660
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -1483,131 +1693,666 @@ describe('TaskManager', () => {
1483
1693
  }).not.toThrow();
1484
1694
  });
1485
1695
 
1486
- it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
1487
- const task = taskManager.getTask(taskId);
1488
- Object.assign(task.data, {
1489
- interaction: {
1490
- ...task.data.interaction,
1491
- outboundType: 'OUTDIAL',
1492
- state: 'new',
1493
- isTerminated: false,
1494
- },
1495
- });
1496
- task.unregisterWebCallListeners = jest.fn();
1497
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1696
+ describe('wrapUpRequired logic in CONTACT_ENDED event', () => {
1697
+ const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
1498
1698
 
1499
- const payload = {
1500
- data: {
1501
- type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
1502
- agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
1503
- eventTime: 1733211616959,
1504
- eventType: 'RoutingMessage',
1505
- interaction: {
1506
- outboundType: 'OUTDIAL',
1507
- state: 'new',
1508
- isTerminated: false,
1509
- },
1510
- interactionId: taskId,
1511
- orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
1512
- trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
1513
- mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
1514
- destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
1515
- owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
1516
- queueMgr: 'aqm',
1517
- reason: 'USER_DECLINED',
1518
- reasonCode: 156,
1519
- },
1520
- };
1699
+ beforeEach(() => {
1700
+ // Set the agent ID on taskManager
1701
+ taskManager.setAgentId(agentId);
1702
+ });
1521
1703
 
1522
- webSocketManagerMock.emit('message', JSON.stringify(payload));
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();
1523
1714
 
1524
- expect(taskManager.getTask(taskId)).toBeUndefined();
1525
- expect(removeTaskSpy).toHaveBeenCalled();
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'],
1724
+ },
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
+ });
1526
1992
  });
1527
1993
 
1528
- it('handle AGENT_OFFER_CONSULT event', () => {
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
+
1529
2013
  const payload = {
1530
2014
  data: {
1531
- ...initalPayload.data,
1532
- type: CC_EVENTS.AGENT_OFFER_CONSULT,
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',
2028
+ destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
2029
+ owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
2030
+ queueMgr: 'aqm',
2031
+ reason: 'USER_DECLINED',
2032
+ reasonCode: 156,
1533
2033
  },
1534
2034
  };
1535
-
1536
- webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1537
- const task = taskManager.getTask(taskId);
1538
- const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
2035
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1539
2036
 
1540
2037
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1541
2038
 
2039
+ expect(taskManager.getTask(taskId)).toBeUndefined();
2040
+ expect(removeTaskSpy).toHaveBeenCalled();
2041
+ <<<<<<< HEAD
1542
2042
  const stateMachineEvent = expectLastStateMachineEvent(
1543
2043
  sendStateMachineEventSpy,
1544
- TaskEvent.OFFER_CONSULT
2044
+ TaskEvent.OUTBOUND_FAILED
1545
2045
  );
1546
- expect(stateMachineEvent?.taskData).toEqual({...payload.data, isConsulted: true});
2046
+ expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
1547
2047
  sendStateMachineEventSpy.mockRestore();
1548
2048
  });
1549
2049
 
1550
- it('should emit TASK_CONSULT_ACCEPTED event on AGENT_CONSULTING event', () => {
1551
- const initialConsultingPayload = {
1552
- data: {
1553
- ...initalPayload.data,
1554
- type: CC_EVENTS.AGENT_OFFER_CONSULT,
1555
- },
1556
- };
1557
-
1558
- 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 = {
1559
2054
  data: {
1560
- ...initalPayload.data,
1561
- type: CC_EVENTS.AGENT_CONSULTING,
2055
+ type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
2056
+ interactionId: taskId,
2057
+ reason: 'CUSTOMER_BUSY',
1562
2058
  },
1563
2059
  };
1564
-
1565
- webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1566
-
1567
- const task = taskManager.getTask(taskId);
1568
- const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1569
-
1570
- webSocketManagerMock.emit('message', JSON.stringify(initialConsultingPayload));
1571
- webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
2060
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1572
2061
  const stateMachineEvent = expectLastStateMachineEvent(
1573
2062
  sendStateMachineEventSpy,
1574
- TaskEvent.CONSULTING_ACTIVE
2063
+ TaskEvent.OUTBOUND_FAILED
1575
2064
  );
1576
- expect(stateMachineEvent?.taskData).toEqual(consultingPayload.data);
2065
+ expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
1577
2066
  sendStateMachineEventSpy.mockRestore();
1578
2067
  });
1579
2068
 
1580
- 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');
1581
2072
  const payload = {
1582
2073
  data: {
1583
- ...initalPayload.data,
1584
- 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
+ },
1585
2082
  },
1586
2083
  };
1587
-
1588
- webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1589
- taskManager.getTask(taskId).data.isConsulted = true;
1590
- const task = taskManager.getTask(taskId);
1591
- const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1592
2084
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1593
2085
  const stateMachineEvent = expectLastStateMachineEvent(
1594
2086
  sendStateMachineEventSpy,
1595
- TaskEvent.CONSULT_END
2087
+ TaskEvent.OUTBOUND_FAILED
1596
2088
  );
1597
- 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');
1598
2092
  sendStateMachineEventSpy.mockRestore();
1599
2093
  });
1600
2094
 
1601
- 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', () => {
1602
2096
  const payload = {
1603
2097
  data: {
1604
- ...initalPayload.data,
1605
- type: CC_EVENTS.AGENT_CONSULT_ENDED,
2098
+ type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
2099
+ interactionId: 'non-existent-task-id',
2100
+ reason: 'CUSTOMER_BUSY',
1606
2101
  },
1607
2102
  };
1608
-
1609
- webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1610
-
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,
2351
+ },
2352
+ };
2353
+
2354
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
2355
+
1611
2356
  taskManager.getTask(taskId).data.isConsulted = true;
1612
2357
  const task = taskManager.getTask(taskId);
1613
2358
 
@@ -1775,12 +2520,23 @@ describe('TaskManager', () => {
1775
2520
 
1776
2521
  webSocketManagerMock.emit('message', JSON.stringify(assignFailedPayload));
1777
2522
 
2523
+ <<<<<<< HEAD
1778
2524
  const stateMachineEvent = expectLastStateMachineEvent(
1779
2525
  sendStateMachineEventSpy,
1780
2526
  TaskEvent.ASSIGN_FAILED
1781
2527
  );
1782
2528
  expect(stateMachineEvent?.reason).toBe(assignFailedPayload.data.reason);
1783
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
1784
2540
  });
1785
2541
 
1786
2542
  it('should remove currentTask from taskCollection on AGENT_WRAPPEDUP event', () => {
@@ -1855,12 +2611,20 @@ describe('TaskManager', () => {
1855
2611
  },
1856
2612
  };
1857
2613
  webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
2614
+ <<<<<<< HEAD
1858
2615
  expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONSULTING_ACTIVE);
1859
2616
  expect(taskManagerEmitSpy).toHaveBeenCalledWith(
1860
2617
  TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
1861
2618
  taskManager.getTask(taskId)
1862
2619
  );
1863
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
1864
2628
  });
1865
2629
 
1866
2630
  it('should update task data on AGENT_CONTACT_UNASSIGNED', () => {
@@ -1887,6 +2651,7 @@ describe('TaskManager', () => {
1887
2651
  },
1888
2652
  };
1889
2653
  webSocketManagerMock.emit('message', JSON.stringify(unassignedPayload));
2654
+ <<<<<<< HEAD
1890
2655
  expect(sendStateMachineEventSpy).not.toHaveBeenCalled();
1891
2656
  expect(updateTaskDataSpy).toHaveBeenCalledWith(unassignedPayload.data);
1892
2657
  });
@@ -1957,6 +2722,13 @@ describe('TaskManager', () => {
1957
2722
  destinationType: null,
1958
2723
  })
1959
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
1960
2732
  });
1961
2733
 
1962
2734
  it('should handle chat interaction and emit TASK_INCOMING immediately', () => {
@@ -1968,6 +2740,11 @@ describe('TaskManager', () => {
1968
2740
  },
1969
2741
  };
1970
2742
 
2743
+ <<<<<<< HEAD
2744
+ =======
2745
+ const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
2746
+
2747
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1971
2748
  // Simulate receiving a chat task
1972
2749
  webSocketManagerMock.emit('message', JSON.stringify(chatPayload));
1973
2750
 
@@ -1990,6 +2767,11 @@ describe('TaskManager', () => {
1990
2767
  },
1991
2768
  };
1992
2769
 
2770
+ <<<<<<< HEAD
2771
+ =======
2772
+ const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
2773
+
2774
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
1993
2775
  // Simulate receiving an email task
1994
2776
  webSocketManagerMock.emit('message', JSON.stringify(emailPayload));
1995
2777
 
@@ -2013,11 +2795,21 @@ describe('TaskManager', () => {
2013
2795
  },
2014
2796
  };
2015
2797
 
2798
+ <<<<<<< HEAD
2016
2799
  webSocketManagerMock.emit('message', JSON.stringify(chatReservedPayload));
2017
2800
  const task = taskManager.getTask(chatReservedPayload.data.interactionId);
2018
2801
  const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
2019
2802
 
2020
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
2021
2813
 
2022
2814
  // 2. Chat task is assigned
2023
2815
  const chatAssignedPayload = {
@@ -2027,9 +2819,18 @@ describe('TaskManager', () => {
2027
2819
  },
2028
2820
  };
2029
2821
 
2822
+ <<<<<<< HEAD
2030
2823
  webSocketManagerMock.emit('message', JSON.stringify(chatAssignedPayload));
2031
2824
 
2032
2825
  expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.ASSIGN);
2826
+ =======
2827
+ const task = taskManager.getTask(chatReservedPayload.data.interactionId);
2828
+ const taskEmitSpy = jest.spyOn(task, 'emit');
2829
+
2830
+ webSocketManagerMock.emit('message', JSON.stringify(chatAssignedPayload));
2831
+
2832
+ expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, task);
2833
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2033
2834
 
2034
2835
  // 3. Chat task is ended with state 'new' to trigger cleanup
2035
2836
  const chatEndedPayload = {
@@ -2037,7 +2838,10 @@ describe('TaskManager', () => {
2037
2838
  ...chatReservedPayload.data,
2038
2839
  type: CC_EVENTS.CONTACT_ENDED,
2039
2840
  interaction: {mediaType: 'chat', state: 'new'}, // Change to 'new' state
2841
+ <<<<<<< HEAD
2040
2842
  wrapUpRequired: false,
2843
+ =======
2844
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2041
2845
  },
2042
2846
  };
2043
2847
 
@@ -2134,8 +2938,18 @@ describe('TaskManager', () => {
2134
2938
  expect(taskManager.getAllTasks()).toHaveProperty(task2Payload.data.interactionId);
2135
2939
  expect(taskManager.getAllTasks()).toHaveProperty(task3Payload.data.interactionId);
2136
2940
 
2941
+ <<<<<<< HEAD
2137
2942
  const task2 = taskManager.getTask(task2Payload.data.interactionId);
2138
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
2139
2953
 
2140
2954
  // End only the second task (chat task)
2141
2955
  const chatEndedPayload = {
@@ -2143,6 +2957,7 @@ describe('TaskManager', () => {
2143
2957
  ...task2Payload.data,
2144
2958
  type: CC_EVENTS.CONTACT_ENDED,
2145
2959
  interaction: {mediaType: 'chat', state: 'new'}, // Using 'new' to trigger cleanup
2960
+ <<<<<<< HEAD
2146
2961
  wrapUpRequired: false,
2147
2962
  },
2148
2963
  };
@@ -2155,6 +2970,17 @@ describe('TaskManager', () => {
2155
2970
  TaskEvent.CONTACT_ENDED
2156
2971
  );
2157
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
2158
2984
 
2159
2985
  // Verify task2 was removed from collection (since state was 'new')
2160
2986
  expect(taskManager.getTask(task2Payload.data.interactionId)).toBeUndefined();
@@ -2165,7 +2991,10 @@ describe('TaskManager', () => {
2165
2991
 
2166
2992
  // Store reference to task3 before we end it
2167
2993
  const task3 = taskManager.getTask(task3Payload.data.interactionId);
2994
+ <<<<<<< HEAD
2168
2995
  const task3SendStateMachineEventSpy = task3.sendStateMachineEvent as jest.Mock;
2996
+ =======
2997
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2169
2998
 
2170
2999
  // Now end task3 with a state that doesn't trigger cleanup
2171
3000
  const emailEndedPayload = {
@@ -2173,6 +3002,7 @@ describe('TaskManager', () => {
2173
3002
  ...task3Payload.data,
2174
3003
  type: CC_EVENTS.CONTACT_ENDED,
2175
3004
  interaction: {mediaType: 'email', state: 'connected'}, // Using 'connected' to NOT trigger cleanup
3005
+ <<<<<<< HEAD
2176
3006
  wrapUpRequired: true,
2177
3007
  },
2178
3008
  };
@@ -2185,6 +3015,15 @@ describe('TaskManager', () => {
2185
3015
  TaskEvent.CONTACT_ENDED
2186
3016
  );
2187
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
2188
3027
 
2189
3028
  // Verify task3 is still in collection (since state was 'connected')
2190
3029
  expect(taskManager.getTask(task3Payload.data.interactionId)).toBeDefined();
@@ -2196,8 +3035,17 @@ describe('TaskManager', () => {
2196
3035
  it('should emit TRANSFER_SUCCESS event on AGENT_VTEAM_TRANSFERRED event', () => {
2197
3036
  // First create a task by emitting the initial payload
2198
3037
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
3038
+ <<<<<<< HEAD
2199
3039
  const task = taskManager.getTask(taskId);
2200
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
2201
3049
 
2202
3050
  const vteamTransferredPayload = {
2203
3051
  data: {
@@ -2221,9 +3069,14 @@ describe('TaskManager', () => {
2221
3069
 
2222
3070
  webSocketManagerMock.emit('message', JSON.stringify(vteamTransferredPayload));
2223
3071
 
3072
+ <<<<<<< HEAD
2224
3073
  // Check that the state machine received the END event
2225
3074
  expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TRANSFER_SUCCESS);
2226
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
2227
3080
 
2228
3081
  // The task should still exist in the collection based on current implementation
2229
3082
  expect(taskManager.getTask(taskId)).toBeDefined();
@@ -2238,7 +3091,14 @@ describe('TaskManager', () => {
2238
3091
  },
2239
3092
  };
2240
3093
  const task = taskManager.getTask(taskId);
3094
+ <<<<<<< HEAD
2241
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
2242
3102
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2243
3103
  expect(sendStateMachineEventSpy).toHaveBeenCalled();
2244
3104
  const stateMachineEvent = sendStateMachineEventSpy.mock.calls.at(-1)?.[0];
@@ -2359,6 +3219,7 @@ describe('TaskManager', () => {
2359
3219
 
2360
3220
  describe('Conference event handling', () => {
2361
3221
  let task;
3222
+ <<<<<<< HEAD
2362
3223
 
2363
3224
  beforeEach(() => {
2364
3225
  task = {
@@ -2366,6 +3227,22 @@ describe('TaskManager', () => {
2366
3227
  emit: jest.fn(),
2367
3228
  updateTaskData: jest.fn(),
2368
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
2369
3246
  };
2370
3247
  taskManager.taskCollection[taskId] = task as any;
2371
3248
  });
@@ -2402,7 +3279,28 @@ describe('TaskManager', () => {
2402
3279
 
2403
3280
  it('sends PARTICIPANT_LEFT_CONFERENCE to state machine as PARTICIPANT_LEAVE', () => {
2404
3281
  const payload = {
3282
+ <<<<<<< HEAD
2405
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
2406
3304
  };
2407
3305
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2408
3306
  expect(task.sendStateMachineEvent).toHaveBeenCalled();
@@ -2410,7 +3308,346 @@ describe('TaskManager', () => {
2410
3308
  expect(call.type).toBe(TaskEvent.PARTICIPANT_LEAVE);
2411
3309
  });
2412
3310
 
3311
+ <<<<<<< HEAD
2413
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
2414
3651
  const payload = {
2415
3652
  data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCING, interactionId: taskId},
2416
3653
  };
@@ -2430,7 +3667,11 @@ describe('TaskManager', () => {
2430
3667
 
2431
3668
  it('only routes conference events to matching tasks', () => {
2432
3669
  const otherTaskId = 'other-task-id';
3670
+ <<<<<<< HEAD
2433
3671
  const otherTask: any = {
3672
+ =======
3673
+ const otherTask = {
3674
+ >>>>>>> 81672300901738f14289fd2a7414c6c4b1389aa6
2434
3675
  data: {interactionId: otherTaskId},
2435
3676
  emit: jest.fn(),
2436
3677
  updateTaskData: jest.fn(),
@@ -2443,6 +3684,7 @@ describe('TaskManager', () => {
2443
3684
  };
2444
3685
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2445
3686
 
3687
+ <<<<<<< HEAD
2446
3688
  expect(task.sendStateMachineEvent).toHaveBeenCalled();
2447
3689
  expect(otherTask.sendStateMachineEvent).not.toHaveBeenCalled();
2448
3690
  });
@@ -2474,6 +3716,939 @@ describe('TaskManager', () => {
2474
3716
  });
2475
3717
 
2476
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
2477
4652
  });
2478
4653
  });
2479
4654
  });