@webex/contact-center 3.12.0-next.9 → 3.12.0-task-refactor.2

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