@webex/contact-center 3.12.0-next.73 → 3.12.0-next.74

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 (188) hide show
  1. package/AGENTS.md +438 -0
  2. package/ai-docs/README.md +131 -0
  3. package/ai-docs/RULES.md +455 -0
  4. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  5. package/ai-docs/patterns/testing-patterns.md +480 -0
  6. package/ai-docs/patterns/typescript-patterns.md +365 -0
  7. package/ai-docs/templates/README.md +102 -0
  8. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  9. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  10. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  11. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  12. package/ai-docs/templates/new-method/00-master.md +80 -0
  13. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  14. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  15. package/ai-docs/templates/new-method/03-tests.md +201 -0
  16. package/ai-docs/templates/new-method/04-validation.md +141 -0
  17. package/ai-docs/templates/new-service/00-master.md +109 -0
  18. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  19. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  20. package/ai-docs/templates/new-service/03-integration.md +178 -0
  21. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  22. package/ai-docs/templates/new-service/05-validation.md +145 -0
  23. package/dist/cc.js +53 -38
  24. package/dist/cc.js.map +1 -1
  25. package/dist/config.js +6 -0
  26. package/dist/config.js.map +1 -1
  27. package/dist/constants.js +14 -1
  28. package/dist/constants.js.map +1 -1
  29. package/dist/index.js +13 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/metrics/behavioral-events.js +26 -0
  32. package/dist/metrics/behavioral-events.js.map +1 -1
  33. package/dist/metrics/constants.js +9 -3
  34. package/dist/metrics/constants.js.map +1 -1
  35. package/dist/services/ApiAiAssistant.js +74 -3
  36. package/dist/services/ApiAiAssistant.js.map +1 -1
  37. package/dist/services/config/Util.js +2 -2
  38. package/dist/services/config/Util.js.map +1 -1
  39. package/dist/services/config/types.js +25 -9
  40. package/dist/services/config/types.js.map +1 -1
  41. package/dist/services/core/Utils.js +74 -27
  42. package/dist/services/core/Utils.js.map +1 -1
  43. package/dist/services/core/websocket/WebSocketManager.js +2 -1
  44. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  45. package/dist/services/core/websocket/types.js.map +1 -1
  46. package/dist/services/index.js +1 -1
  47. package/dist/services/index.js.map +1 -1
  48. package/dist/services/task/Task.js +688 -0
  49. package/dist/services/task/Task.js.map +1 -0
  50. package/dist/services/task/TaskFactory.js +45 -0
  51. package/dist/services/task/TaskFactory.js.map +1 -0
  52. package/dist/services/task/TaskManager.js +725 -612
  53. package/dist/services/task/TaskManager.js.map +1 -1
  54. package/dist/services/task/TaskUtils.js +162 -26
  55. package/dist/services/task/TaskUtils.js.map +1 -1
  56. package/dist/services/task/constants.js +6 -1
  57. package/dist/services/task/constants.js.map +1 -1
  58. package/dist/services/task/digital/Digital.js +77 -0
  59. package/dist/services/task/digital/Digital.js.map +1 -0
  60. package/dist/services/task/state-machine/TaskStateMachine.js +837 -0
  61. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  62. package/dist/services/task/state-machine/actions.js +543 -0
  63. package/dist/services/task/state-machine/actions.js.map +1 -0
  64. package/dist/services/task/state-machine/constants.js +161 -0
  65. package/dist/services/task/state-machine/constants.js.map +1 -0
  66. package/dist/services/task/state-machine/guards.js +340 -0
  67. package/dist/services/task/state-machine/guards.js.map +1 -0
  68. package/dist/services/task/state-machine/index.js +53 -0
  69. package/dist/services/task/state-machine/index.js.map +1 -0
  70. package/dist/services/task/state-machine/types.js +54 -0
  71. package/dist/services/task/state-machine/types.js.map +1 -0
  72. package/dist/services/task/state-machine/uiControlsComputer.js +553 -0
  73. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  74. package/dist/services/task/taskDataNormalizer.js +99 -0
  75. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  76. package/dist/services/task/types.js +168 -16
  77. package/dist/services/task/types.js.map +1 -1
  78. package/dist/services/task/voice/Voice.js +1042 -0
  79. package/dist/services/task/voice/Voice.js.map +1 -0
  80. package/dist/services/task/voice/WebRTC.js +149 -0
  81. package/dist/services/task/voice/WebRTC.js.map +1 -0
  82. package/dist/types/cc.d.ts +19 -18
  83. package/dist/types/config.d.ts +6 -0
  84. package/dist/types/constants.d.ts +14 -1
  85. package/dist/types/index.d.ts +11 -5
  86. package/dist/types/metrics/constants.d.ts +7 -1
  87. package/dist/types/services/ApiAiAssistant.d.ts +11 -3
  88. package/dist/types/services/config/types.d.ts +113 -9
  89. package/dist/types/services/core/Utils.d.ts +23 -10
  90. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
  91. package/dist/types/services/core/websocket/types.d.ts +1 -1
  92. package/dist/types/services/index.d.ts +1 -1
  93. package/dist/types/services/task/Task.d.ts +157 -0
  94. package/dist/types/services/task/TaskFactory.d.ts +12 -0
  95. package/dist/types/services/task/TaskUtils.d.ts +46 -2
  96. package/dist/types/services/task/constants.d.ts +5 -0
  97. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  98. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1144 -0
  99. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  100. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  101. package/dist/types/services/task/state-machine/guards.d.ts +90 -0
  102. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  103. package/dist/types/services/task/state-machine/types.d.ts +267 -0
  104. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  105. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  106. package/dist/types/services/task/types.d.ts +551 -78
  107. package/dist/types/services/task/voice/Voice.d.ts +184 -0
  108. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  109. package/dist/types/types.d.ts +92 -0
  110. package/dist/types/webex.d.ts +1 -0
  111. package/dist/types.js +85 -0
  112. package/dist/types.js.map +1 -1
  113. package/dist/webex.js +14 -2
  114. package/dist/webex.js.map +1 -1
  115. package/package.json +15 -12
  116. package/src/cc.ts +63 -43
  117. package/src/config.ts +6 -0
  118. package/src/constants.ts +14 -1
  119. package/src/index.ts +14 -5
  120. package/src/metrics/ai-docs/AGENTS.md +348 -0
  121. package/src/metrics/ai-docs/ARCHITECTURE.md +336 -0
  122. package/src/metrics/behavioral-events.ts +28 -0
  123. package/src/metrics/constants.ts +9 -4
  124. package/src/services/ApiAiAssistant.ts +104 -3
  125. package/src/services/agent/ai-docs/AGENTS.md +238 -0
  126. package/src/services/agent/ai-docs/ARCHITECTURE.md +302 -0
  127. package/src/services/ai-docs/AGENTS.md +384 -0
  128. package/src/services/config/Util.ts +2 -2
  129. package/src/services/config/ai-docs/AGENTS.md +253 -0
  130. package/src/services/config/ai-docs/ARCHITECTURE.md +424 -0
  131. package/src/services/config/types.ts +116 -10
  132. package/src/services/core/Utils.ts +85 -34
  133. package/src/services/core/ai-docs/AGENTS.md +379 -0
  134. package/src/services/core/ai-docs/ARCHITECTURE.md +696 -0
  135. package/src/services/core/websocket/WebSocketManager.ts +2 -0
  136. package/src/services/core/websocket/types.ts +1 -1
  137. package/src/services/index.ts +1 -1
  138. package/src/services/task/Task.ts +837 -0
  139. package/src/services/task/TaskFactory.ts +55 -0
  140. package/src/services/task/TaskManager.ts +738 -697
  141. package/src/services/task/TaskUtils.ts +205 -25
  142. package/src/services/task/ai-docs/AGENTS.md +455 -0
  143. package/src/services/task/ai-docs/ARCHITECTURE.md +585 -0
  144. package/src/services/task/constants.ts +5 -0
  145. package/src/services/task/digital/Digital.ts +95 -0
  146. package/src/services/task/state-machine/TaskStateMachine.ts +1077 -0
  147. package/src/services/task/state-machine/actions.ts +685 -0
  148. package/src/services/task/state-machine/ai-docs/AGENTS.md +495 -0
  149. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1135 -0
  150. package/src/services/task/state-machine/constants.ts +172 -0
  151. package/src/services/task/state-machine/guards.ts +406 -0
  152. package/src/services/task/state-machine/index.ts +28 -0
  153. package/src/services/task/state-machine/types.ts +241 -0
  154. package/src/services/task/state-machine/uiControlsComputer.ts +867 -0
  155. package/src/services/task/taskDataNormalizer.ts +137 -0
  156. package/src/services/task/types.ts +654 -85
  157. package/src/services/task/voice/Voice.ts +1267 -0
  158. package/src/services/task/voice/WebRTC.ts +187 -0
  159. package/src/types.ts +112 -1
  160. package/src/utils/AGENTS.md +276 -0
  161. package/src/webex.js +2 -0
  162. package/test/unit/spec/cc.ts +133 -3
  163. package/test/unit/spec/logger-proxy.ts +70 -0
  164. package/test/unit/spec/services/ApiAiAssistant.ts +122 -17
  165. package/test/unit/spec/services/WebCallingService.ts +7 -1
  166. package/test/unit/spec/services/config/index.ts +27 -27
  167. package/test/unit/spec/services/core/Utils.ts +335 -1
  168. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +66 -40
  169. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  170. package/test/unit/spec/services/task/Task.ts +477 -0
  171. package/test/unit/spec/services/task/TaskFactory.ts +62 -0
  172. package/test/unit/spec/services/task/TaskManager.ts +821 -1936
  173. package/test/unit/spec/services/task/TaskUtils.ts +206 -0
  174. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  175. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +1825 -0
  176. package/test/unit/spec/services/task/state-machine/guards.ts +479 -0
  177. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  178. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +2020 -0
  179. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  180. package/test/unit/spec/services/task/voice/Voice.ts +631 -0
  181. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  182. package/umd/contact-center.min.js +2 -2
  183. package/umd/contact-center.min.js.map +1 -1
  184. package/dist/services/task/index.js +0 -1530
  185. package/dist/services/task/index.js.map +0 -1
  186. package/dist/types/services/task/index.d.ts +0 -650
  187. package/src/services/task/index.ts +0 -1806
  188. package/test/unit/spec/services/task/index.ts +0 -2205
@@ -5,16 +5,21 @@ import {CALL_EVENT_KEYS, CallingClientConfig, LINE_EVENTS} from '@webex/calling'
5
5
  import {CC_AGENT_EVENTS, CC_EVENTS} from '../../../../../src/services/config/types';
6
6
  import TaskManager from '../../../../../src/services/task/TaskManager';
7
7
  import * as contact from '../../../../../src/services/task/contact';
8
- import Task from '../../../../../src/services/task';
8
+ import Task from '../../../../../src/services/task/Task';
9
9
  import {TASK_EVENTS} from '../../../../../src/services/task/types';
10
+ import {TaskEvent} from '../../../../../src/services/task/state-machine';
11
+ import WebRTC from '../../../../../src/services/task/voice/WebRTC';
12
+ import {Profile} from '../../../../../src/services/config/types';
10
13
  import WebCallingService from '../../../../../src/services/WebCallingService';
11
14
  import config from '../../../../../src/config';
12
15
  import {CC_TASK_EVENTS} from '../../../../../src/services/config/types';
16
+ import TaskFactory from '../../../../../src/services/task/TaskFactory';
13
17
 
14
18
  describe('TaskManager', () => {
15
19
  let mockCall;
16
20
  let mockApiAIAssistant;
17
21
  let webSocketManagerMock;
22
+ let rtdWebSocketManagerMock;
18
23
  let onSpy;
19
24
  let offSpy;
20
25
  let taskManager;
@@ -24,6 +29,108 @@ describe('TaskManager', () => {
24
29
  let webex: WebexSDK;
25
30
  const taskId = '0ae913a4-c857-4705-8d49-76dd3dde75e4';
26
31
 
32
+ const createMockTask = (data = taskDataMock) => {
33
+ const task = new EventEmitter() as any;
34
+
35
+ const updateTaskData = jest.fn().mockImplementation((newData) => {
36
+ task.data = {...task.data, ...newData};
37
+ return task;
38
+ });
39
+
40
+ Object.assign(task, {
41
+ data,
42
+ accept: jest.fn(),
43
+ decline: jest.fn(),
44
+ updateTaskData,
45
+ unregisterWebCallListeners: jest.fn(),
46
+ cancelAutoWrapupTimer: jest.fn(),
47
+ });
48
+
49
+ const taskEventMap: Partial<Record<TaskEvent, string>> = {
50
+ [TaskEvent.TASK_INCOMING]: TASK_EVENTS.TASK_INCOMING,
51
+ [TaskEvent.TASK_OFFERED]: TASK_EVENTS.TASK_OFFER_CONTACT,
52
+ [TaskEvent.OFFER_CONSULT]: TASK_EVENTS.TASK_OFFER_CONSULT,
53
+ [TaskEvent.HYDRATE]: TASK_EVENTS.TASK_HYDRATE,
54
+ [TaskEvent.ASSIGN]: TASK_EVENTS.TASK_ASSIGNED,
55
+ [TaskEvent.HOLD_SUCCESS]: TASK_EVENTS.TASK_HOLD,
56
+ [TaskEvent.UNHOLD_SUCCESS]: TASK_EVENTS.TASK_RESUME,
57
+ [TaskEvent.CONSULT_CREATED]: TASK_EVENTS.TASK_CONSULT_CREATED,
58
+ [TaskEvent.CONSULTING_ACTIVE]: TASK_EVENTS.TASK_CONSULT_ACCEPTED,
59
+ [TaskEvent.CONSULT_END]: TASK_EVENTS.TASK_CONSULT_END,
60
+ [TaskEvent.CONSULT_FAILED]: CC_EVENTS.AGENT_CONSULT_FAILED,
61
+ [TaskEvent.CTQ_CANCEL]: TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED,
62
+ [TaskEvent.CTQ_CANCEL_FAILED]: TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED,
63
+ [TaskEvent.END]: TASK_EVENTS.TASK_END,
64
+ [TaskEvent.CONTACT_ENDED]: TASK_EVENTS.TASK_END,
65
+ [TaskEvent.ASSIGN_FAILED]: TASK_EVENTS.TASK_REJECT,
66
+ [TaskEvent.INVITE_FAILED]: TASK_EVENTS.TASK_REJECT,
67
+ [TaskEvent.RONA]: TASK_EVENTS.TASK_REJECT,
68
+ [TaskEvent.OUTBOUND_FAILED]: TASK_EVENTS.TASK_OUTDIAL_FAILED,
69
+ [TaskEvent.RECORDING_STARTED]: TASK_EVENTS.TASK_RECORDING_STARTED,
70
+ [TaskEvent.PAUSE_RECORDING]: TASK_EVENTS.TASK_RECORDING_PAUSED,
71
+ [TaskEvent.RESUME_RECORDING]: TASK_EVENTS.TASK_RECORDING_RESUMED,
72
+ [TaskEvent.WRAPUP_COMPLETE]: TASK_EVENTS.TASK_WRAPPEDUP,
73
+ };
74
+
75
+ task.sendStateMachineEvent = jest.fn().mockImplementation((event) => {
76
+ if (event.taskData) {
77
+ task.updateTaskData(event.taskData);
78
+ }
79
+
80
+ const mappedEvent = taskEventMap[event.type as TaskEvent];
81
+ if (mappedEvent) {
82
+ if (
83
+ [TaskEvent.ASSIGN_FAILED, TaskEvent.RONA, TaskEvent.INVITE_FAILED].includes(
84
+ event.type as TaskEvent
85
+ )
86
+ ) {
87
+ task.emit(mappedEvent, event.reason ?? event.taskData?.reason);
88
+ } else if (event.type === TaskEvent.OUTBOUND_FAILED) {
89
+ task.emit(mappedEvent, event.reason);
90
+ } else {
91
+ task.emit(mappedEvent, task);
92
+ }
93
+ }
94
+
95
+ if ([TaskEvent.ASSIGN, TaskEvent.CONSULTING_ACTIVE].includes(event.type as TaskEvent)) {
96
+ task.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, task);
97
+ }
98
+
99
+ // Auto-answer is now handled at the Task layer (triggered by state machine actions)
100
+ if (
101
+ [TaskEvent.TASK_OFFERED, TaskEvent.OFFER_CONSULT].includes(event.type as TaskEvent) &&
102
+ (event.taskData?.isAutoAnswering === true || event.taskData?.isAutoAnswering === 'true')
103
+ ) {
104
+ Promise.resolve(task.accept())
105
+ .then(() => {
106
+ task.emit(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
107
+ })
108
+ .catch(() => undefined);
109
+ }
110
+
111
+ // Cleanup is now emitted by state machine actions (Task layer).
112
+ // Simulate the TASK_CLEANUP emission for unit tests using mock tasks.
113
+ const eventType = event.type as TaskEvent;
114
+ const shouldCleanup =
115
+ eventType === TaskEvent.CONTACT_ENDED ||
116
+ eventType === TaskEvent.END ||
117
+ eventType === TaskEvent.TASK_WRAPUP ||
118
+ eventType === TaskEvent.WRAPUP_COMPLETE ||
119
+ eventType === TaskEvent.ASSIGN_FAILED ||
120
+ eventType === TaskEvent.INVITE_FAILED ||
121
+ eventType === TaskEvent.RONA ||
122
+ eventType === TaskEvent.OUTBOUND_FAILED ||
123
+ (eventType === TaskEvent.CONSULT_END && task.data?.isConsulted === true);
124
+
125
+ if (shouldCleanup) {
126
+ const removeFromCollection = eventType !== TaskEvent.CONTACT_ENDED;
127
+ task.emit(TASK_EVENTS.TASK_CLEANUP, task, {removeFromCollection});
128
+ }
129
+ });
130
+
131
+ return task;
132
+ };
133
+
27
134
  taskDataMock = {
28
135
  type: CC_EVENTS.AGENT_CONTACT_RESERVED,
29
136
  agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
@@ -43,18 +150,21 @@ describe('TaskManager', () => {
43
150
  data: taskDataMock,
44
151
  };
45
152
 
153
+ const expectLastStateMachineEvent = (
154
+ spy: jest.SpyInstance | jest.Mock,
155
+ expectedType: TaskEvent
156
+ ) => {
157
+ expect(spy).toHaveBeenCalled();
158
+ const lastCall = spy.mock.calls[spy.mock.calls.length - 1] || [];
159
+ const event = lastCall[3]?.type ? lastCall[3] : lastCall[0];
160
+ expect(event?.type).toBe(expectedType);
161
+ return event;
162
+ };
163
+
46
164
  beforeEach(() => {
47
165
  contactMock = contact;
48
166
  webSocketManagerMock = new EventEmitter();
49
- mockApiAIAssistant = {
50
- sendEvent: jest.fn().mockResolvedValue({}),
51
- setAIFeatureFlags: jest.fn(),
52
- aiFeature: {
53
- realtimeTranscripts: {
54
- enable: true,
55
- },
56
- },
57
- };
167
+ rtdWebSocketManagerMock = new EventEmitter();
58
168
 
59
169
  webex = {
60
170
  logger: {
@@ -84,25 +194,25 @@ describe('TaskManager', () => {
84
194
  onSpy = jest.spyOn(webCallingService, 'on');
85
195
  offSpy = jest.spyOn(webCallingService, 'off');
86
196
 
197
+ mockApiAIAssistant = {
198
+ sendEvent: jest.fn().mockResolvedValue({}),
199
+ };
200
+
87
201
  taskManager = new TaskManager(
88
- mockApiAIAssistant,
202
+ mockApiAIAssistant as any,
89
203
  contactMock,
90
204
  webCallingService,
91
- webSocketManagerMock
205
+ webSocketManagerMock as any,
206
+ rtdWebSocketManagerMock as any
92
207
  );
93
- const taskMock = {
94
- emit: jest.fn(),
95
- accept: jest.fn(),
96
- decline: jest.fn(),
97
- updateTaskData: jest.fn().mockImplementation((updatedData) => {
98
- taskMock.data = {...taskMock.data, ...updatedData};
99
- return taskMock;
100
- }),
101
- data: taskDataMock,
102
- };
103
- taskManager.taskCollection[taskId] = taskMock;
104
- taskManager.agentId = 'test-agent-id';
208
+ taskManager.taskCollection[taskId] = createMockTask(taskDataMock);
209
+ (taskManager as any).setupTaskListeners?.(taskManager.taskCollection[taskId]);
105
210
  taskManager.call = mockCall;
211
+ taskManager.setAgentId('test-agent-id');
212
+
213
+ jest
214
+ .spyOn(TaskFactory, 'createTask')
215
+ .mockImplementation((contact, webCallingService, data, configFlags) => createMockTask(data));
106
216
  });
107
217
 
108
218
  afterEach(() => {
@@ -113,7 +223,8 @@ describe('TaskManager', () => {
113
223
  it('should initialize TaskManager and register listeners', () => {
114
224
  webSocketManagerMock.emit('message', JSON.stringify({data: taskDataMock}));
115
225
  const incomingCallCb = onSpy.mock.calls[0][1];
116
- const taskEmitSpy = jest.spyOn(taskManager, 'emit');
226
+ const incomingHandler = jest.fn();
227
+ taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
117
228
 
118
229
  expect(taskManager).toBeInstanceOf(TaskManager);
119
230
  expect(webCallingService.listenerCount(LINE_EVENTS.INCOMING_CALL)).toBe(1);
@@ -122,10 +233,8 @@ describe('TaskManager', () => {
122
233
 
123
234
  incomingCallCb(mockCall);
124
235
 
125
- expect(taskEmitSpy).toHaveBeenCalledWith(
126
- TASK_EVENTS.TASK_INCOMING,
127
- taskManager.getTask(taskId)
128
- );
236
+ expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(taskId));
237
+ taskManager.off(TASK_EVENTS.TASK_INCOMING, incomingHandler);
129
238
  });
130
239
 
131
240
  it('should re-emit task related events', () => {
@@ -141,15 +250,32 @@ describe('TaskManager', () => {
141
250
 
142
251
  webSocketManagerMock.emit('message', JSON.stringify(dummyPayload));
143
252
 
144
- expect(taskEmitSpy).toHaveBeenCalledWith(dummyPayload.data.type, dummyPayload.data);
253
+ expect(taskEmitSpy).toHaveBeenCalledWith(
254
+ TASK_EVENTS.TASK_CONSULT_ACCEPTED,
255
+ taskManager.getTask(taskId)
256
+ );
145
257
  });
146
258
 
147
259
  it('should invoke sendEvent for configured start/stop backend events', () => {
260
+ taskManager.setConfigFlags({
261
+ isEndTaskEnabled: true,
262
+ isEndConsultEnabled: true,
263
+ webRtcEnabled: true,
264
+ autoWrapup: false,
265
+ aiFeature: {
266
+ id: 'ai-feature-1',
267
+ realtimeTranscripts: {
268
+ enable: true,
269
+ },
270
+ },
271
+ });
272
+
273
+ const interactionId = taskId;
148
274
  const message = (type: CC_EVENTS) =>
149
275
  JSON.stringify({
150
276
  data: {
151
277
  ...taskDataMock,
152
- taskId,
278
+ interactionId,
153
279
  type,
154
280
  },
155
281
  });
@@ -158,67 +284,114 @@ describe('TaskManager', () => {
158
284
  webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
159
285
  webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_CONFERENCED));
160
286
  webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_ENDED));
161
- webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
162
287
  webSocketManagerMock.emit('message', message(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE));
288
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
163
289
 
164
290
  expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledTimes(6);
165
291
  expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledWith(
166
292
  'test-agent-id',
167
- taskId,
293
+ interactionId,
168
294
  'CUSTOM_EVENT',
169
295
  'GET_TRANSCRIPTS',
170
296
  'START'
171
297
  );
172
298
  expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledWith(
173
299
  'test-agent-id',
174
- taskId,
300
+ interactionId,
175
301
  'CUSTOM_EVENT',
176
302
  'GET_TRANSCRIPTS',
177
303
  'STOP'
178
304
  );
179
305
  });
180
306
 
181
- it('should not invoke sendEvent for transcript events when realtime transcript feature is disabled', () => {
182
- mockApiAIAssistant.aiFeature = {
183
- realtimeTranscripts: {
184
- enable: false,
307
+ it('should not invoke sendEvent when realtime transcripts are disabled in aiFeature', () => {
308
+ taskManager.setConfigFlags({
309
+ isEndTaskEnabled: true,
310
+ isEndConsultEnabled: true,
311
+ webRtcEnabled: true,
312
+ autoWrapup: false,
313
+ aiFeature: {
314
+ id: 'ai-feature-1',
315
+ realtimeTranscripts: {
316
+ enable: false,
317
+ },
185
318
  },
186
- };
187
- mockApiAIAssistant.setAIFeatureFlags(mockApiAIAssistant.aiFeature);
319
+ });
188
320
 
189
321
  const message = (type: CC_EVENTS) =>
190
322
  JSON.stringify({
191
323
  data: {
192
324
  ...taskDataMock,
193
- taskId,
325
+ interactionId: taskId,
194
326
  type,
195
327
  },
196
328
  });
197
329
 
198
330
  webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONTACT_ASSIGNED));
199
331
  webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
200
- webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_CONFERENCED));
201
- webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_ENDED));
202
- webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
203
- webSocketManagerMock.emit('message', message(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE));
204
332
 
205
333
  expect(mockApiAIAssistant.sendEvent).not.toHaveBeenCalled();
206
334
  });
207
335
 
208
- it('should emit REAL_TIME_TRANSCRIPTION from task object', () => {
336
+ it('should not invoke sendEvent when realtime transcripts config is missing', () => {
337
+ taskManager.setConfigFlags({
338
+ isEndTaskEnabled: true,
339
+ isEndConsultEnabled: true,
340
+ webRtcEnabled: true,
341
+ autoWrapup: false,
342
+ aiFeature: {
343
+ id: 'ai-feature-1',
344
+ suggestedResponses: {
345
+ enable: true,
346
+ },
347
+ },
348
+ });
349
+
350
+ const message = (type: CC_EVENTS) =>
351
+ JSON.stringify({
352
+ data: {
353
+ ...taskDataMock,
354
+ interactionId: taskId,
355
+ type,
356
+ },
357
+ });
358
+
359
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONTACT_ASSIGNED));
360
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
361
+
362
+ expect(mockApiAIAssistant.sendEvent).not.toHaveBeenCalled();
363
+ });
364
+
365
+ it('should emit REAL_TIME_TRANSCRIPTION from RTD websocket payload', () => {
209
366
  const task = taskManager.getTask(taskId);
210
367
  const taskEmitSpy = jest.spyOn(task, 'emit');
211
368
  const realtimePayload = {
212
369
  data: {
213
- ...taskDataMock,
214
- type: CC_EVENTS.REAL_TIME_TRANSCRIPTION,
370
+ agentId: 'test-agent-id',
215
371
  data: {
216
- content: 'hello from transcript',
372
+ content: 'Thank you. Okay.',
373
+ conversationId: taskId,
374
+ isFinal: true,
375
+ languageCode: 'en-US',
376
+ messageId: '1',
377
+ orgId: 'org-id',
378
+ publishTimestamp: 1773807297475,
379
+ role: 'AGENT',
380
+ trackingId: 'tracking-id',
381
+ utteranceId: 'utterance-id',
382
+ },
383
+ notifDetails: {
384
+ actionEvent: 'REAL_TIME_TRANSCRIPTION',
217
385
  },
386
+ notifType: 'REAL_TIME_TRANSCRIPTION',
387
+ orgId: 'org-id',
218
388
  },
389
+ orgId: 'org-id',
390
+ trackingId: 'notifs_tracking-id',
391
+ type: 'REAL_TIME_TRANSCRIPTION',
219
392
  };
220
393
 
221
- webSocketManagerMock.emit('message', JSON.stringify(realtimePayload));
394
+ taskManager.handleRealtimeWebsocketEvent(JSON.stringify(realtimePayload));
222
395
 
223
396
  expect(taskEmitSpy).toHaveBeenCalledWith(
224
397
  CC_EVENTS.REAL_TIME_TRANSCRIPTION,
@@ -226,22 +399,37 @@ describe('TaskManager', () => {
226
399
  );
227
400
  });
228
401
 
229
- it('should emit REAL_TIME_TRANSCRIPTION from RTD websocket payload on task object', () => {
230
- const task = taskManager.getTask(taskId);
231
- const taskEmitSpy = jest.spyOn(task, 'emit');
402
+ it('should ignore RTD transcript events when task is not found', () => {
232
403
  const realtimePayload = {
233
404
  data: {
234
- notifType: CC_EVENTS.REAL_TIME_TRANSCRIPTION,
235
405
  data: {
236
- conversationId: taskId,
237
- content: 'hello from rtd websocket',
406
+ content: 'Thank you. Okay.',
407
+ conversationId: 'missing-task-id',
408
+ isFinal: true,
409
+ languageCode: 'en-US',
410
+ messageId: '1',
411
+ orgId: 'org-id',
412
+ publishTimestamp: 1773807297475,
413
+ role: 'AGENT',
414
+ trackingId: 'tracking-id',
415
+ utteranceId: 'utterance-id',
238
416
  },
417
+ notifDetails: {
418
+ actionEvent: 'REAL_TIME_TRANSCRIPTION',
419
+ },
420
+ notifType: 'REAL_TIME_TRANSCRIPTION',
421
+ orgId: 'org-id',
239
422
  },
423
+ orgId: 'org-id',
424
+ trackingId: 'notifs_tracking-id',
425
+ type: 'REAL_TIME_TRANSCRIPTION',
240
426
  };
241
427
 
428
+ const existingTaskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
429
+
242
430
  taskManager.handleRealtimeWebsocketEvent(JSON.stringify(realtimePayload));
243
431
 
244
- expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.REAL_TIME_TRANSCRIPTION, realtimePayload.data);
432
+ expect(existingTaskEmitSpy).not.toHaveBeenCalled();
245
433
  });
246
434
 
247
435
  it('should not re-emit agent related events', () => {
@@ -259,7 +447,7 @@ describe('TaskManager', () => {
259
447
 
260
448
  webSocketManagerMock.emit('message', JSON.stringify(dummyPayload));
261
449
 
262
- expect(taskEmitSpy).not.toHaveBeenCalledWith(dummyPayload.data.type, dummyPayload.data);
450
+ expect(taskEmitSpy).not.toHaveBeenCalled();
263
451
  });
264
452
 
265
453
  it('should handle WebSocket message for AGENT_CONTACT_RESERVED and emit task:incoming for browser case', () => {
@@ -280,14 +468,12 @@ describe('TaskManager', () => {
280
468
  },
281
469
  };
282
470
 
283
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
471
+ const incomingHandler = jest.fn();
472
+ taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
284
473
 
285
474
  webSocketManagerMock.emit('message', JSON.stringify(payload));
286
475
 
287
- expect(taskIncomingSpy).toHaveBeenCalledWith(
288
- TASK_EVENTS.TASK_INCOMING,
289
- taskManager.getTask(payload.data.interactionId)
290
- );
476
+ expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(payload.data.interactionId));
291
477
  expect(taskManager.getTask(payload.data.interactionId)).toBe(taskManager.getTask(taskId));
292
478
  expect(taskManager.getAllTasks()).toHaveProperty(payload.data.interactionId);
293
479
 
@@ -312,6 +498,7 @@ describe('TaskManager', () => {
312
498
  taskManager.getTask(payload.data.interactionId),
313
499
  'emit'
314
500
  );
501
+ const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
315
502
 
316
503
  webSocketManagerMock.emit('message', JSON.stringify(assignedPayload));
317
504
 
@@ -319,7 +506,7 @@ describe('TaskManager', () => {
319
506
  TASK_EVENTS.TASK_ASSIGNED,
320
507
  taskManager.getTask(taskId)
321
508
  );
322
- expect(taskIncomingSpy).toHaveBeenCalledWith(
509
+ expect(taskManagerEmitSpy).toHaveBeenCalledWith(
323
510
  TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
324
511
  taskManager.getTask(taskId)
325
512
  );
@@ -344,16 +531,64 @@ describe('TaskManager', () => {
344
531
  },
345
532
  };
346
533
 
347
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
534
+ const incomingHandler = jest.fn();
535
+ taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
348
536
 
349
537
  webSocketManagerMock.emit('message', JSON.stringify(payload));
350
538
 
351
- expect(taskIncomingSpy).toHaveBeenCalledWith(
352
- TASK_EVENTS.TASK_INCOMING,
353
- taskManager.getTask(taskId)
354
- );
539
+ expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(taskId));
355
540
  expect(taskManager.getTask(payload.data.interactionId)).toBe(taskManager.getTask(taskId));
356
541
  expect(taskManager.getAllTasks()).toHaveProperty(payload.data.interactionId);
542
+ taskManager.off(TASK_EVENTS.TASK_INCOMING, incomingHandler);
543
+ });
544
+
545
+ it('should send mapped events through the state machine without duplicate updates', () => {
546
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
547
+ const task = taskManager.getTask(taskId);
548
+ const updateSpy = task.updateTaskData as jest.Mock;
549
+ updateSpy.mockClear();
550
+ const sendSpy = task.sendStateMachineEvent as jest.Mock;
551
+ sendSpy.mockClear();
552
+ const cleanupSpy = jest.spyOn(taskManager as any, 'handleTaskCleanup');
553
+
554
+ const assignFailedPayload = {
555
+ data: {
556
+ ...initalPayload.data,
557
+ type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
558
+ reason: 'ASSIGN_FAILED',
559
+ },
560
+ };
561
+
562
+ webSocketManagerMock.emit('message', JSON.stringify(assignFailedPayload));
563
+
564
+ const stateMachineEvent = expectLastStateMachineEvent(sendSpy, TaskEvent.ASSIGN_FAILED);
565
+ expect(stateMachineEvent).toEqual({
566
+ type: TaskEvent.ASSIGN_FAILED,
567
+ reason: assignFailedPayload.data.reason,
568
+ });
569
+ expect(updateSpy).toHaveBeenCalledWith(assignFailedPayload.data);
570
+ expect(cleanupSpy).toHaveBeenCalledWith(task);
571
+ });
572
+
573
+ it('should update task data directly when no state machine mapping exists', () => {
574
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
575
+ const task = taskManager.getTask(taskId);
576
+ const updateSpy = task.updateTaskData as jest.Mock;
577
+ updateSpy.mockClear();
578
+ const sendSpy = task.sendStateMachineEvent as jest.Mock;
579
+ sendSpy.mockClear();
580
+
581
+ const participantMovedPayload = {
582
+ data: {
583
+ ...initalPayload.data,
584
+ type: CC_EVENTS.CONSULTED_PARTICIPANT_MOVING,
585
+ },
586
+ };
587
+
588
+ webSocketManagerMock.emit('message', JSON.stringify(participantMovedPayload));
589
+
590
+ expect(sendSpy).not.toHaveBeenCalled();
591
+ expect(updateSpy).toHaveBeenCalledWith(participantMovedPayload.data);
357
592
  });
358
593
 
359
594
  it('should return task by ID', () => {
@@ -438,12 +673,30 @@ describe('TaskManager', () => {
438
673
  it('test call listeners being switched off on call end', () => {
439
674
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
440
675
 
441
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
442
- const webCallListenerSpy = jest.spyOn(
443
- taskManager.getTask(taskId),
444
- 'unregisterWebCallListeners'
445
- );
676
+ const webrtcTask = new WebRTC(contactMock, webCallingService, taskDataMock, {
677
+ isEndTaskEnabled: true,
678
+ isEndConsultEnabled: true,
679
+ });
680
+ (taskManager as any).taskCollection[taskId] = webrtcTask;
681
+ // TaskManager must listen to task-level cleanup events emitted by the state machine.
682
+ // This is normally wired when TaskManager creates the task via TaskFactory.
683
+ (taskManager as any).setupTaskListeners(webrtcTask);
684
+
685
+ const task = taskManager.getTask(taskId)!;
686
+ // This test doesn't validate UI controls; avoid requiring full interaction.media
687
+ // shape for WebRTC UI controls computation.
688
+ jest.spyOn(task as any, 'updateUiControls').mockImplementation(() => undefined);
689
+ const originalEmit = task.emit;
690
+ jest.spyOn(task, 'emit').mockImplementation((event, arg) => {
691
+ if (event === CC_EVENTS.CONTACT_ENDED) {
692
+ return;
693
+ }
694
+ return originalEmit.call(task, event, arg);
695
+ });
696
+
697
+ const webCallListenerSpy = jest.spyOn(task, 'unregisterWebCallListeners');
446
698
  const callOffSpy = jest.spyOn(mockCall, 'off');
699
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
447
700
  const payload = {
448
701
  data: {
449
702
  type: CC_EVENTS.CONTACT_ENDED,
@@ -461,10 +714,25 @@ describe('TaskManager', () => {
461
714
  },
462
715
  };
463
716
 
717
+ // Ensure the state machine is hydrated into a connected state before CONTACT_ENDED
718
+ const hydratePayload = {
719
+ data: {
720
+ ...payload.data,
721
+ type: CC_EVENTS.AGENT_CONTACT,
722
+ interaction: {state: 'connected', mediaType: 'telephony'},
723
+ },
724
+ };
725
+
726
+ taskManager.getTask(taskId).data = hydratePayload.data;
727
+ webSocketManagerMock.emit('message', JSON.stringify(hydratePayload));
728
+
464
729
  taskManager.getTask(taskId).data = payload.data;
465
- const task = taskManager.getTask(taskId);
466
730
  webSocketManagerMock.emit('message', JSON.stringify(payload));
467
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
731
+ const stateMachineEvent = expectLastStateMachineEvent(
732
+ sendStateMachineEventSpy,
733
+ TaskEvent.CONTACT_ENDED
734
+ );
735
+ expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(false);
468
736
  expect(webCallListenerSpy).toHaveBeenCalledWith();
469
737
  expect(callOffSpy).toHaveBeenCalledWith(
470
738
  CALL_EVENT_KEYS.REMOTE_MEDIA,
@@ -475,12 +743,13 @@ describe('TaskManager', () => {
475
743
  expect(offSpy.mock.calls.length).toBe(2); // 1 for incoming call and 1 for remote media
476
744
  expect(offSpy).toHaveBeenCalledWith(CALL_EVENT_KEYS.REMOTE_MEDIA, offSpy.mock.calls[0][1]);
477
745
  expect(offSpy).toHaveBeenCalledWith(LINE_EVENTS.INCOMING_CALL, offSpy.mock.calls[1][1]);
746
+ sendStateMachineEventSpy.mockRestore();
478
747
  });
479
748
 
480
749
  it('should emit TASK_END event with wrapupRequired on regular call end', () => {
481
750
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
482
-
483
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
751
+ const task = taskManager.getTask(taskId);
752
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
484
753
  const payload = {
485
754
  data: {
486
755
  type: CC_EVENTS.CONTACT_ENDED,
@@ -495,20 +764,25 @@ describe('TaskManager', () => {
495
764
  destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
496
765
  owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
497
766
  queueMgr: 'aqm',
767
+ agentsPendingWrapUp: ['test-agent-id'],
498
768
  },
499
769
  };
500
770
 
501
- taskManager.getTask(taskId).updateTaskData(payload.data);
771
+ task.updateTaskData(payload.data);
502
772
  webSocketManagerMock.emit('message', JSON.stringify(payload));
503
- expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.CONTACT_ENDED, {...payload.data});
504
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, taskManager.getTask(taskId));
773
+ const stateMachineEvent = expectLastStateMachineEvent(
774
+ sendStateMachineEventSpy,
775
+ TaskEvent.CONTACT_ENDED
776
+ );
777
+ expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(true);
778
+ sendStateMachineEventSpy.mockRestore();
505
779
  });
506
780
 
507
781
  it('should emit TASK_REJECT event on AGENT_INVITE_FAILED event', () => {
508
782
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
509
783
 
510
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
511
- const metricsTrackSpy = jest.spyOn(taskManager.metricsManager, 'trackEvent');
784
+ const task = taskManager.getTask(taskId);
785
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
512
786
  const payload = {
513
787
  data: {
514
788
  type: CC_EVENTS.AGENT_INVITE_FAILED,
@@ -527,35 +801,40 @@ describe('TaskManager', () => {
527
801
  },
528
802
  };
529
803
 
530
- taskManager.getTask(taskId).updateTaskData(payload.data);
804
+ task.updateTaskData(payload.data);
531
805
  webSocketManagerMock.emit('message', JSON.stringify(payload));
532
- expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.AGENT_INVITE_FAILED, {...payload.data});
533
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_REJECT, payload.data.reason);
534
- // Verify the correct metric event name is used for AGENT_INVITE_FAILED
535
- expect(metricsTrackSpy).toHaveBeenCalled();
536
- expect(metricsTrackSpy.mock.calls[0][0]).toBe('Agent Invite Failed');
806
+ const stateMachineEvent = expectLastStateMachineEvent(
807
+ sendStateMachineEventSpy,
808
+ TaskEvent.INVITE_FAILED
809
+ );
810
+ expect(stateMachineEvent?.reason).toBe(payload.data.reason);
811
+ sendStateMachineEventSpy.mockRestore();
537
812
  });
538
813
 
539
- it('should not emit TASK_HYDRATE if task is already present in taskManager', () => {
814
+ it('should emit TASK_HYDRATE even if task is already present in taskManager', () => {
540
815
  const payload = {
541
816
  data: {
542
817
  ...initalPayload.data,
543
818
  type: CC_EVENTS.AGENT_CONTACT,
544
819
  },
545
820
  };
546
- const taskEmitSpy = jest.spyOn(taskManager, 'emit');
821
+ const existingTask = taskManager.getTask(taskId);
822
+ const sendStateMachineEventSpy = jest.spyOn(existingTask, 'sendStateMachineEvent');
547
823
  webSocketManagerMock.emit('message', JSON.stringify(payload));
548
824
 
549
- expect(taskEmitSpy).not.toHaveBeenCalledWith(
550
- TASK_EVENTS.TASK_HYDRATE,
551
- taskManager.getTask(taskId)
825
+ const stateMachineEvent = expectLastStateMachineEvent(
826
+ sendStateMachineEventSpy,
827
+ TaskEvent.HYDRATE
552
828
  );
829
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
830
+ expect(existingTask).toBe(taskManager.getTask(taskId));
553
831
  expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
554
832
  taskManager.getTask(taskId)
555
833
  );
834
+ sendStateMachineEventSpy.mockRestore();
556
835
  });
557
836
 
558
- it('should emit TASK_INCOMING event on AGENT_CONTACT event if task is new and not in the taskManager ', () => {
837
+ it('should emit TASK_HYDRATE event on AGENT_CONTACT when task is created from payload', () => {
559
838
  taskManager.taskCollection = [];
560
839
  const payload = {
561
840
  data: {
@@ -565,13 +844,15 @@ describe('TaskManager', () => {
565
844
  },
566
845
  };
567
846
 
568
- const taskEmitSpy = jest.spyOn(taskManager, 'emit');
569
847
  webSocketManagerMock.emit('message', JSON.stringify(payload));
570
848
 
571
- expect(taskEmitSpy).toHaveBeenCalledWith(
572
- TASK_EVENTS.TASK_INCOMING,
573
- taskManager.getTask(taskId)
849
+ const createdTask = taskManager.getTask(taskId);
850
+ const sendStateMachineEventSpy = createdTask.sendStateMachineEvent as jest.Mock;
851
+ const stateMachineEvent = expectLastStateMachineEvent(
852
+ sendStateMachineEventSpy,
853
+ TaskEvent.HYDRATE
574
854
  );
855
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
575
856
  expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
576
857
  taskManager.getTask(taskId)
577
858
  );
@@ -586,10 +867,15 @@ describe('TaskManager', () => {
586
867
  },
587
868
  };
588
869
 
589
- const taskEmitSpy = jest.spyOn(taskManager, 'emit');
590
870
  webSocketManagerMock.emit('message', JSON.stringify(payload));
591
871
 
592
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_HYDRATE, taskManager.getTask(taskId));
872
+ const createdTask = taskManager.getTask(taskId);
873
+ const sendStateMachineEventSpy = createdTask.sendStateMachineEvent as jest.Mock;
874
+ const stateMachineEvent = expectLastStateMachineEvent(
875
+ sendStateMachineEventSpy,
876
+ TaskEvent.HYDRATE
877
+ );
878
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
593
879
  expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
594
880
  taskManager.getTask(taskId)
595
881
  );
@@ -662,7 +948,7 @@ describe('TaskManager', () => {
662
948
  expect(createdTask.data.isConferenceInProgress).toBe(false);
663
949
  });
664
950
 
665
- it('should emit TASK_END event on AGENT_WRAPUP event', () => {
951
+ it('should emit TASK_WRAPUP event on AGENT_WRAPUP event', () => {
666
952
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
667
953
 
668
954
  const wrapupPayload = {
@@ -684,13 +970,15 @@ describe('TaskManager', () => {
684
970
  };
685
971
 
686
972
  const task = taskManager.getTask(taskId);
687
- const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
688
- const taskEmitSpy = jest.spyOn(task, 'emit');
973
+ const updateTaskDataSpy = task.updateTaskData as jest.Mock;
974
+ updateTaskDataSpy.mockClear();
975
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
689
976
 
690
977
  webSocketManagerMock.emit('message', JSON.stringify(wrapupPayload));
691
978
 
692
979
  expect(updateTaskDataSpy).toHaveBeenCalledWith(wrapupPayload.data);
693
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
980
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TASK_WRAPUP);
981
+ sendStateMachineEventSpy.mockRestore();
694
982
  });
695
983
 
696
984
  it('should emit TASK_HOLD event on AGENT_CONTACT_HELD event', () => {
@@ -713,13 +1001,20 @@ describe('TaskManager', () => {
713
1001
  },
714
1002
  };
715
1003
 
716
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
717
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1004
+ const task = taskManager.getTask(taskId);
1005
+ const taskUpdateTaskDataSpy = task.updateTaskData as jest.Mock;
1006
+ taskUpdateTaskDataSpy.mockClear();
1007
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
718
1008
 
719
1009
  webSocketManagerMock.emit('message', JSON.stringify(payload));
720
1010
 
721
1011
  expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
722
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_HOLD, taskManager.getTask(taskId));
1012
+ const stateMachineEvent = expectLastStateMachineEvent(
1013
+ sendStateMachineEventSpy,
1014
+ TaskEvent.HOLD_SUCCESS
1015
+ );
1016
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1017
+ sendStateMachineEventSpy.mockRestore();
723
1018
  });
724
1019
 
725
1020
  it('should emit TASK_RESUME event on AGENT_CONTACT_UNHELD event', () => {
@@ -742,11 +1037,18 @@ describe('TaskManager', () => {
742
1037
  },
743
1038
  };
744
1039
 
745
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
746
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1040
+ const task = taskManager.getTask(taskId);
1041
+ const taskUpdateTaskDataSpy = task.updateTaskData as jest.Mock;
1042
+ taskUpdateTaskDataSpy.mockClear();
1043
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
747
1044
  webSocketManagerMock.emit('message', JSON.stringify(payload));
748
1045
  expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
749
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_RESUME, taskManager.getTask(taskId));
1046
+ const stateMachineEvent = expectLastStateMachineEvent(
1047
+ sendStateMachineEventSpy,
1048
+ TaskEvent.UNHOLD_SUCCESS
1049
+ );
1050
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1051
+ sendStateMachineEventSpy.mockRestore();
750
1052
  });
751
1053
 
752
1054
  it('handle AGENT_CONSULT_CREATED event', () => {
@@ -759,17 +1061,16 @@ describe('TaskManager', () => {
759
1061
 
760
1062
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
761
1063
  const task = taskManager.getTask(taskId);
762
- const taskUpdateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
763
- const taskEmitSpy = jest.spyOn(task, 'emit');
1064
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
764
1065
 
765
1066
  webSocketManagerMock.emit('message', JSON.stringify(payload));
766
1067
 
767
- expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith({
768
- ...payload.data,
769
- isConsulted: false,
770
- });
771
- expect(task.data.isConsulted).toBe(false);
772
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONSULT_CREATED, task);
1068
+ const stateMachineEvent = expectLastStateMachineEvent(
1069
+ sendStateMachineEventSpy,
1070
+ TaskEvent.CONSULT_CREATED
1071
+ );
1072
+ expect(stateMachineEvent?.taskData).toEqual({...payload.data, isConsulted: false});
1073
+ sendStateMachineEventSpy.mockRestore();
773
1074
  });
774
1075
 
775
1076
  it('handle AGENT_OFFER_CONTACT event', () => {
@@ -782,11 +1083,17 @@ describe('TaskManager', () => {
782
1083
 
783
1084
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
784
1085
 
785
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1086
+ const task = taskManager.getTask(taskId);
1087
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
786
1088
 
787
1089
  webSocketManagerMock.emit('message', JSON.stringify(payload));
788
1090
 
789
- expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
1091
+ const stateMachineEvent = expectLastStateMachineEvent(
1092
+ sendStateMachineEventSpy,
1093
+ TaskEvent.TASK_OFFERED
1094
+ );
1095
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1096
+ sendStateMachineEventSpy.mockRestore();
790
1097
  });
791
1098
 
792
1099
  describe('Auto-Answer Functionality', () => {
@@ -796,8 +1103,8 @@ describe('TaskManager', () => {
796
1103
 
797
1104
  const task = taskManager.getTask(taskId);
798
1105
  const taskEmitSpy = jest.spyOn(task, 'emit');
799
- const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
800
1106
  const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
1107
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
801
1108
 
802
1109
  // Step 2: Trigger AGENT_OFFER_CONTACT with auto-answer
803
1110
  const autoAnswerPayload = {
@@ -821,9 +1128,14 @@ describe('TaskManager', () => {
821
1128
  // Verify accept was called
822
1129
  expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
823
1130
 
824
- // Verify BOTH events were emitted
825
- expect(taskManagerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OFFER_CONTACT, task);
1131
+ const stateMachineEvent = expectLastStateMachineEvent(
1132
+ sendStateMachineEventSpy,
1133
+ TaskEvent.TASK_OFFERED
1134
+ );
1135
+ expect(stateMachineEvent?.taskData).toEqual(autoAnswerPayload.data);
1136
+ // Verify task auto-answer event was emitted
826
1137
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
1138
+ sendStateMachineEventSpy.mockRestore();
827
1139
  });
828
1140
 
829
1141
  it('should NOT emit TASK_AUTO_ANSWERED event when auto-answer fails', async () => {
@@ -869,6 +1181,7 @@ describe('TaskManager', () => {
869
1181
  const task = taskManager.getTask(taskId);
870
1182
  const taskEmitSpy = jest.spyOn(task, 'emit');
871
1183
  const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
1184
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
872
1185
 
873
1186
  // Step 2: Trigger AGENT_OFFER_CONSULT with auto-answer
874
1187
  const consultAutoAnswerPayload = {
@@ -893,12 +1206,20 @@ describe('TaskManager', () => {
893
1206
  // Verify accept was called
894
1207
  expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
895
1208
 
896
- // Verify BOTH events were emitted
897
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OFFER_CONSULT, task);
1209
+ const stateMachineEvent = expectLastStateMachineEvent(
1210
+ sendStateMachineEventSpy,
1211
+ TaskEvent.OFFER_CONSULT
1212
+ );
1213
+ expect(stateMachineEvent?.taskData).toEqual({
1214
+ ...consultAutoAnswerPayload.data,
1215
+ isConsulted: true,
1216
+ });
1217
+ // Verify task auto-answer event was emitted
898
1218
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
899
1219
 
900
1220
  // Verify isConsulted flag is set correctly
901
1221
  expect(task.data.isConsulted).toBe(true);
1222
+ sendStateMachineEventSpy.mockRestore();
902
1223
  });
903
1224
 
904
1225
  it('should NOT emit TASK_AUTO_ANSWERED when isAutoAnswering is false', async () => {
@@ -939,24 +1260,20 @@ describe('TaskManager', () => {
939
1260
  });
940
1261
  });
941
1262
 
942
- it('should NOT remove OUTDIAL task from taskCollection on AGENT_OUTBOUND_FAILED when terminated (wrap-up flow)', () => {
1263
+ it('should remove OUTDIAL task from taskCollection on AGENT_OUTBOUND_FAILED when terminated', () => {
943
1264
  const task = taskManager.getTask(taskId);
944
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
945
- task.data = {
946
- ...task.data,
947
- ...newData,
948
- interaction: {
949
- ...task.data.interaction,
950
- ...newData.interaction,
951
- outboundType: 'OUTDIAL',
952
- state: 'new',
953
- isTerminated: true,
954
- },
955
- };
956
- return task;
1265
+ Object.assign(task.data, {
1266
+ interaction: {
1267
+ ...task.data.interaction,
1268
+ outboundType: 'OUTDIAL',
1269
+ state: 'new',
1270
+ isTerminated: true,
1271
+ },
1272
+ agentsPendingWrapUp: ['agent-123'],
957
1273
  });
958
1274
  task.unregisterWebCallListeners = jest.fn();
959
1275
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1276
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
960
1277
 
961
1278
  const payload = {
962
1279
  data: {
@@ -969,6 +1286,7 @@ describe('TaskManager', () => {
969
1286
  state: 'new',
970
1287
  isTerminated: true,
971
1288
  },
1289
+ agentsPendingWrapUp: ['agent-123'],
972
1290
  interactionId: taskId,
973
1291
  orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
974
1292
  trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
@@ -983,23 +1301,61 @@ describe('TaskManager', () => {
983
1301
 
984
1302
  webSocketManagerMock.emit('message', JSON.stringify(payload));
985
1303
 
986
- expect(taskManager.getTask(taskId)).toBeDefined();
987
- expect(removeTaskSpy).not.toHaveBeenCalled();
1304
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1305
+
1306
+ expect(taskManager.getTask(taskId)).toBeUndefined();
1307
+ expect(removeTaskSpy).toHaveBeenCalled();
1308
+ const stateMachineEvent = expectLastStateMachineEvent(
1309
+ sendStateMachineEventSpy,
1310
+ TaskEvent.OUTBOUND_FAILED
1311
+ );
1312
+ expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
1313
+ sendStateMachineEventSpy.mockRestore();
988
1314
  });
989
1315
 
990
1316
  it('should emit TASK_OUTDIAL_FAILED event on AGENT_OUTBOUND_FAILED', () => {
991
1317
  const task = taskManager.getTask(taskId);
992
- task.updateTaskData = jest.fn().mockReturnValue(task);
993
- const taskEmitSpy = jest.spyOn(task, 'emit');
1318
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1319
+ const payload = {
1320
+ data: {
1321
+ type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
1322
+ interactionId: taskId,
1323
+ reason: 'CUSTOMER_BUSY',
1324
+ },
1325
+ };
1326
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1327
+ const stateMachineEvent = expectLastStateMachineEvent(
1328
+ sendStateMachineEventSpy,
1329
+ TaskEvent.OUTBOUND_FAILED
1330
+ );
1331
+ expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
1332
+ sendStateMachineEventSpy.mockRestore();
1333
+ });
1334
+
1335
+ it('should pass taskData in OUTBOUND_FAILED event for shouldWrapUp guard evaluation', () => {
1336
+ const task = taskManager.getTask(taskId);
1337
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
994
1338
  const payload = {
995
1339
  data: {
996
1340
  type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
997
1341
  interactionId: taskId,
998
1342
  reason: 'CUSTOMER_BUSY',
1343
+ agentsPendingWrapUp: ['agent-123'],
1344
+ interaction: {
1345
+ outboundType: 'OUTDIAL',
1346
+ isTerminated: true,
1347
+ },
999
1348
  },
1000
1349
  };
1001
1350
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1002
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OUTDIAL_FAILED, 'CUSTOMER_BUSY');
1351
+ const stateMachineEvent = expectLastStateMachineEvent(
1352
+ sendStateMachineEventSpy,
1353
+ TaskEvent.OUTBOUND_FAILED
1354
+ );
1355
+ expect(stateMachineEvent?.taskData).toBeDefined();
1356
+ expect(stateMachineEvent?.taskData?.agentsPendingWrapUp).toEqual(['agent-123']);
1357
+ expect(stateMachineEvent?.taskData?.interaction?.outboundType).toBe('OUTDIAL');
1358
+ sendStateMachineEventSpy.mockRestore();
1003
1359
  });
1004
1360
 
1005
1361
  it('should handle AGENT_OUTBOUND_FAILED gracefully when task is undefined', () => {
@@ -1017,23 +1373,15 @@ describe('TaskManager', () => {
1017
1373
  });
1018
1374
 
1019
1375
  it('should NOT remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp exists', () => {
1020
- const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
1021
- taskManager.setAgentId(agentId);
1022
-
1023
1376
  const task = taskManager.getTask(taskId);
1024
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1025
- task.data = {
1026
- ...task.data,
1027
- ...newData,
1028
- interaction: {
1029
- ...task.data.interaction,
1030
- outboundType: 'OUTDIAL',
1031
- state: 'new',
1032
- mediaType: 'telephony',
1033
- },
1034
- agentsPendingWrapUp: [agentId],
1035
- };
1036
- return task;
1377
+ Object.assign(task.data, {
1378
+ interaction: {
1379
+ ...task.data.interaction,
1380
+ outboundType: 'OUTDIAL',
1381
+ state: 'new',
1382
+ mediaType: 'telephony',
1383
+ },
1384
+ agentsPendingWrapUp: ['test-agent-id'],
1037
1385
  });
1038
1386
  task.unregisterWebCallListeners = jest.fn();
1039
1387
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -1047,7 +1395,7 @@ describe('TaskManager', () => {
1047
1395
  state: 'new',
1048
1396
  mediaType: 'telephony',
1049
1397
  },
1050
- agentsPendingWrapUp: [agentId],
1398
+ agentsPendingWrapUp: ['test-agent-id'],
1051
1399
  },
1052
1400
  };
1053
1401
 
@@ -1059,19 +1407,14 @@ describe('TaskManager', () => {
1059
1407
 
1060
1408
  it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is empty', () => {
1061
1409
  const task = taskManager.getTask(taskId);
1062
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1063
- task.data = {
1064
- ...task.data,
1065
- ...newData,
1066
- interaction: {
1067
- ...task.data.interaction,
1068
- outboundType: 'OUTDIAL',
1069
- state: 'new',
1070
- mediaType: 'telephony',
1071
- },
1072
- agentsPendingWrapUp: [],
1073
- };
1074
- return task;
1410
+ Object.assign(task.data, {
1411
+ interaction: {
1412
+ ...task.data.interaction,
1413
+ outboundType: 'OUTDIAL',
1414
+ state: 'new',
1415
+ mediaType: 'telephony',
1416
+ },
1417
+ agentsPendingWrapUp: [],
1075
1418
  });
1076
1419
  task.unregisterWebCallListeners = jest.fn();
1077
1420
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -1096,19 +1439,13 @@ describe('TaskManager', () => {
1096
1439
 
1097
1440
  it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is undefined', () => {
1098
1441
  const task = taskManager.getTask(taskId);
1099
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1100
- task.data = {
1101
- ...task.data,
1102
- ...newData,
1103
- interaction: {
1104
- ...task.data.interaction,
1105
- outboundType: 'OUTDIAL',
1106
- state: 'new',
1107
- mediaType: 'telephony',
1108
- },
1109
- // agentsPendingWrapUp is undefined
1110
- };
1111
- return task;
1442
+ Object.assign(task.data, {
1443
+ interaction: {
1444
+ ...task.data.interaction,
1445
+ outboundType: 'OUTDIAL',
1446
+ state: 'new',
1447
+ mediaType: 'telephony',
1448
+ },
1112
1449
  });
1113
1450
  task.unregisterWebCallListeners = jest.fn();
1114
1451
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -1147,333 +1484,29 @@ describe('TaskManager', () => {
1147
1484
  }).not.toThrow();
1148
1485
  });
1149
1486
 
1150
- describe('wrapUpRequired logic in CONTACT_ENDED event', () => {
1151
- const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
1152
-
1153
- beforeEach(() => {
1154
- // Set the agent ID on taskManager
1155
- taskManager.setAgentId(agentId);
1487
+ it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
1488
+ const task = taskManager.getTask(taskId);
1489
+ Object.assign(task.data, {
1490
+ interaction: {
1491
+ ...task.data.interaction,
1492
+ outboundType: 'OUTDIAL',
1493
+ state: 'new',
1494
+ isTerminated: false,
1495
+ },
1156
1496
  });
1497
+ task.unregisterWebCallListeners = jest.fn();
1498
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1157
1499
 
1158
- it('should set wrapUpRequired to true when agent is in agentsPendingWrapUp array for non-campaign tasks', () => {
1159
- const task = taskManager.getTask(taskId);
1160
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1161
- task.data = {
1162
- ...task.data,
1163
- ...newData,
1164
- };
1165
- return task;
1166
- });
1167
- task.unregisterWebCallListeners = jest.fn();
1168
-
1169
- const payload = {
1170
- data: {
1171
- type: CC_EVENTS.CONTACT_ENDED,
1172
- interactionId: taskId,
1173
- interaction: {
1174
- state: 'connected',
1175
- mediaType: 'telephony',
1176
- },
1177
- agentsPendingWrapUp: [agentId, 'other-agent-id'],
1178
- },
1179
- };
1180
-
1181
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1182
-
1183
- expect(task.updateTaskData).toHaveBeenCalledWith(
1184
- expect.objectContaining({
1185
- wrapUpRequired: true,
1186
- })
1187
- );
1188
- });
1189
-
1190
- it('should set wrapUpRequired to false when agent is not in agentsPendingWrapUp array', () => {
1191
- const task = taskManager.getTask(taskId);
1192
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1193
- task.data = {
1194
- ...task.data,
1195
- ...newData,
1196
- };
1197
- return task;
1198
- });
1199
- task.unregisterWebCallListeners = jest.fn();
1200
-
1201
- const payload = {
1202
- data: {
1203
- type: CC_EVENTS.CONTACT_ENDED,
1204
- interactionId: taskId,
1205
- interaction: {
1206
- state: 'connected',
1207
- mediaType: 'telephony',
1208
- },
1209
- agentsPendingWrapUp: ['other-agent-id', 'another-agent-id'],
1210
- },
1211
- };
1212
-
1213
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1214
-
1215
- expect(task.updateTaskData).toHaveBeenCalledWith(
1216
- expect.objectContaining({
1217
- wrapUpRequired: false,
1218
- })
1219
- );
1220
- });
1221
-
1222
- it('should set wrapUpRequired to false when agentsPendingWrapUp is an empty array', () => {
1223
- const task = taskManager.getTask(taskId);
1224
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1225
- task.data = {
1226
- ...task.data,
1227
- ...newData,
1228
- };
1229
- return task;
1230
- });
1231
- task.unregisterWebCallListeners = jest.fn();
1232
-
1233
- const payload = {
1234
- data: {
1235
- type: CC_EVENTS.CONTACT_ENDED,
1236
- interactionId: taskId,
1237
- interaction: {
1238
- state: 'connected',
1239
- mediaType: 'telephony',
1240
- },
1241
- agentsPendingWrapUp: [],
1242
- },
1243
- };
1244
-
1245
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1246
-
1247
- expect(task.updateTaskData).toHaveBeenCalledWith(
1248
- expect.objectContaining({
1249
- wrapUpRequired: false,
1250
- })
1251
- );
1252
- });
1253
-
1254
- it('should set wrapUpRequired to false when agentsPendingWrapUp is undefined', () => {
1255
- const task = taskManager.getTask(taskId);
1256
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1257
- task.data = {
1258
- ...task.data,
1259
- ...newData,
1260
- };
1261
- return task;
1262
- });
1263
- task.unregisterWebCallListeners = jest.fn();
1264
-
1265
- const payload = {
1266
- data: {
1267
- type: CC_EVENTS.CONTACT_ENDED,
1268
- interactionId: taskId,
1269
- interaction: {
1270
- state: 'connected',
1271
- mediaType: 'telephony',
1272
- },
1273
- // agentsPendingWrapUp is not defined
1274
- },
1275
- };
1276
-
1277
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1278
-
1279
- expect(task.updateTaskData).toHaveBeenCalledWith(
1280
- expect.objectContaining({
1281
- wrapUpRequired: false,
1282
- })
1283
- );
1284
- });
1285
-
1286
- it('should set wrapUpRequired to false when agentsPendingWrapUp is null', () => {
1287
- const task = taskManager.getTask(taskId);
1288
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1289
- task.data = {
1290
- ...task.data,
1291
- ...newData,
1292
- };
1293
- return task;
1294
- });
1295
- task.unregisterWebCallListeners = jest.fn();
1296
-
1297
- const payload = {
1298
- data: {
1299
- type: CC_EVENTS.CONTACT_ENDED,
1300
- interactionId: taskId,
1301
- interaction: {
1302
- state: 'connected',
1303
- mediaType: 'telephony',
1304
- },
1305
- agentsPendingWrapUp: null,
1306
- },
1307
- };
1308
-
1309
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1310
-
1311
- expect(task.updateTaskData).toHaveBeenCalledWith(
1312
- expect.objectContaining({
1313
- wrapUpRequired: false,
1314
- })
1315
- );
1316
- });
1317
-
1318
- it('should set wrapUpRequired to true when agent is the only one in agentsPendingWrapUp for non-campaign tasks', () => {
1319
- const task = taskManager.getTask(taskId);
1320
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1321
- task.data = {
1322
- ...task.data,
1323
- ...newData,
1324
- };
1325
- return task;
1326
- });
1327
- task.unregisterWebCallListeners = jest.fn();
1328
-
1329
- const payload = {
1330
- data: {
1331
- type: CC_EVENTS.CONTACT_ENDED,
1332
- interactionId: taskId,
1333
- interaction: {
1334
- state: 'connected',
1335
- mediaType: 'telephony',
1336
- },
1337
- agentsPendingWrapUp: [agentId],
1338
- },
1339
- };
1340
-
1341
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1342
-
1343
- expect(task.updateTaskData).toHaveBeenCalledWith(
1344
- expect.objectContaining({
1345
- wrapUpRequired: true,
1346
- })
1347
- );
1348
- });
1349
-
1350
- it('should set wrapUpRequired to true for different interaction states when agent is in agentsPendingWrapUp for non-campaign tasks', () => {
1351
- const task = taskManager.getTask(taskId);
1352
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1353
- task.data = {
1354
- ...task.data,
1355
- ...newData,
1356
- interaction: {
1357
- ...task.data.interaction,
1358
- ...newData.interaction,
1359
- },
1360
- };
1361
- return task;
1362
- });
1363
- task.unregisterWebCallListeners = jest.fn();
1364
-
1365
- // Test with 'connected' state
1366
- const payloadConnected = {
1367
- data: {
1368
- type: CC_EVENTS.CONTACT_ENDED,
1369
- interactionId: taskId,
1370
- interaction: {
1371
- state: 'connected',
1372
- mediaType: 'telephony',
1373
- },
1374
- agentsPendingWrapUp: [agentId],
1375
- },
1376
- };
1377
-
1378
- webSocketManagerMock.emit('message', JSON.stringify(payloadConnected));
1379
-
1380
- expect(task.updateTaskData).toHaveBeenNthCalledWith(
1381
- 1,
1382
- expect.objectContaining({
1383
- wrapUpRequired: true,
1384
- })
1385
- );
1386
-
1387
- // Test with 'held' state to verify it still works regardless of state
1388
- const payloadHeld = {
1389
- data: {
1390
- type: CC_EVENTS.CONTACT_ENDED,
1391
- interactionId: taskId,
1392
- interaction: {
1393
- state: 'held',
1394
- mediaType: 'telephony',
1395
- },
1396
- agentsPendingWrapUp: [agentId],
1397
- },
1398
- };
1399
-
1400
- webSocketManagerMock.emit('message', JSON.stringify(payloadHeld));
1401
-
1402
- expect(task.updateTaskData).toHaveBeenNthCalledWith(
1403
- 2,
1404
- expect.objectContaining({
1405
- wrapUpRequired: true,
1406
- })
1407
- );
1408
- });
1409
-
1410
- it('should set wrapUpRequired to false for campaign preview tasks even when agent is in agentsPendingWrapUp', () => {
1411
- const task = taskManager.getTask(taskId);
1412
- // Set up task as a campaign preview task via outboundType
1413
- task.data.interaction = {
1414
- ...task.data.interaction,
1415
- outboundType: 'STANDARD_PREVIEW_CAMPAIGN',
1416
- };
1417
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1418
- task.data = {
1419
- ...task.data,
1420
- ...newData,
1421
- };
1422
- return task;
1423
- });
1424
- task.unregisterWebCallListeners = jest.fn();
1425
-
1426
- const payload = {
1427
- data: {
1428
- type: CC_EVENTS.CONTACT_ENDED,
1429
- interactionId: taskId,
1430
- interaction: {
1431
- state: 'connected',
1432
- mediaType: 'telephony',
1433
- },
1434
- agentsPendingWrapUp: [agentId],
1435
- },
1436
- };
1437
-
1438
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1439
-
1440
- expect(task.updateTaskData).toHaveBeenCalledWith(
1441
- expect.objectContaining({
1442
- wrapUpRequired: false,
1443
- })
1444
- );
1445
- });
1446
- });
1447
-
1448
- it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
1449
- const task = taskManager.getTask(taskId);
1450
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1451
- task.data = {
1452
- ...task.data,
1453
- ...newData,
1454
- interaction: {
1455
- ...task.data.interaction,
1456
- ...newData.interaction,
1457
- outboundType: 'OUTDIAL',
1458
- state: 'new',
1459
- isTerminated: false,
1460
- },
1461
- };
1462
- return task;
1463
- });
1464
- task.unregisterWebCallListeners = jest.fn();
1465
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1466
-
1467
- const payload = {
1468
- data: {
1469
- type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
1470
- agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
1471
- eventTime: 1733211616959,
1472
- eventType: 'RoutingMessage',
1473
- interaction: {
1474
- outboundType: 'OUTDIAL',
1475
- state: 'new',
1476
- isTerminated: false,
1500
+ const payload = {
1501
+ data: {
1502
+ type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
1503
+ agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
1504
+ eventTime: 1733211616959,
1505
+ eventType: 'RoutingMessage',
1506
+ interaction: {
1507
+ outboundType: 'OUTDIAL',
1508
+ state: 'new',
1509
+ isTerminated: false,
1477
1510
  },
1478
1511
  interactionId: taskId,
1479
1512
  orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
@@ -1503,20 +1536,16 @@ describe('TaskManager', () => {
1503
1536
 
1504
1537
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1505
1538
  const task = taskManager.getTask(taskId);
1506
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1507
- task.data = {...newData, isConsulted: true};
1508
- return task;
1509
- });
1510
- const taskEmitSpy = jest.spyOn(task, 'emit');
1539
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1511
1540
 
1512
1541
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1513
1542
 
1514
- expect(task.updateTaskData).toHaveBeenCalledWith({
1515
- ...payload.data,
1516
- isConsulted: true,
1517
- });
1518
- expect(task.data.isConsulted).toBe(true);
1519
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OFFER_CONSULT, task);
1543
+ const stateMachineEvent = expectLastStateMachineEvent(
1544
+ sendStateMachineEventSpy,
1545
+ TaskEvent.OFFER_CONSULT
1546
+ );
1547
+ expect(stateMachineEvent?.taskData).toEqual({...payload.data, isConsulted: true});
1548
+ sendStateMachineEventSpy.mockRestore();
1520
1549
  });
1521
1550
 
1522
1551
  it('should emit TASK_CONSULT_ACCEPTED event on AGENT_CONSULTING event', () => {
@@ -1535,24 +1564,18 @@ describe('TaskManager', () => {
1535
1564
  };
1536
1565
 
1537
1566
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1538
- taskManager.getTask(taskId).updateTaskData = jest.fn().mockImplementation((newData) => {
1539
- taskManager.getTask(taskId).data = {...newData, isConsulted: true};
1540
- return taskManager.getTask(taskId);
1541
- });
1542
1567
 
1543
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1544
- const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
1568
+ const task = taskManager.getTask(taskId);
1569
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1570
+
1545
1571
  webSocketManagerMock.emit('message', JSON.stringify(initialConsultingPayload));
1546
1572
  webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
1547
- expect(taskManager.getTask(taskId).data.isConsulted).toBe(true);
1548
- expect(taskEmitSpy).toHaveBeenCalledWith(
1549
- TASK_EVENTS.TASK_CONSULT_ACCEPTED,
1550
- taskManager.getTask(taskId)
1551
- );
1552
- expect(taskManagerEmitSpy).toHaveBeenCalledWith(
1553
- TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
1554
- taskManager.getTask(taskId)
1573
+ const stateMachineEvent = expectLastStateMachineEvent(
1574
+ sendStateMachineEventSpy,
1575
+ TaskEvent.CONSULTING_ACTIVE
1555
1576
  );
1577
+ expect(stateMachineEvent?.taskData).toEqual(consultingPayload.data);
1578
+ sendStateMachineEventSpy.mockRestore();
1556
1579
  });
1557
1580
 
1558
1581
  it('should emit TASK_CONSULT_ENDED event on AGENT_CONSULT_ENDED event', () => {
@@ -1564,14 +1587,16 @@ describe('TaskManager', () => {
1564
1587
  };
1565
1588
 
1566
1589
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1567
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1568
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1590
+ taskManager.getTask(taskId).data.isConsulted = true;
1591
+ const task = taskManager.getTask(taskId);
1592
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1569
1593
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1570
- expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
1571
- expect(taskEmitSpy).toHaveBeenCalledWith(
1572
- TASK_EVENTS.TASK_CONSULT_END,
1573
- taskManager.getTask(taskId)
1594
+ const stateMachineEvent = expectLastStateMachineEvent(
1595
+ sendStateMachineEventSpy,
1596
+ TaskEvent.CONSULT_END
1574
1597
  );
1598
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1599
+ sendStateMachineEventSpy.mockRestore();
1575
1600
  });
1576
1601
 
1577
1602
  it('should emit TASK_CONSULT_ENDED event and remove currentTask when on AGENT_CONSULT_ENDED event when requested for a consult', () => {
@@ -1584,19 +1609,14 @@ describe('TaskManager', () => {
1584
1609
 
1585
1610
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1586
1611
 
1587
- taskManager.getTask(taskId).updateTaskData = jest.fn().mockImplementation((newData) => {
1588
- taskManager.getTask(taskId).data = {...newData, isConsulted: true};
1589
- return taskManager.getTask(taskId);
1590
- });
1612
+ taskManager.getTask(taskId).data.isConsulted = true;
1591
1613
  const task = taskManager.getTask(taskId);
1592
1614
 
1593
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1594
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1615
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1595
1616
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1596
- expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
1597
- expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.AGENT_CONSULT_ENDED, payload.data);
1598
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONSULT_END, task);
1617
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONSULT_END);
1599
1618
  expect(taskManager.getTask(taskId)).toBeUndefined(); // Ensure task is removed from the task collection after the consult ends
1619
+ sendStateMachineEventSpy.mockRestore();
1600
1620
  });
1601
1621
 
1602
1622
  it('should emit TASK_CANCELLED event on AGENT_CTQ_CANCELLED event', () => {
@@ -1608,14 +1628,15 @@ describe('TaskManager', () => {
1608
1628
  };
1609
1629
 
1610
1630
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1611
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1612
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1631
+ const task = taskManager.getTask(taskId);
1632
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1613
1633
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1614
- expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
1615
- expect(taskEmitSpy).toHaveBeenCalledWith(
1616
- TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED,
1617
- taskManager.getTask(taskId)
1634
+ const stateMachineEvent = expectLastStateMachineEvent(
1635
+ sendStateMachineEventSpy,
1636
+ TaskEvent.CTQ_CANCEL
1618
1637
  );
1638
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1639
+ sendStateMachineEventSpy.mockRestore();
1619
1640
  });
1620
1641
 
1621
1642
  it('should handle AGENT_CONSULT_FAILED event', () => {
@@ -1629,9 +1650,15 @@ describe('TaskManager', () => {
1629
1650
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1630
1651
 
1631
1652
  // Always spy on the updated task object after CONTACT_RESERVED is emitted
1632
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1653
+ const task = taskManager.getTask(taskId);
1654
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1633
1655
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1634
- expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
1656
+ const stateMachineEvent = expectLastStateMachineEvent(
1657
+ sendStateMachineEventSpy,
1658
+ TaskEvent.CONSULT_FAILED
1659
+ );
1660
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1661
+ sendStateMachineEventSpy.mockRestore();
1635
1662
  });
1636
1663
 
1637
1664
  it('should emit TASK_CONSULT_QUEUE_FAILED on AGENT_CTQ_CANCEL_FAILED event', () => {
@@ -1643,14 +1670,15 @@ describe('TaskManager', () => {
1643
1670
  };
1644
1671
 
1645
1672
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1646
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1647
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1673
+ const task = taskManager.getTask(taskId);
1674
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1648
1675
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1649
- expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
1650
- expect(taskEmitSpy).toHaveBeenCalledWith(
1651
- TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED,
1652
- taskManager.getTask(taskId)
1676
+ const stateMachineEvent = expectLastStateMachineEvent(
1677
+ sendStateMachineEventSpy,
1678
+ TaskEvent.CTQ_CANCEL_FAILED
1653
1679
  );
1680
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1681
+ sendStateMachineEventSpy.mockRestore();
1654
1682
  });
1655
1683
 
1656
1684
  it('should emit TASK_REJECT event on AGENT_CONTACT_OFFER_RONA event', () => {
@@ -1693,15 +1721,14 @@ describe('TaskManager', () => {
1693
1721
  };
1694
1722
 
1695
1723
  taskManager.taskCollection[taskId] = taskManager.getTask(taskId);
1696
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1697
- const metricsTrackSpy = jest.spyOn(taskManager.metricsManager, 'trackEvent');
1724
+ const task = taskManager.getTask(taskId);
1725
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1698
1726
 
1699
1727
  webSocketManagerMock.emit('message', JSON.stringify(ronaPayload));
1700
1728
 
1701
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_REJECT, ronaPayload.data.reason);
1702
- // Verify the correct metric event name is used for AGENT_CONTACT_OFFER_RONA
1703
- expect(metricsTrackSpy).toHaveBeenCalled();
1704
- expect(metricsTrackSpy.mock.calls[0][0]).toBe('Agent RONA');
1729
+ const stateMachineEvent = expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.RONA);
1730
+ expect(stateMachineEvent?.reason).toBe(ronaPayload.data.reason);
1731
+ sendStateMachineEventSpy.mockRestore();
1705
1732
  });
1706
1733
 
1707
1734
  it('should emit TASK_REJECT event on AGENT_CONTACT_ASSIGN_FAILED event', () => {
@@ -1745,20 +1772,16 @@ describe('TaskManager', () => {
1745
1772
 
1746
1773
  taskManager.taskCollection[taskId] = taskManager.getTask(taskId);
1747
1774
  const task = taskManager.getTask(taskId);
1748
- const taskEmitSpy = jest.spyOn(task, 'emit');
1749
- const taskUpdateDataSpy = jest.spyOn(task, 'updateTaskData');
1750
- const metricsTrackSpy = jest.spyOn(taskManager.metricsManager, 'trackEvent');
1775
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1751
1776
 
1752
1777
  webSocketManagerMock.emit('message', JSON.stringify(assignFailedPayload));
1753
1778
 
1754
- expect(taskUpdateDataSpy).toHaveBeenCalledWith(assignFailedPayload.data);
1755
- expect(taskEmitSpy).toHaveBeenCalledWith(
1756
- TASK_EVENTS.TASK_REJECT,
1757
- assignFailedPayload.data.reason
1779
+ const stateMachineEvent = expectLastStateMachineEvent(
1780
+ sendStateMachineEventSpy,
1781
+ TaskEvent.ASSIGN_FAILED
1758
1782
  );
1759
- // Verify the correct metric event name is used for AGENT_CONTACT_ASSIGN_FAILED
1760
- expect(metricsTrackSpy).toHaveBeenCalled();
1761
- expect(metricsTrackSpy.mock.calls[0][0]).toBe('Agent Contact Assign Failed');
1783
+ expect(stateMachineEvent?.reason).toBe(assignFailedPayload.data.reason);
1784
+ sendStateMachineEventSpy.mockRestore();
1762
1785
  });
1763
1786
 
1764
1787
  it('should remove currentTask from taskCollection on AGENT_WRAPPEDUP event', () => {
@@ -1781,11 +1804,12 @@ describe('TaskManager', () => {
1781
1804
 
1782
1805
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1783
1806
  const task = taskManager.getTask(taskId);
1784
- const taskEmitSpy = jest.spyOn(task, 'emit');
1807
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1785
1808
 
1786
1809
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1787
1810
 
1788
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WRAPPEDUP, task);
1811
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.WRAPUP_COMPLETE);
1812
+ sendStateMachineEventSpy.mockRestore();
1789
1813
  expect(taskManager.getTask(taskId)).toBeUndefined();
1790
1814
  });
1791
1815
 
@@ -1811,7 +1835,8 @@ describe('TaskManager', () => {
1811
1835
  };
1812
1836
 
1813
1837
  const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1814
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1838
+ const taskUpdateTaskDataSpy = taskManager.getTask(taskId).updateTaskData as jest.Mock;
1839
+ taskUpdateTaskDataSpy.mockClear();
1815
1840
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1816
1841
  expect(taskEmitSpy).not.toHaveBeenCalled();
1817
1842
  expect(taskUpdateTaskDataSpy).not.toHaveBeenCalled();
@@ -1820,7 +1845,8 @@ describe('TaskManager', () => {
1820
1845
  it('should emit TASK_CONSULTING event when agent is consulting', () => {
1821
1846
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1822
1847
  taskManager.getTask(taskId).data.isConsulted = false;
1823
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1848
+ const task = taskManager.getTask(taskId);
1849
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1824
1850
  const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
1825
1851
  const consultingPayload = {
1826
1852
  data: {
@@ -1830,20 +1856,21 @@ describe('TaskManager', () => {
1830
1856
  },
1831
1857
  };
1832
1858
  webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
1833
- expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.AGENT_CONSULTING, consultingPayload.data);
1834
- expect(taskEmitSpy).toHaveBeenCalledWith(
1835
- TASK_EVENTS.TASK_CONSULTING,
1836
- taskManager.getTask(taskId)
1837
- );
1859
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONSULTING_ACTIVE);
1838
1860
  expect(taskManagerEmitSpy).toHaveBeenCalledWith(
1839
1861
  TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
1840
1862
  taskManager.getTask(taskId)
1841
1863
  );
1864
+ sendStateMachineEventSpy.mockRestore();
1842
1865
  });
1843
1866
 
1844
- it('should emit TASK_END event on AGENT_CONTACT_UNASSIGNED', () => {
1867
+ it('should update task data on AGENT_CONTACT_UNASSIGNED', () => {
1845
1868
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1846
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1869
+ const task = taskManager.getTask(taskId);
1870
+ const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
1871
+ sendStateMachineEventSpy.mockClear();
1872
+ const updateTaskDataSpy = task.updateTaskData as jest.Mock;
1873
+ updateTaskDataSpy.mockClear();
1847
1874
  const unassignedPayload = {
1848
1875
  data: {
1849
1876
  type: CC_EVENTS.AGENT_CONTACT_UNASSIGNED,
@@ -1861,11 +1888,76 @@ describe('TaskManager', () => {
1861
1888
  },
1862
1889
  };
1863
1890
  webSocketManagerMock.emit('message', JSON.stringify(unassignedPayload));
1864
- expect(taskEmitSpy).toHaveBeenCalledWith(
1865
- CC_EVENTS.AGENT_CONTACT_UNASSIGNED,
1866
- unassignedPayload.data
1891
+ expect(sendStateMachineEventSpy).not.toHaveBeenCalled();
1892
+ expect(updateTaskDataSpy).toHaveBeenCalledWith(unassignedPayload.data);
1893
+ });
1894
+
1895
+ it('preserves consult fields from state context during consulting payload refresh', () => {
1896
+ const task = createMockTask({
1897
+ ...taskDataMock,
1898
+ interaction: {state: 'consulting'} as any,
1899
+ interactionId: taskId,
1900
+ });
1901
+ task.stateMachineService = {
1902
+ getSnapshot: () => ({
1903
+ value: 'CONSULTING',
1904
+ context: {
1905
+ consultDestinationAgentId: 'agent-preserved',
1906
+ consultDestinationType: 'agent',
1907
+ },
1908
+ }),
1909
+ };
1910
+
1911
+ const incomingTaskData = {
1912
+ ...taskDataMock,
1913
+ interaction: {state: 'consulting'} as any,
1914
+ interactionId: taskId,
1915
+ destAgentId: null,
1916
+ destinationType: null,
1917
+ };
1918
+
1919
+ (taskManager as any).updateTaskData(task, incomingTaskData);
1920
+
1921
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1922
+ expect.objectContaining({
1923
+ destAgentId: 'agent-preserved',
1924
+ destinationType: 'agent',
1925
+ })
1926
+ );
1927
+ });
1928
+
1929
+ it('does not preserve stale consult fields once consult is no longer active', () => {
1930
+ const task = createMockTask({
1931
+ ...taskDataMock,
1932
+ interaction: {state: 'connected'} as any,
1933
+ interactionId: taskId,
1934
+ });
1935
+ task.stateMachineService = {
1936
+ getSnapshot: () => ({
1937
+ value: 'CONNECTED',
1938
+ context: {
1939
+ consultDestinationAgentId: 'agent-stale',
1940
+ consultDestinationType: 'agent',
1941
+ },
1942
+ }),
1943
+ };
1944
+
1945
+ const incomingTaskData = {
1946
+ ...taskDataMock,
1947
+ interaction: {state: 'connected'} as any,
1948
+ interactionId: taskId,
1949
+ destAgentId: null,
1950
+ destinationType: null,
1951
+ };
1952
+
1953
+ (taskManager as any).updateTaskData(task, incomingTaskData);
1954
+
1955
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1956
+ expect.objectContaining({
1957
+ destAgentId: null,
1958
+ destinationType: null,
1959
+ })
1867
1960
  );
1868
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, taskManager.getTask(taskId));
1869
1961
  });
1870
1962
 
1871
1963
  it('should handle chat interaction and emit TASK_INCOMING immediately', () => {
@@ -1877,16 +1969,16 @@ describe('TaskManager', () => {
1877
1969
  },
1878
1970
  };
1879
1971
 
1880
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
1881
-
1882
1972
  // Simulate receiving a chat task
1883
1973
  webSocketManagerMock.emit('message', JSON.stringify(chatPayload));
1884
1974
 
1885
- // For non-telephony tasks, TASK_INCOMING should be emitted immediately
1886
- expect(taskIncomingSpy).toHaveBeenCalledWith(
1887
- TASK_EVENTS.TASK_INCOMING,
1888
- taskManager.getTask(chatPayload.data.interactionId)
1975
+ const chatTask = taskManager.getTask(chatPayload.data.interactionId);
1976
+ const sendStateMachineEventSpy = chatTask.sendStateMachineEvent as jest.Mock;
1977
+ const stateMachineEvent = expectLastStateMachineEvent(
1978
+ sendStateMachineEventSpy,
1979
+ TaskEvent.TASK_INCOMING
1889
1980
  );
1981
+ expect(stateMachineEvent?.taskData).toEqual(chatPayload.data);
1890
1982
  expect(taskManager.getAllTasks()).toHaveProperty(chatPayload.data.interactionId);
1891
1983
  });
1892
1984
 
@@ -1899,16 +1991,16 @@ describe('TaskManager', () => {
1899
1991
  },
1900
1992
  };
1901
1993
 
1902
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
1903
-
1904
1994
  // Simulate receiving an email task
1905
1995
  webSocketManagerMock.emit('message', JSON.stringify(emailPayload));
1906
1996
 
1907
- // For non-telephony tasks, TASK_INCOMING should be emitted immediately
1908
- expect(taskIncomingSpy).toHaveBeenCalledWith(
1909
- TASK_EVENTS.TASK_INCOMING,
1910
- taskManager.getTask(emailPayload.data.interactionId)
1997
+ const emailTask = taskManager.getTask(emailPayload.data.interactionId);
1998
+ const sendStateMachineEventSpy = emailTask.sendStateMachineEvent as jest.Mock;
1999
+ const stateMachineEvent = expectLastStateMachineEvent(
2000
+ sendStateMachineEventSpy,
2001
+ TaskEvent.TASK_INCOMING
1911
2002
  );
2003
+ expect(stateMachineEvent?.taskData).toEqual(emailPayload.data);
1912
2004
  expect(taskManager.getAllTasks()).toHaveProperty(emailPayload.data.interactionId);
1913
2005
  });
1914
2006
 
@@ -1922,13 +2014,11 @@ describe('TaskManager', () => {
1922
2014
  },
1923
2015
  };
1924
2016
 
1925
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
1926
2017
  webSocketManagerMock.emit('message', JSON.stringify(chatReservedPayload));
2018
+ const task = taskManager.getTask(chatReservedPayload.data.interactionId);
2019
+ const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
1927
2020
 
1928
- expect(taskIncomingSpy).toHaveBeenCalledWith(
1929
- TASK_EVENTS.TASK_INCOMING,
1930
- taskManager.getTask(chatReservedPayload.data.interactionId)
1931
- );
2021
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TASK_INCOMING);
1932
2022
 
1933
2023
  // 2. Chat task is assigned
1934
2024
  const chatAssignedPayload = {
@@ -1938,12 +2028,9 @@ describe('TaskManager', () => {
1938
2028
  },
1939
2029
  };
1940
2030
 
1941
- const task = taskManager.getTask(chatReservedPayload.data.interactionId);
1942
- const taskEmitSpy = jest.spyOn(task, 'emit');
1943
-
1944
2031
  webSocketManagerMock.emit('message', JSON.stringify(chatAssignedPayload));
1945
2032
 
1946
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, task);
2033
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.ASSIGN);
1947
2034
 
1948
2035
  // 3. Chat task is ended with state 'new' to trigger cleanup
1949
2036
  const chatEndedPayload = {
@@ -1951,12 +2038,15 @@ describe('TaskManager', () => {
1951
2038
  ...chatReservedPayload.data,
1952
2039
  type: CC_EVENTS.CONTACT_ENDED,
1953
2040
  interaction: {mediaType: 'chat', state: 'new'}, // Change to 'new' state
2041
+ wrapUpRequired: false,
1954
2042
  },
1955
2043
  };
1956
2044
 
2045
+ // Simulate state on the task to allow cleanup logic
2046
+ task.data.interaction.state = 'new';
1957
2047
  webSocketManagerMock.emit('message', JSON.stringify(chatEndedPayload));
1958
2048
 
1959
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
2049
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONTACT_ENDED);
1960
2050
  // Verify task is removed since it was in a 'new' state
1961
2051
  expect(taskManager.getTask(chatReservedPayload.data.interactionId)).toBeUndefined();
1962
2052
  });
@@ -2045,13 +2135,8 @@ describe('TaskManager', () => {
2045
2135
  expect(taskManager.getAllTasks()).toHaveProperty(task2Payload.data.interactionId);
2046
2136
  expect(taskManager.getAllTasks()).toHaveProperty(task3Payload.data.interactionId);
2047
2137
 
2048
- // Create spies for all tasks
2049
- const task1EmitSpy = jest.spyOn(taskManager.getTask(task1Payload.data.interactionId), 'emit');
2050
- const task2EmitSpy = jest.spyOn(taskManager.getTask(task2Payload.data.interactionId), 'emit');
2051
- const task3EmitSpy = jest.spyOn(taskManager.getTask(task3Payload.data.interactionId), 'emit');
2052
-
2053
- // Store reference to task2 before it gets removed
2054
2138
  const task2 = taskManager.getTask(task2Payload.data.interactionId);
2139
+ const task2SendStateMachineEventSpy = task2.sendStateMachineEvent as jest.Mock;
2055
2140
 
2056
2141
  // End only the second task (chat task)
2057
2142
  const chatEndedPayload = {
@@ -2059,15 +2144,18 @@ describe('TaskManager', () => {
2059
2144
  ...task2Payload.data,
2060
2145
  type: CC_EVENTS.CONTACT_ENDED,
2061
2146
  interaction: {mediaType: 'chat', state: 'new'}, // Using 'new' to trigger cleanup
2147
+ wrapUpRequired: false,
2062
2148
  },
2063
2149
  };
2064
2150
 
2151
+ task2.data.interaction.state = 'new';
2065
2152
  webSocketManagerMock.emit('message', JSON.stringify(chatEndedPayload));
2066
2153
 
2067
- // Verify only task2 emitted TASK_END
2068
- expect(task1EmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END);
2069
- expect(task2EmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task2);
2070
- expect(task3EmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END);
2154
+ const firstEndEvent = expectLastStateMachineEvent(
2155
+ task2SendStateMachineEventSpy,
2156
+ TaskEvent.CONTACT_ENDED
2157
+ );
2158
+ expect(firstEndEvent?.taskData).toEqual(chatEndedPayload.data);
2071
2159
 
2072
2160
  // Verify task2 was removed from collection (since state was 'new')
2073
2161
  expect(taskManager.getTask(task2Payload.data.interactionId)).toBeUndefined();
@@ -2078,6 +2166,7 @@ describe('TaskManager', () => {
2078
2166
 
2079
2167
  // Store reference to task3 before we end it
2080
2168
  const task3 = taskManager.getTask(task3Payload.data.interactionId);
2169
+ const task3SendStateMachineEventSpy = task3.sendStateMachineEvent as jest.Mock;
2081
2170
 
2082
2171
  // Now end task3 with a state that doesn't trigger cleanup
2083
2172
  const emailEndedPayload = {
@@ -2085,31 +2174,32 @@ describe('TaskManager', () => {
2085
2174
  ...task3Payload.data,
2086
2175
  type: CC_EVENTS.CONTACT_ENDED,
2087
2176
  interaction: {mediaType: 'email', state: 'connected'}, // Using 'connected' to NOT trigger cleanup
2177
+ wrapUpRequired: true,
2178
+ agentsPendingWrapUp: ['test-agent-id'],
2088
2179
  },
2089
2180
  };
2090
2181
 
2182
+ task3.data.interaction.state = 'connected';
2091
2183
  webSocketManagerMock.emit('message', JSON.stringify(emailEndedPayload));
2092
2184
 
2093
- // Verify task3 emitted TASK_END
2094
- expect(task3EmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task3);
2185
+ const secondEndEvent = expectLastStateMachineEvent(
2186
+ task3SendStateMachineEventSpy,
2187
+ TaskEvent.CONTACT_ENDED
2188
+ );
2189
+ expect(secondEndEvent?.taskData).toEqual(emailEndedPayload.data);
2095
2190
 
2096
2191
  // Verify task3 is still in collection (since state was 'connected')
2097
2192
  expect(taskManager.getTask(task3Payload.data.interactionId)).toBeDefined();
2098
2193
 
2099
2194
  // Verify task1 remains unaffected
2100
- expect(task1EmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END);
2101
2195
  expect(taskManager.getTask(task1Payload.data.interactionId)).toBeDefined();
2102
2196
  });
2103
2197
 
2104
- it('should emit TASK_END event on AGENT_VTEAM_TRANSFERRED event', () => {
2198
+ it('should emit TRANSFER_SUCCESS event on AGENT_VTEAM_TRANSFERRED event', () => {
2105
2199
  // First create a task by emitting the initial payload
2106
2200
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
2107
-
2108
- // Get a reference to the task from taskCollection
2109
2201
  const task = taskManager.getTask(taskId);
2110
-
2111
- // Now spy on the task's emit method
2112
- const taskEmitSpy = jest.spyOn(task, 'emit');
2202
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
2113
2203
 
2114
2204
  const vteamTransferredPayload = {
2115
2205
  data: {
@@ -2133,8 +2223,9 @@ describe('TaskManager', () => {
2133
2223
 
2134
2224
  webSocketManagerMock.emit('message', JSON.stringify(vteamTransferredPayload));
2135
2225
 
2136
- // Check that task.emit was called with TASK_END event
2137
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
2226
+ // Check that the state machine received the END event
2227
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TRANSFER_SUCCESS);
2228
+ sendStateMachineEventSpy.mockRestore();
2138
2229
 
2139
2230
  // The task should still exist in the collection based on current implementation
2140
2231
  expect(taskManager.getTask(taskId)).toBeDefined();
@@ -2149,12 +2240,16 @@ describe('TaskManager', () => {
2149
2240
  },
2150
2241
  };
2151
2242
  const task = taskManager.getTask(taskId);
2152
- const updateSpy = jest.spyOn(task, 'updateTaskData').mockImplementation((data) => {
2153
- task.data = {...(task.data || {}), ...(data || {})};
2154
- return task;
2155
- });
2243
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
2156
2244
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2157
- expect(updateSpy).toHaveBeenCalledWith(payload.data);
2245
+ expect(sendStateMachineEventSpy).toHaveBeenCalled();
2246
+ const stateMachineEvent = sendStateMachineEventSpy.mock.calls.at(-1)?.[0];
2247
+ expect(stateMachineEvent?.type).toBe(TaskEvent.TASK_WRAPUP);
2248
+ expect(stateMachineEvent?.taskData).toEqual({
2249
+ ...payload.data,
2250
+ wrapUpRequired: true,
2251
+ });
2252
+ sendStateMachineEventSpy.mockRestore();
2158
2253
  });
2159
2254
 
2160
2255
  it('should not attempt cleanup twice when AGENT_CONTACT_UNASSIGNED is followed by AGENT_WRAPUP', () => {
@@ -2230,1367 +2325,157 @@ describe('TaskManager', () => {
2230
2325
  });
2231
2326
 
2232
2327
  describe('should emit appropriate task events for recording events', () => {
2233
- ['PAUSED', 'PAUSE_FAILED', 'RESUMED', 'RESUME_FAILED'].forEach((suffix) => {
2328
+ const eventMap: Record<string, TaskEvent | null> = {
2329
+ STARTED: TaskEvent.RECORDING_STARTED,
2330
+ PAUSED: TaskEvent.PAUSE_RECORDING,
2331
+ PAUSE_FAILED: null,
2332
+ RESUMED: TaskEvent.RESUME_RECORDING,
2333
+ RESUME_FAILED: null,
2334
+ };
2335
+
2336
+ ['STARTED', 'PAUSED', 'PAUSE_FAILED', 'RESUMED', 'RESUME_FAILED'].forEach((suffix) => {
2234
2337
  const ccEvent = CC_EVENTS[`CONTACT_RECORDING_${suffix}`];
2235
- const taskEvent = TASK_EVENTS[`TASK_RECORDING_${suffix}`];
2236
- it(`should emit ${taskEvent} on ${ccEvent} event`, () => {
2338
+ const expectedTaskEvent = eventMap[suffix];
2339
+ it(`should ${expectedTaskEvent ? 'send' : 'not send'} ${
2340
+ expectedTaskEvent ?? 'a'
2341
+ } state machine event on ${ccEvent} event`, () => {
2237
2342
  const payload = {data: {...initalPayload.data, type: ccEvent}};
2238
2343
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
2239
2344
  const task = taskManager.getTask(taskId);
2240
- const spy = jest.spyOn(task, 'emit');
2345
+ const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
2346
+ sendStateMachineEventSpy.mockClear();
2241
2347
 
2242
2348
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2243
- expect(spy).toHaveBeenCalledWith(taskEvent, task);
2349
+ if (expectedTaskEvent) {
2350
+ const stateMachineEvent = expectLastStateMachineEvent(
2351
+ sendStateMachineEventSpy,
2352
+ expectedTaskEvent
2353
+ );
2354
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
2355
+ } else {
2356
+ expect(sendStateMachineEventSpy).not.toHaveBeenCalled();
2357
+ }
2244
2358
  });
2245
2359
  });
2246
2360
  });
2247
2361
 
2248
2362
  describe('Conference event handling', () => {
2249
2363
  let task;
2250
- const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
2251
2364
 
2252
2365
  beforeEach(() => {
2253
- // Set the agentId on taskManager before tests run
2254
- taskManager.setAgentId(agentId);
2255
-
2256
2366
  task = {
2257
2367
  data: {interactionId: taskId},
2258
2368
  emit: jest.fn(),
2259
- updateTaskData: jest.fn().mockImplementation((updatedData) => {
2260
- // Mock the updateTaskData method to actually update task.data
2261
- task.data = {...task.data, ...updatedData};
2262
- return task;
2263
- }),
2369
+ updateTaskData: jest.fn(),
2370
+ sendStateMachineEvent: jest.fn(),
2264
2371
  };
2265
- taskManager.taskCollection[taskId] = task;
2372
+ taskManager.taskCollection[taskId] = task as any;
2266
2373
  });
2267
2374
 
2268
- it('should handle AGENT_CONSULT_CONFERENCED event', () => {
2375
+ it('sends AGENT_CONSULT_CONFERENCED to state machine as CONFERENCE_START', () => {
2269
2376
  const payload = {
2270
- data: {
2271
- type: CC_EVENTS.AGENT_CONSULT_CONFERENCED,
2272
- interactionId: taskId,
2273
- isConferencing: true,
2274
- },
2377
+ data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCED, interactionId: taskId},
2275
2378
  };
2276
-
2277
2379
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2278
-
2279
- expect(task.data.isConferencing).toBe(true);
2280
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
2380
+ expect(task.sendStateMachineEvent).toHaveBeenCalled();
2381
+ const call = task.sendStateMachineEvent.mock.calls[0][0];
2382
+ expect(call.type).toBe(TaskEvent.CONFERENCE_START);
2281
2383
  });
2282
2384
 
2283
- it('should handle AGENT_CONSULT_CONFERENCING event', () => {
2385
+ it('sends PARTICIPANT_JOINED_CONFERENCE to state machine as CONFERENCE_START', () => {
2284
2386
  const payload = {
2285
- data: {
2286
- type: CC_EVENTS.AGENT_CONSULT_CONFERENCING,
2287
- interactionId: taskId,
2288
- isConferencing: true,
2289
- },
2387
+ data: {type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE, interactionId: taskId},
2290
2388
  };
2291
-
2292
2389
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2293
-
2294
- expect(task.data.isConferencing).toBe(true);
2295
- // No task event emission for conferencing - only for conferenced (completed)
2296
- expect(task.emit).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
2390
+ expect(task.sendStateMachineEvent).toHaveBeenCalled();
2391
+ const call = task.sendStateMachineEvent.mock.calls[0][0];
2392
+ expect(call.type).toBe(TaskEvent.CONFERENCE_START);
2297
2393
  });
2298
2394
 
2299
- it('should handle AGENT_CONSULT_CONFERENCE_FAILED event', () => {
2395
+ it('sends AGENT_CONSULT_CONFERENCE_FAILED to state machine as CONFERENCE_FAILED', () => {
2300
2396
  const payload = {
2301
- data: {
2302
- type: CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED,
2303
- interactionId: taskId,
2304
- reason: 'Network error',
2305
- },
2397
+ data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED, interactionId: taskId},
2306
2398
  };
2307
-
2308
2399
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2309
-
2310
- expect(task.data.reason).toBe('Network error');
2311
- // No event emission expected for failure - handled by contact method promise rejection
2400
+ expect(task.sendStateMachineEvent).toHaveBeenCalled();
2401
+ const call = task.sendStateMachineEvent.mock.calls[0][0];
2402
+ expect(call.type).toBe(TaskEvent.CONFERENCE_FAILED);
2312
2403
  });
2313
2404
 
2314
- it('should handle PARTICIPANT_JOINED_CONFERENCE event', () => {
2405
+ it('sends PARTICIPANT_LEFT_CONFERENCE to state machine as PARTICIPANT_LEAVE', () => {
2315
2406
  const payload = {
2316
- data: {
2317
- type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE,
2318
- interactionId: taskId,
2319
- participantId: 'new-participant-123',
2320
- participantType: 'agent',
2321
- interaction: {
2322
- participants: {
2323
- [agentId]: {pType: 'Agent', hasLeft: false},
2324
- 'new-participant-123': {pType: 'Agent', hasLeft: false},
2325
- },
2326
- media: {
2327
- [taskId]: {
2328
- mType: 'mainCall',
2329
- participants: [agentId, 'new-participant-123'],
2330
- },
2331
- },
2332
- },
2333
- },
2407
+ data: {type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, interactionId: taskId},
2334
2408
  };
2335
-
2336
2409
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2337
-
2338
- expect(task.data.participantId).toBe('new-participant-123');
2339
- expect(task.data.participantType).toBe('agent');
2340
- // No specific task event emission for participant joined - just data update
2410
+ expect(task.sendStateMachineEvent).toHaveBeenCalled();
2411
+ const call = task.sendStateMachineEvent.mock.calls[0][0];
2412
+ expect(call.type).toBe(TaskEvent.PARTICIPANT_LEAVE);
2341
2413
  });
2342
2414
 
2343
- it('should call updateTaskData only once for PARTICIPANT_JOINED_CONFERENCE with pre-calculated isConferenceInProgress', () => {
2415
+ it('handles AGENT_CONSULT_CONFERENCING event without errors', () => {
2344
2416
  const payload = {
2345
- data: {
2346
- type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE,
2347
- interactionId: taskId,
2348
- participantId: 'new-agent-789',
2349
- interaction: {
2350
- participants: {
2351
- [agentId]: {pType: 'Agent', hasLeft: false},
2352
- 'agent-2': {pType: 'Agent', hasLeft: false},
2353
- 'new-agent-789': {pType: 'Agent', hasLeft: false},
2354
- 'customer-1': {pType: 'Customer', hasLeft: false},
2355
- },
2356
- media: {
2357
- [taskId]: {
2358
- mType: 'mainCall',
2359
- participants: [agentId, 'agent-2', 'new-agent-789', 'customer-1'],
2360
- },
2361
- },
2362
- },
2363
- },
2417
+ data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCING, interactionId: taskId},
2364
2418
  };
2365
-
2366
- const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
2367
-
2368
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2369
-
2370
- // Verify updateTaskData was called exactly once
2371
- expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
2372
-
2373
- // Verify it was called with isConferenceInProgress already calculated
2374
- expect(updateTaskDataSpy).toHaveBeenCalledWith(
2375
- expect.objectContaining({
2376
- participantId: 'new-agent-789',
2377
- isConferenceInProgress: true, // 3 active agents
2378
- })
2379
- );
2380
-
2381
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
2382
- });
2383
-
2384
- describe('PARTICIPANT_LEFT_CONFERENCE event handling', () => {
2385
- it('should call updateTaskData only once for PARTICIPANT_LEFT_CONFERENCE with pre-calculated isConferenceInProgress', () => {
2386
- const payload = {
2387
- data: {
2388
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2389
- interactionId: taskId,
2390
- interaction: {
2391
- participants: {
2392
- [agentId]: {pType: 'Agent', hasLeft: false},
2393
- 'agent-2': {pType: 'Agent', hasLeft: true}, // This agent left
2394
- 'customer-1': {pType: 'Customer', hasLeft: false},
2395
- },
2396
- media: {
2397
- [taskId]: {
2398
- mType: 'mainCall',
2399
- participants: [agentId, 'customer-1'], // agent-2 removed from participants
2400
- },
2401
- },
2402
- },
2403
- },
2404
- };
2405
-
2406
- const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
2407
-
2408
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2409
-
2410
- // Verify updateTaskData was called exactly once
2411
- expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
2412
-
2413
- // Verify it was called with isConferenceInProgress already calculated
2414
- expect(updateTaskDataSpy).toHaveBeenCalledWith(
2415
- expect.objectContaining({
2416
- isConferenceInProgress: false, // Only 1 active agent remains
2417
- })
2418
- );
2419
-
2420
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2421
- });
2422
-
2423
- it('should emit TASK_PARTICIPANT_LEFT event when participant leaves conference', () => {
2424
- const payload = {
2425
- data: {
2426
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2427
- interactionId: taskId,
2428
- interaction: {
2429
- participants: {
2430
- [agentId]: {
2431
- hasLeft: false,
2432
- },
2433
- },
2434
- },
2435
- },
2436
- };
2437
-
2438
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2439
-
2440
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2441
- });
2442
-
2443
- it('should NOT remove task when agent is still in interaction', () => {
2444
- const payload = {
2445
- data: {
2446
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2447
- interactionId: taskId,
2448
- interaction: {
2449
- participants: {
2450
- [agentId]: {
2451
- hasLeft: false,
2452
- },
2453
- },
2454
- },
2455
- },
2456
- };
2457
-
2458
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2459
-
2460
- // Task should still exist in collection
2461
- expect(taskManager.getTask(taskId)).toBeDefined();
2462
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2463
- });
2464
-
2465
- it('should NOT remove task when agent left but is in main interaction', () => {
2466
- const payload = {
2467
- data: {
2468
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2469
- interactionId: taskId,
2470
- interaction: {
2471
- participants: {
2472
- [agentId]: {
2473
- hasLeft: true,
2474
- },
2475
- },
2476
- media: {
2477
- [taskId]: {
2478
- mType: 'mainCall',
2479
- participants: [agentId],
2480
- },
2481
- },
2482
- },
2483
- },
2484
- };
2485
-
2486
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2487
-
2488
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2489
-
2490
- // Task should still exist - not removed
2491
- expect(removeTaskSpy).not.toHaveBeenCalled();
2492
- expect(taskManager.getTask(taskId)).toBeDefined();
2493
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2494
- });
2495
-
2496
- it('should NOT remove task when agent left but is primary (owner)', () => {
2497
- const payload = {
2498
- data: {
2499
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2500
- interactionId: taskId,
2501
- interaction: {
2502
- participants: {
2503
- [agentId]: {
2504
- hasLeft: true,
2505
- },
2506
- },
2507
- owner: agentId,
2508
- media: {
2509
- [taskId]: {
2510
- mType: 'consultCall',
2511
- participants: ['other-agent'],
2512
- },
2513
- },
2514
- },
2515
- },
2516
- };
2517
-
2518
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2519
-
2520
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2521
-
2522
- // Task should still exist - not removed because agent is primary
2523
- expect(removeTaskSpy).not.toHaveBeenCalled();
2524
- expect(taskManager.getTask(taskId)).toBeDefined();
2525
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2526
- });
2527
-
2528
- it('should remove task when agent left and is NOT in main interaction and is NOT primary', () => {
2529
- const payload = {
2530
- data: {
2531
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2532
- interactionId: taskId,
2533
- interaction: {
2534
- participants: {
2535
- [agentId]: {
2536
- hasLeft: true,
2537
- },
2538
- },
2539
- owner: 'another-agent-id',
2540
- media: {
2541
- [taskId]: {
2542
- mType: 'mainCall',
2543
- participants: ['another-agent-id'],
2544
- },
2545
- },
2546
- },
2547
- },
2548
- };
2549
-
2550
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2551
-
2552
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2553
-
2554
- // Task should be removed
2555
- expect(removeTaskSpy).toHaveBeenCalled();
2556
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2557
- });
2558
-
2559
- it('should remove task when agent is not in participants list', () => {
2560
- const payload = {
2561
- data: {
2562
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2563
- interactionId: taskId,
2564
- interaction: {
2565
- participants: {
2566
- 'other-agent-id': {
2567
- hasLeft: false,
2568
- },
2569
- },
2570
- owner: 'another-agent-id',
2571
- },
2572
- },
2573
- };
2574
-
2575
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2576
-
2577
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2578
-
2579
- // Task should be removed because agent is not in participants
2580
- expect(removeTaskSpy).toHaveBeenCalled();
2581
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2582
- });
2583
-
2584
- it('should update isConferenceInProgress based on remaining active agents', () => {
2585
- const payload = {
2586
- data: {
2587
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2588
- interactionId: taskId,
2589
- interaction: {
2590
- participants: {
2591
- [agentId]: {
2592
- hasLeft: false,
2593
- pType: 'Agent',
2594
- },
2595
- 'agent-2': {
2596
- hasLeft: false,
2597
- pType: 'Agent',
2598
- },
2599
- 'customer-1': {
2600
- hasLeft: false,
2601
- pType: 'Customer',
2602
- },
2603
- },
2604
- media: {
2605
- [taskId]: {
2606
- mType: 'mainCall',
2607
- participants: [agentId, 'agent-2', 'customer-1'],
2608
- },
2609
- },
2610
- },
2611
- },
2612
- };
2613
-
2614
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2615
-
2616
- // isConferenceInProgress should be true (2 active agents)
2617
- expect(task.data.isConferenceInProgress).toBe(true);
2618
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2619
- });
2620
-
2621
- it('should set isConferenceInProgress to false when only one agent remains', () => {
2622
- const payload = {
2623
- data: {
2624
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2625
- interactionId: taskId,
2626
- interaction: {
2627
- participants: {
2628
- [agentId]: {
2629
- hasLeft: false,
2630
- pType: 'Agent',
2631
- },
2632
- 'agent-2': {
2633
- hasLeft: true,
2634
- pType: 'Agent',
2635
- },
2636
- 'customer-1': {
2637
- hasLeft: false,
2638
- pType: 'Customer',
2639
- },
2640
- },
2641
- media: {
2642
- [taskId]: {
2643
- mType: 'mainCall',
2644
- participants: [agentId, 'customer-1'],
2645
- },
2646
- },
2647
- },
2648
- },
2649
- };
2650
-
2651
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2652
-
2653
- // isConferenceInProgress should be false (only 1 active agent)
2654
- expect(task.data.isConferenceInProgress).toBe(false);
2655
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2656
- });
2657
-
2658
- it('should handle participant left when no participants data exists', () => {
2659
- const payload = {
2660
- data: {
2661
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2662
- interactionId: taskId,
2663
- interaction: {},
2664
- },
2665
- };
2666
-
2667
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2668
-
2419
+ expect(() => {
2669
2420
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2670
-
2671
- // When no participants data exists, checkParticipantNotInInteraction returns true
2672
- // Since agent won't be in main interaction either, task should be removed
2673
- expect(removeTaskSpy).toHaveBeenCalled();
2674
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2675
- });
2421
+ }).not.toThrow();
2676
2422
  });
2677
2423
 
2678
- it('should handle PARTICIPANT_LEFT_CONFERENCE_FAILED event', () => {
2424
+ it('handles PARTICIPANT_LEFT_CONFERENCE_FAILED event without errors', () => {
2679
2425
  const payload = {
2680
- data: {
2681
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED,
2682
- interactionId: taskId,
2683
- reason: 'Exit failed',
2684
- },
2426
+ data: {type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED, interactionId: taskId},
2685
2427
  };
2686
-
2687
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2688
-
2689
- expect(task.data.reason).toBe('Exit failed');
2690
- // No event emission expected for failure - handled by contact method promise rejection
2428
+ expect(() => {
2429
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2430
+ }).not.toThrow();
2691
2431
  });
2692
2432
 
2693
- it('should only update task for matching interactionId', () => {
2433
+ it('only routes conference events to matching tasks', () => {
2694
2434
  const otherTaskId = 'other-task-id';
2695
- const otherTask = {
2435
+ const otherTask: any = {
2696
2436
  data: {interactionId: otherTaskId},
2697
2437
  emit: jest.fn(),
2438
+ updateTaskData: jest.fn(),
2439
+ sendStateMachineEvent: jest.fn(),
2698
2440
  };
2699
2441
  taskManager.taskCollection[otherTaskId] = otherTask;
2700
2442
 
2701
2443
  const payload = {
2702
- data: {
2703
- type: CC_EVENTS.AGENT_CONSULT_CONFERENCED,
2704
- interactionId: taskId,
2705
- isConferencing: true,
2706
- },
2444
+ data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCED, interactionId: taskId},
2707
2445
  };
2708
-
2709
2446
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2710
2447
 
2711
- // Only the matching task should be updated
2712
- expect(task.data.isConferencing).toBe(true);
2713
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
2714
-
2715
- // Other task should not be affected
2716
- expect(otherTask.data.isConferencing).toBeUndefined();
2717
- expect(otherTask.emit).not.toHaveBeenCalled();
2448
+ expect(task.sendStateMachineEvent).toHaveBeenCalled();
2449
+ expect(otherTask.sendStateMachineEvent).not.toHaveBeenCalled();
2718
2450
  });
2719
2451
  });
2720
2452
 
2721
- describe('handleTaskCleanup - stage changes', () => {
2722
- const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
2723
-
2724
- beforeEach(() => {
2725
- taskManager.setAgentId(agentId);
2726
- });
2727
-
2728
- it('should remove OUTDIAL task on CONTACT_ENDED when current agent is NOT in agentsPendingWrapUp', () => {
2729
- const task = taskManager.getTask(taskId);
2730
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
2731
- task.data = {
2732
- ...task.data,
2733
- ...newData,
2734
- interaction: {
2735
- ...task.data.interaction,
2736
- outboundType: 'OUTDIAL',
2737
- state: 'new',
2738
- mediaType: 'telephony',
2739
- },
2740
- agentsPendingWrapUp: ['different-agent-123'], // Current agent not in the list
2741
- };
2742
- return task;
2743
- });
2744
- task.unregisterWebCallListeners = jest.fn();
2745
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2746
-
2747
- const payload = {
2748
- data: {
2749
- type: CC_EVENTS.CONTACT_ENDED,
2750
- interactionId: taskId,
2751
- interaction: {
2752
- outboundType: 'OUTDIAL',
2753
- state: 'new',
2754
- mediaType: 'telephony',
2755
- },
2756
- agentsPendingWrapUp: ['different-agent-123'], // Current agent not in the list
2757
- },
2758
- };
2759
-
2760
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2761
-
2762
- expect(removeTaskSpy).toHaveBeenCalled();
2763
- expect(taskManager.getTask(taskId)).toBeUndefined();
2764
- });
2765
-
2766
- it('should NOT remove OUTDIAL task on CONTACT_ENDED when current agent IS in agentsPendingWrapUp', () => {
2767
- const task = taskManager.getTask(taskId);
2768
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
2769
- task.data = {
2770
- ...task.data,
2771
- ...newData,
2772
- interaction: {
2773
- ...task.data.interaction,
2774
- outboundType: 'OUTDIAL',
2775
- state: 'new',
2776
- mediaType: 'telephony',
2777
- },
2778
- agentsPendingWrapUp: [agentId, 'other-agent-456'], // Current agent IS in the list
2779
- };
2780
- return task;
2781
- });
2782
- task.unregisterWebCallListeners = jest.fn();
2783
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2784
-
2785
- const payload = {
2786
- data: {
2787
- type: CC_EVENTS.CONTACT_ENDED,
2788
- interactionId: taskId,
2789
- interaction: {
2790
- outboundType: 'OUTDIAL',
2791
- state: 'new',
2792
- mediaType: 'telephony',
2793
- },
2794
- agentsPendingWrapUp: [agentId, 'other-agent-456'], // Current agent IS in the list
2795
- },
2796
- };
2797
-
2798
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2799
-
2800
- expect(removeTaskSpy).not.toHaveBeenCalled();
2801
- expect(taskManager.getTask(taskId)).toBeDefined();
2802
- });
2803
-
2804
- it('should remove OUTDIAL task when needsWrapUp is false and task is outdial', () => {
2805
- const task = taskManager.getTask(taskId);
2806
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
2807
- task.data = {
2808
- ...task.data,
2809
- ...newData,
2810
- interaction: {
2811
- ...task.data.interaction,
2812
- outboundType: 'OUTDIAL',
2813
- state: 'WRAPUP', // Not 'new' state
2814
- mediaType: 'telephony',
2815
- },
2816
- agentsPendingWrapUp: [], // No agents pending wrap-up
2817
- };
2818
- return task;
2819
- });
2820
- task.unregisterWebCallListeners = jest.fn();
2821
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2822
-
2823
- const payload = {
2824
- data: {
2825
- type: CC_EVENTS.CONTACT_ENDED,
2826
- interactionId: taskId,
2827
- interaction: {
2828
- outboundType: 'OUTDIAL',
2829
- state: 'WRAPUP', // Not 'new' state
2830
- mediaType: 'telephony',
2831
- },
2832
- agentsPendingWrapUp: [], // No agents pending wrap-up
2833
- },
2834
- };
2835
-
2836
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2837
-
2838
- expect(removeTaskSpy).toHaveBeenCalled();
2839
- expect(taskManager.getTask(taskId)).toBeUndefined();
2840
- });
2841
-
2842
- it('should remove OUTDIAL task when needsWrapUp is false (agentsPendingWrapUp is undefined)', () => {
2843
- const task = taskManager.getTask(taskId);
2844
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
2845
- task.data = {
2846
- ...task.data,
2847
- ...newData,
2848
- interaction: {
2849
- ...task.data.interaction,
2850
- outboundType: 'OUTDIAL',
2851
- state: 'WRAPUP',
2852
- mediaType: 'telephony',
2853
- },
2854
- agentsPendingWrapUp: undefined, // No agentsPendingWrapUp field
2855
- };
2856
- return task;
2857
- });
2858
- task.unregisterWebCallListeners = jest.fn();
2859
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2860
-
2861
- const payload = {
2862
- data: {
2863
- type: CC_EVENTS.CONTACT_ENDED,
2864
- interactionId: taskId,
2865
- interaction: {
2866
- outboundType: 'OUTDIAL',
2867
- state: 'WRAPUP',
2868
- mediaType: 'telephony',
2869
- },
2870
- // agentsPendingWrapUp not included
2871
- },
2872
- };
2873
-
2874
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2875
-
2876
- expect(removeTaskSpy).toHaveBeenCalled();
2877
- expect(taskManager.getTask(taskId)).toBeUndefined();
2878
- });
2453
+ describe('state machine integration', () => {
2454
+ it('maps CC events to task state machine events using normalized payload', () => {
2455
+ const mapped = (TaskManager as any).mapEventToTaskStateMachineEvent(
2456
+ CC_EVENTS.AGENT_CONTACT_ASSIGNED,
2457
+ {...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED}
2458
+ );
2879
2459
 
2880
- it('should NOT remove OUTDIAL task when needsWrapUp is true (current agent in agentsPendingWrapUp) even if state is WRAPUP', () => {
2881
- const task = taskManager.getTask(taskId);
2882
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
2883
- task.data = {
2884
- ...task.data,
2885
- ...newData,
2886
- interaction: {
2887
- ...task.data.interaction,
2888
- outboundType: 'OUTDIAL',
2889
- state: 'WRAPUP',
2890
- mediaType: 'telephony',
2891
- },
2892
- agentsPendingWrapUp: [agentId], // Current agent needs wrap-up
2893
- };
2894
- return task;
2460
+ expect(mapped).toEqual({
2461
+ type: TaskEvent.ASSIGN,
2462
+ taskData: {...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED},
2895
2463
  });
2896
- task.unregisterWebCallListeners = jest.fn();
2897
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2898
-
2899
- const payload = {
2900
- data: {
2901
- type: CC_EVENTS.CONTACT_ENDED,
2902
- interactionId: taskId,
2903
- interaction: {
2904
- outboundType: 'OUTDIAL',
2905
- state: 'WRAPUP',
2906
- mediaType: 'telephony',
2907
- },
2908
- agentsPendingWrapUp: [agentId], // Current agent needs wrap-up
2909
- },
2910
- };
2911
-
2912
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2913
-
2914
- expect(removeTaskSpy).not.toHaveBeenCalled();
2915
- expect(taskManager.getTask(taskId)).toBeDefined();
2916
2464
  });
2917
2465
 
2918
- it('should remove non-OUTDIAL task when state is new regardless of agentsPendingWrapUp', () => {
2466
+ it('sends mapped events to the task state machine service', () => {
2467
+ const payload = {...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED};
2919
2468
  const task = taskManager.getTask(taskId);
2920
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
2921
- task.data = {
2922
- ...task.data,
2923
- ...newData,
2924
- interaction: {
2925
- ...task.data.interaction,
2926
- outboundType: 'PREVIEW', // Not OUTDIAL
2927
- state: 'new',
2928
- mediaType: 'telephony',
2929
- },
2930
- agentsPendingWrapUp: [agentId],
2931
- };
2932
- return task;
2933
- });
2934
- task.unregisterWebCallListeners = jest.fn();
2935
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2936
-
2937
- const payload = {
2938
- data: {
2939
- type: CC_EVENTS.CONTACT_ENDED,
2940
- interactionId: taskId,
2941
- interaction: {
2942
- outboundType: 'PREVIEW',
2943
- state: 'new',
2944
- mediaType: 'telephony',
2945
- },
2946
- agentsPendingWrapUp: [agentId],
2947
- },
2948
- };
2949
-
2950
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2469
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
2951
2470
 
2952
- expect(removeTaskSpy).toHaveBeenCalled();
2953
- expect(taskManager.getTask(taskId)).toBeUndefined();
2954
- });
2471
+ webSocketManagerMock.emit('message', JSON.stringify({data: payload}));
2955
2472
 
2956
- it('should handle agentsPendingWrapUp with multiple agents correctly - remove if current agent not in list', () => {
2957
- const task = taskManager.getTask(taskId);
2958
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
2959
- task.data = {
2960
- ...task.data,
2961
- ...newData,
2962
- interaction: {
2963
- ...task.data.interaction,
2964
- outboundType: 'OUTDIAL',
2965
- state: 'new',
2966
- mediaType: 'telephony',
2967
- },
2968
- agentsPendingWrapUp: ['agent-1', 'agent-2', 'agent-3'], // Current agent not in the list
2969
- };
2970
- return task;
2473
+ expect(sendStateMachineEventSpy).toHaveBeenCalledWith({
2474
+ type: TaskEvent.ASSIGN,
2475
+ taskData: payload,
2971
2476
  });
2972
- task.unregisterWebCallListeners = jest.fn();
2973
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2974
-
2975
- const payload = {
2976
- data: {
2977
- type: CC_EVENTS.CONTACT_ENDED,
2978
- interactionId: taskId,
2979
- interaction: {
2980
- outboundType: 'OUTDIAL',
2981
- state: 'new',
2982
- mediaType: 'telephony',
2983
- },
2984
- agentsPendingWrapUp: ['agent-1', 'agent-2', 'agent-3'],
2985
- },
2986
- };
2987
-
2988
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2989
-
2990
- expect(removeTaskSpy).toHaveBeenCalled();
2991
- expect(taskManager.getTask(taskId)).toBeUndefined();
2992
- });
2993
-
2994
- it('should handle agentsPendingWrapUp with multiple agents correctly - keep if current agent is in list', () => {
2995
- const task = taskManager.getTask(taskId);
2996
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
2997
- task.data = {
2998
- ...task.data,
2999
- ...newData,
3000
- interaction: {
3001
- ...task.data.interaction,
3002
- outboundType: 'OUTDIAL',
3003
- state: 'new',
3004
- mediaType: 'telephony',
3005
- },
3006
- agentsPendingWrapUp: ['agent-1', agentId, 'agent-3'], // Current agent IS in the list
3007
- };
3008
- return task;
3009
- });
3010
- task.unregisterWebCallListeners = jest.fn();
3011
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
3012
-
3013
- const payload = {
3014
- data: {
3015
- type: CC_EVENTS.CONTACT_ENDED,
3016
- interactionId: taskId,
3017
- interaction: {
3018
- outboundType: 'OUTDIAL',
3019
- state: 'new',
3020
- mediaType: 'telephony',
3021
- },
3022
- agentsPendingWrapUp: ['agent-1', agentId, 'agent-3'],
3023
- },
3024
- };
3025
-
3026
- webSocketManagerMock.emit('message', JSON.stringify(payload));
3027
-
3028
- expect(removeTaskSpy).not.toHaveBeenCalled();
3029
- expect(taskManager.getTask(taskId)).toBeDefined();
3030
- });
3031
- });
3032
-
3033
- describe('CONTACT_MERGED event handling', () => {
3034
- let task;
3035
- let taskEmitSpy;
3036
- let managerEmitSpy;
3037
-
3038
- beforeEach(() => {
3039
- // Create initial task
3040
- webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
3041
- task = taskManager.getTask(taskId);
3042
- taskEmitSpy = jest.spyOn(task, 'emit');
3043
- managerEmitSpy = jest.spyOn(taskManager, 'emit');
3044
- });
3045
-
3046
- it('should update existing task data and emit TASK_MERGED event when CONTACT_MERGED is received', () => {
3047
- const mergedPayload = {
3048
- data: {
3049
- type: CC_EVENTS.CONTACT_MERGED,
3050
- interactionId: taskId,
3051
- agentId: taskDataMock.agentId,
3052
- interaction: {
3053
- ...taskDataMock.interaction,
3054
- state: 'merged',
3055
- customField: 'updated-value',
3056
- },
3057
- },
3058
- };
3059
-
3060
- webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
3061
-
3062
- const updatedTask = taskManager.getTask(taskId);
3063
- expect(updatedTask).toBeDefined();
3064
- expect(updatedTask.data.interaction.customField).toBe('updated-value');
3065
- expect(updatedTask.data.interaction.state).toBe('merged');
3066
- expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, updatedTask);
3067
- });
3068
-
3069
- it('should create new task when CONTACT_MERGED is received for non-existing task', () => {
3070
- const newMergedTaskId = 'new-merged-task-id';
3071
- const mergedPayload = {
3072
- data: {
3073
- type: CC_EVENTS.CONTACT_MERGED,
3074
- interactionId: newMergedTaskId,
3075
- agentId: taskDataMock.agentId,
3076
- interaction: {
3077
- mediaType: 'telephony',
3078
- state: 'merged',
3079
- participants: {
3080
- [taskDataMock.agentId]: {
3081
- isWrapUp: false,
3082
- hasJoined: true,
3083
- },
3084
- },
3085
- },
3086
- },
3087
- };
3088
-
3089
- // Verify task doesn't exist before
3090
- expect(taskManager.getTask(newMergedTaskId)).toBeUndefined();
3091
-
3092
- webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
3093
-
3094
- // Verify task was created
3095
- const newTask = taskManager.getTask(newMergedTaskId);
3096
- expect(newTask).toBeDefined();
3097
- expect(newTask.data.interactionId).toBe(newMergedTaskId);
3098
- expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, newTask);
3099
- });
3100
-
3101
- it('should remove child task when childInteractionId is present in CONTACT_MERGED', () => {
3102
- const childTaskId = 'child-task-id';
3103
- const parentTaskId = 'parent-task-id';
3104
-
3105
- // Create child task
3106
- const childPayload = {
3107
- data: {
3108
- type: CC_EVENTS.AGENT_CONTACT_RESERVED,
3109
- interactionId: childTaskId,
3110
- agentId: taskDataMock.agentId,
3111
- interaction: {mediaType: 'telephony'},
3112
- },
3113
- };
3114
- webSocketManagerMock.emit('message', JSON.stringify(childPayload));
3115
-
3116
- // Verify child task exists
3117
- expect(taskManager.getTask(childTaskId)).toBeDefined();
3118
-
3119
- // Create parent task
3120
- const parentPayload = {
3121
- data: {
3122
- type: CC_EVENTS.AGENT_CONTACT_RESERVED,
3123
- interactionId: parentTaskId,
3124
- agentId: taskDataMock.agentId,
3125
- interaction: {mediaType: 'telephony'},
3126
- },
3127
- };
3128
- webSocketManagerMock.emit('message', JSON.stringify(parentPayload));
3129
-
3130
- // Send CONTACT_MERGED with childInteractionId
3131
- const mergedPayload = {
3132
- data: {
3133
- type: CC_EVENTS.CONTACT_MERGED,
3134
- interactionId: parentTaskId,
3135
- childInteractionId: childTaskId,
3136
- agentId: taskDataMock.agentId,
3137
- interaction: {
3138
- mediaType: 'telephony',
3139
- state: 'merged',
3140
- },
3141
- },
3142
- };
3143
-
3144
- webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
3145
-
3146
- // Verify child task was removed
3147
- expect(taskManager.getTask(childTaskId)).toBeUndefined();
3148
-
3149
- // Verify parent task still exists
3150
- expect(taskManager.getTask(parentTaskId)).toBeDefined();
3151
-
3152
- // Verify TASK_MERGED event was emitted
3153
- expect(managerEmitSpy).toHaveBeenCalledWith(
3154
- TASK_EVENTS.TASK_MERGED,
3155
- expect.objectContaining({
3156
- data: expect.objectContaining({
3157
- interactionId: parentTaskId,
3158
- }),
3159
- })
3160
- );
3161
- });
3162
-
3163
- it('should handle CONTACT_MERGED with EP-DN participant correctly', () => {
3164
- const epdnTaskId = 'epdn-merged-task';
3165
- const mergedPayload = {
3166
- data: {
3167
- type: CC_EVENTS.CONTACT_MERGED,
3168
- interactionId: epdnTaskId,
3169
- agentId: taskDataMock.agentId,
3170
- interaction: {
3171
- mediaType: 'telephony',
3172
- state: 'merged',
3173
- participants: {
3174
- [taskDataMock.agentId]: {
3175
- type: 'Agent',
3176
- isWrapUp: false,
3177
- hasJoined: true,
3178
- },
3179
- 'epdn-participant': {
3180
- type: 'EpDn',
3181
- epId: 'entry-point-123',
3182
- isWrapUp: false,
3183
- },
3184
- },
3185
- },
3186
- },
3187
- };
3188
-
3189
- webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
3190
-
3191
- const mergedTask = taskManager.getTask(epdnTaskId);
3192
- expect(mergedTask).toBeDefined();
3193
- expect(mergedTask.data.interaction.participants['epdn-participant']).toBeDefined();
3194
- expect(mergedTask.data.interaction.participants['epdn-participant'].type).toBe('EpDn');
3195
- expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, mergedTask);
3196
- });
3197
-
3198
- it('should not affect other tasks when CONTACT_MERGED is received', () => {
3199
- const otherTaskId = 'other-unrelated-task';
3200
- const otherPayload = {
3201
- data: {
3202
- type: CC_EVENTS.AGENT_CONTACT_RESERVED,
3203
- interactionId: otherTaskId,
3204
- agentId: taskDataMock.agentId,
3205
- interaction: {mediaType: 'chat'},
3206
- },
3207
- };
3208
- webSocketManagerMock.emit('message', JSON.stringify(otherPayload));
3209
-
3210
- const otherTask = taskManager.getTask(otherTaskId);
3211
- const otherTaskEmitSpy = jest.spyOn(otherTask, 'emit');
3212
-
3213
- // Send CONTACT_MERGED for the original task
3214
- const mergedPayload = {
3215
- data: {
3216
- type: CC_EVENTS.CONTACT_MERGED,
3217
- interactionId: taskId,
3218
- agentId: taskDataMock.agentId,
3219
- interaction: {
3220
- mediaType: 'telephony',
3221
- state: 'merged',
3222
- },
3223
- },
3224
- };
3225
-
3226
- webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
3227
-
3228
- // Verify other task was not affected
3229
- expect(otherTaskEmitSpy).not.toHaveBeenCalled();
3230
- expect(otherTask.data.interaction.mediaType).toBe('chat');
3231
-
3232
- // Verify original task was updated
3233
- expect(managerEmitSpy).toHaveBeenCalledWith(
3234
- TASK_EVENTS.TASK_MERGED,
3235
- expect.objectContaining({
3236
- data: expect.objectContaining({
3237
- interactionId: taskId,
3238
- }),
3239
- })
3240
- );
3241
- });
3242
- });
3243
-
3244
- describe('Campaign Preview Reservation', () => {
3245
- it('should create a task and emit TASK_CAMPAIGN_PREVIEW_RESERVATION when AgentOfferCampaignReservation is received', () => {
3246
- const campaignPayload = {
3247
- data: {
3248
- type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3249
- interactionId: 'campaign-interaction-123',
3250
- agentId: taskDataMock.agentId,
3251
- orgId: taskDataMock.orgId,
3252
- trackingId: 'campaign-tracking-456',
3253
- interaction: {
3254
- mediaType: 'telephony',
3255
- callProcessingDetails: {
3256
- campaignId: 'campaign-789',
3257
- },
3258
- },
3259
- },
3260
- };
3261
-
3262
- const managerEmitSpy = jest.spyOn(taskManager, 'emit');
3263
-
3264
- webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
3265
-
3266
- // Should emit with a task object (not raw data)
3267
- expect(managerEmitSpy).toHaveBeenCalledWith(
3268
- TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION,
3269
- expect.objectContaining({
3270
- data: expect.objectContaining({
3271
- interactionId: 'campaign-interaction-123',
3272
- type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3273
- wrapUpRequired: false,
3274
- isAutoAnswering: false,
3275
- }),
3276
- })
3277
- );
3278
-
3279
- // Task should be in the collection so subsequent events (e.g. AGENT_CONTACT_ASSIGNED) can find it
3280
- expect(taskManager['taskCollection']['campaign-interaction-123']).toBeDefined();
3281
- });
3282
-
3283
- it('should not emit TASK_INCOMING for campaign preview reservation when incoming WebRTC call arrives', () => {
3284
- const campaignPayload = {
3285
- data: {
3286
- type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3287
- interactionId: 'campaign-interaction-123',
3288
- agentId: taskDataMock.agentId,
3289
- orgId: taskDataMock.orgId,
3290
- trackingId: 'campaign-tracking-456',
3291
- interaction: {
3292
- mediaType: 'telephony',
3293
- callProcessingDetails: {
3294
- campaignId: 'campaign-789',
3295
- },
3296
- },
3297
- },
3298
- };
3299
-
3300
- // Remove the default task so only the campaign preview task is in the collection
3301
- delete taskManager['taskCollection'][taskId];
3302
-
3303
- // Create campaign preview task via the reservation event
3304
- webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
3305
-
3306
- const managerEmitSpy = jest.spyOn(taskManager, 'emit');
3307
-
3308
- // Simulate an incoming WebRTC call
3309
- const incomingCallCb = onSpy.mock.calls[0][1];
3310
- incomingCallCb(mockCall);
3311
-
3312
- // TASK_INCOMING should NOT be emitted because the only telephony task is a campaign preview
3313
- expect(managerEmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, expect.anything());
3314
- });
3315
-
3316
- it('should update existing task when AgentOfferCampaignReservation is received for known interactionId', () => {
3317
- const campaignPayload = {
3318
- data: {
3319
- type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3320
- interactionId: 'campaign-interaction-123',
3321
- agentId: taskDataMock.agentId,
3322
- orgId: taskDataMock.orgId,
3323
- trackingId: 'campaign-tracking-456',
3324
- interaction: {
3325
- mediaType: 'telephony',
3326
- callProcessingDetails: {
3327
- campaignId: 'campaign-789',
3328
- },
3329
- },
3330
- },
3331
- };
3332
-
3333
- // Send the first reservation to create the task
3334
- webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
3335
-
3336
- const managerEmitSpy = jest.spyOn(taskManager, 'emit');
3337
-
3338
- // Send a second reservation for the same interactionId
3339
- webSocketManagerMock.emit('message', JSON.stringify(campaignPayload));
3340
-
3341
- expect(managerEmitSpy).toHaveBeenCalledWith(
3342
- TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_RESERVATION,
3343
- expect.objectContaining({
3344
- data: expect.objectContaining({
3345
- interactionId: 'campaign-interaction-123',
3346
- }),
3347
- })
3348
- );
3349
- });
3350
-
3351
- it('should emit TASK_CAMPAIGN_CONTACT_UPDATED and NOT remove task when CampaignContactUpdated is received', () => {
3352
- const campaignInteractionId = 'campaign-interaction-123';
3353
-
3354
- // First create a campaign preview task
3355
- const reservationPayload = {
3356
- data: {
3357
- type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3358
- interactionId: campaignInteractionId,
3359
- agentId: taskDataMock.agentId,
3360
- orgId: taskDataMock.orgId,
3361
- trackingId: 'campaign-tracking-456',
3362
- interaction: {
3363
- mediaType: 'telephony',
3364
- callProcessingDetails: {
3365
- campaignId: 'campaign-789',
3366
- },
3367
- },
3368
- },
3369
- };
3370
-
3371
- webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
3372
-
3373
- // Verify task exists in collection
3374
- const task = taskManager['taskCollection'][campaignInteractionId];
3375
- expect(task).toBeDefined();
3376
-
3377
- const taskEmitSpy = jest.spyOn(task, 'emit');
3378
-
3379
- // Now send CampaignContactUpdated
3380
- const campaignContactUpdatedPayload = {
3381
- data: {
3382
- type: CC_EVENTS.CAMPAIGN_CONTACT_UPDATED,
3383
- interactionId: campaignInteractionId,
3384
- agentId: taskDataMock.agentId,
3385
- orgId: taskDataMock.orgId,
3386
- interaction: {
3387
- mediaType: 'telephony',
3388
- state: 'new',
3389
- callProcessingDetails: {
3390
- campaignId: 'campaign-789',
3391
- },
3392
- },
3393
- },
3394
- };
3395
-
3396
- webSocketManagerMock.emit('message', JSON.stringify(campaignContactUpdatedPayload));
3397
-
3398
- // Task should still exist in collection (not removed — non-terminal event)
3399
- expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
3400
-
3401
- // TASK_CAMPAIGN_CONTACT_UPDATED should have been emitted
3402
- expect(taskEmitSpy).toHaveBeenCalledWith(
3403
- TASK_EVENTS.TASK_CAMPAIGN_CONTACT_UPDATED,
3404
- expect.objectContaining({
3405
- data: expect.objectContaining({
3406
- interactionId: campaignInteractionId,
3407
- }),
3408
- })
3409
- );
3410
-
3411
- // TASK_END should NOT have been emitted
3412
- expect(taskEmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.anything());
3413
- });
3414
-
3415
- it('should emit TASK_CAMPAIGN_PREVIEW_ACCEPT_FAILED when CampaignPreviewAcceptFailed is received', () => {
3416
- const campaignInteractionId = 'campaign-interaction-123';
3417
-
3418
- // First create a campaign preview task
3419
- const reservationPayload = {
3420
- data: {
3421
- type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3422
- interactionId: campaignInteractionId,
3423
- agentId: taskDataMock.agentId,
3424
- orgId: taskDataMock.orgId,
3425
- trackingId: 'campaign-tracking-456',
3426
- interaction: {
3427
- mediaType: 'telephony',
3428
- callProcessingDetails: {
3429
- campaignId: 'campaign-789',
3430
- },
3431
- },
3432
- },
3433
- };
3434
-
3435
- webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
3436
-
3437
- const task = taskManager['taskCollection'][campaignInteractionId];
3438
- expect(task).toBeDefined();
3439
-
3440
- const taskEmitSpy = jest.spyOn(task, 'emit');
3441
-
3442
- const failPayload = {
3443
- data: {
3444
- type: CC_EVENTS.CAMPAIGN_PREVIEW_ACCEPT_FAILED,
3445
- interactionId: campaignInteractionId,
3446
- campaignId: 'campaign-789',
3447
- reason: 'INTERNAL_ERROR',
3448
- },
3449
- };
3450
-
3451
- webSocketManagerMock.emit('message', JSON.stringify(failPayload));
3452
-
3453
- expect(taskEmitSpy).toHaveBeenCalledWith(
3454
- TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_ACCEPT_FAILED,
3455
- expect.objectContaining({
3456
- data: expect.objectContaining({interactionId: campaignInteractionId}),
3457
- })
3458
- );
3459
- // Task should still exist (failure is non-terminal)
3460
- expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
3461
-
3462
- // Failure payload (reason, campaignId) should be merged into task.data
3463
- expect(task.data.reason).toBe('INTERNAL_ERROR');
3464
-
3465
- // Original reservation data must be preserved
3466
- expect(task.data.interaction).toBeDefined();
3467
- expect(task.data.interaction.callProcessingDetails.campaignId).toBe('campaign-789');
3468
- });
3469
-
3470
- it('should emit TASK_CAMPAIGN_PREVIEW_SKIP_FAILED when CampaignPreviewSkipFailed is received', () => {
3471
- const campaignInteractionId = 'campaign-interaction-123';
3472
-
3473
- const reservationPayload = {
3474
- data: {
3475
- type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3476
- interactionId: campaignInteractionId,
3477
- agentId: taskDataMock.agentId,
3478
- orgId: taskDataMock.orgId,
3479
- trackingId: 'campaign-tracking-456',
3480
- interaction: {
3481
- mediaType: 'telephony',
3482
- callProcessingDetails: {
3483
- campaignId: 'campaign-789',
3484
- },
3485
- },
3486
- },
3487
- };
3488
-
3489
- webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
3490
-
3491
- const task = taskManager['taskCollection'][campaignInteractionId];
3492
- expect(task).toBeDefined();
3493
-
3494
- const taskEmitSpy = jest.spyOn(task, 'emit');
3495
-
3496
- const failPayload = {
3497
- data: {
3498
- type: CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED,
3499
- interactionId: campaignInteractionId,
3500
- campaignId: 'campaign-789',
3501
- reason: 'INTERNAL_ERROR',
3502
- },
3503
- };
3504
-
3505
- webSocketManagerMock.emit('message', JSON.stringify(failPayload));
3506
-
3507
- expect(taskEmitSpy).toHaveBeenCalledWith(
3508
- TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_SKIP_FAILED,
3509
- expect.objectContaining({
3510
- data: expect.objectContaining({interactionId: campaignInteractionId}),
3511
- })
3512
- );
3513
- expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
3514
-
3515
- // Failure payload (reason) should be merged into task.data
3516
- expect(task.data.reason).toBe('INTERNAL_ERROR');
3517
-
3518
- // Original reservation data must be preserved
3519
- expect(task.data.interaction).toBeDefined();
3520
- expect(task.data.interaction.callProcessingDetails.campaignId).toBe('campaign-789');
3521
- });
3522
-
3523
- it('should emit TASK_CAMPAIGN_PREVIEW_REMOVE_FAILED when CampaignPreviewRemoveFailed is received', () => {
3524
- const campaignInteractionId = 'campaign-interaction-123';
3525
-
3526
- const reservationPayload = {
3527
- data: {
3528
- type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION,
3529
- interactionId: campaignInteractionId,
3530
- agentId: taskDataMock.agentId,
3531
- orgId: taskDataMock.orgId,
3532
- trackingId: 'campaign-tracking-456',
3533
- interaction: {
3534
- mediaType: 'telephony',
3535
- callProcessingDetails: {
3536
- campaignId: 'campaign-789',
3537
- },
3538
- },
3539
- },
3540
- };
3541
-
3542
- webSocketManagerMock.emit('message', JSON.stringify(reservationPayload));
3543
-
3544
- const task = taskManager['taskCollection'][campaignInteractionId];
3545
- expect(task).toBeDefined();
3546
-
3547
- const taskEmitSpy = jest.spyOn(task, 'emit');
3548
-
3549
- const failPayload = {
3550
- data: {
3551
- type: CC_EVENTS.CAMPAIGN_PREVIEW_REMOVE_FAILED,
3552
- interactionId: campaignInteractionId,
3553
- campaignId: 'campaign-789',
3554
- reason: 'INTERNAL_ERROR',
3555
- },
3556
- };
3557
-
3558
- webSocketManagerMock.emit('message', JSON.stringify(failPayload));
3559
-
3560
- expect(taskEmitSpy).toHaveBeenCalledWith(
3561
- TASK_EVENTS.TASK_CAMPAIGN_PREVIEW_REMOVE_FAILED,
3562
- expect.objectContaining({
3563
- data: expect.objectContaining({interactionId: campaignInteractionId}),
3564
- })
3565
- );
3566
- expect(taskManager['taskCollection'][campaignInteractionId]).toBeDefined();
3567
-
3568
- // Failure payload (reason) should be merged into task.data
3569
- expect(task.data.reason).toBe('INTERNAL_ERROR');
3570
-
3571
- // Original reservation data must be preserved
3572
- expect(task.data.interaction).toBeDefined();
3573
- expect(task.data.interaction.callProcessingDetails.campaignId).toBe('campaign-789');
3574
- });
3575
-
3576
- it('should not emit campaign preview failure events when task does not exist', () => {
3577
- const nonExistentId = 'non-existent-interaction';
3578
-
3579
- const failPayload = {
3580
- data: {
3581
- type: CC_EVENTS.CAMPAIGN_PREVIEW_SKIP_FAILED,
3582
- interactionId: nonExistentId,
3583
- campaignId: 'campaign-789',
3584
- reason: 'INTERNAL_ERROR',
3585
- },
3586
- };
3587
-
3588
- // Should not throw when task is not found
3589
- expect(() => {
3590
- webSocketManagerMock.emit('message', JSON.stringify(failPayload));
3591
- }).not.toThrow();
3592
2477
 
3593
- expect(taskManager['taskCollection'][nonExistentId]).toBeUndefined();
2478
+ sendStateMachineEventSpy.mockRestore();
3594
2479
  });
3595
2480
  });
3596
2481
  });