@webex/contact-center 3.12.0-next.8 → 3.12.0-next.80

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 (205) 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 +265 -29
  24. package/dist/cc.js.map +1 -1
  25. package/dist/config.js +6 -0
  26. package/dist/config.js.map +1 -1
  27. package/dist/constants.js +16 -1
  28. package/dist/constants.js.map +1 -1
  29. package/dist/index.js +20 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/metrics/behavioral-events.js +101 -0
  32. package/dist/metrics/behavioral-events.js.map +1 -1
  33. package/dist/metrics/constants.js +23 -4
  34. package/dist/metrics/constants.js.map +1 -1
  35. package/dist/services/ApiAiAssistant.js +74 -3
  36. package/dist/services/ApiAiAssistant.js.map +1 -1
  37. package/dist/services/UserPreference.js +427 -0
  38. package/dist/services/UserPreference.js.map +1 -0
  39. package/dist/services/config/Util.js +3 -3
  40. package/dist/services/config/Util.js.map +1 -1
  41. package/dist/services/config/constants.js +23 -2
  42. package/dist/services/config/constants.js.map +1 -1
  43. package/dist/services/config/types.js +49 -9
  44. package/dist/services/config/types.js.map +1 -1
  45. package/dist/services/core/Err.js.map +1 -1
  46. package/dist/services/core/Utils.js +107 -32
  47. package/dist/services/core/Utils.js.map +1 -1
  48. package/dist/services/core/websocket/WebSocketManager.js +2 -1
  49. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  50. package/dist/services/core/websocket/types.js.map +1 -1
  51. package/dist/services/index.js +1 -1
  52. package/dist/services/index.js.map +1 -1
  53. package/dist/services/task/Task.js +688 -0
  54. package/dist/services/task/Task.js.map +1 -0
  55. package/dist/services/task/TaskFactory.js +45 -0
  56. package/dist/services/task/TaskFactory.js.map +1 -0
  57. package/dist/services/task/TaskManager.js +725 -526
  58. package/dist/services/task/TaskManager.js.map +1 -1
  59. package/dist/services/task/TaskUtils.js +162 -26
  60. package/dist/services/task/TaskUtils.js.map +1 -1
  61. package/dist/services/task/constants.js +9 -2
  62. package/dist/services/task/constants.js.map +1 -1
  63. package/dist/services/task/dialer.js +78 -0
  64. package/dist/services/task/dialer.js.map +1 -1
  65. package/dist/services/task/digital/Digital.js +77 -0
  66. package/dist/services/task/digital/Digital.js.map +1 -0
  67. package/dist/services/task/state-machine/TaskStateMachine.js +837 -0
  68. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  69. package/dist/services/task/state-machine/actions.js +543 -0
  70. package/dist/services/task/state-machine/actions.js.map +1 -0
  71. package/dist/services/task/state-machine/constants.js +161 -0
  72. package/dist/services/task/state-machine/constants.js.map +1 -0
  73. package/dist/services/task/state-machine/guards.js +340 -0
  74. package/dist/services/task/state-machine/guards.js.map +1 -0
  75. package/dist/services/task/state-machine/index.js +53 -0
  76. package/dist/services/task/state-machine/index.js.map +1 -0
  77. package/dist/services/task/state-machine/types.js +54 -0
  78. package/dist/services/task/state-machine/types.js.map +1 -0
  79. package/dist/services/task/state-machine/uiControlsComputer.js +553 -0
  80. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  81. package/dist/services/task/taskDataNormalizer.js +99 -0
  82. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  83. package/dist/services/task/types.js +212 -4
  84. package/dist/services/task/types.js.map +1 -1
  85. package/dist/services/task/voice/Voice.js +1042 -0
  86. package/dist/services/task/voice/Voice.js.map +1 -0
  87. package/dist/services/task/voice/WebRTC.js +149 -0
  88. package/dist/services/task/voice/WebRTC.js.map +1 -0
  89. package/dist/types/cc.d.ts +94 -1
  90. package/dist/types/config.d.ts +6 -0
  91. package/dist/types/constants.d.ts +16 -1
  92. package/dist/types/index.d.ts +21 -6
  93. package/dist/types/metrics/constants.d.ts +19 -1
  94. package/dist/types/services/ApiAiAssistant.d.ts +11 -3
  95. package/dist/types/services/UserPreference.d.ts +118 -0
  96. package/dist/types/services/config/constants.d.ts +21 -0
  97. package/dist/types/services/config/types.d.ts +171 -10
  98. package/dist/types/services/core/Err.d.ts +4 -0
  99. package/dist/types/services/core/Utils.d.ts +33 -13
  100. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
  101. package/dist/types/services/core/websocket/types.d.ts +1 -1
  102. package/dist/types/services/index.d.ts +1 -1
  103. package/dist/types/services/task/Task.d.ts +157 -0
  104. package/dist/types/services/task/TaskFactory.d.ts +12 -0
  105. package/dist/types/services/task/TaskUtils.d.ts +46 -2
  106. package/dist/types/services/task/constants.d.ts +7 -0
  107. package/dist/types/services/task/dialer.d.ts +30 -0
  108. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  109. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1144 -0
  110. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  111. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  112. package/dist/types/services/task/state-machine/guards.d.ts +90 -0
  113. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  114. package/dist/types/services/task/state-machine/types.d.ts +267 -0
  115. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  116. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  117. package/dist/types/services/task/types.d.ts +603 -66
  118. package/dist/types/services/task/voice/Voice.d.ts +184 -0
  119. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  120. package/dist/types/types.d.ts +94 -0
  121. package/dist/types/webex.d.ts +1 -0
  122. package/dist/types.js +85 -0
  123. package/dist/types.js.map +1 -1
  124. package/dist/webex.js +14 -2
  125. package/dist/webex.js.map +1 -1
  126. package/package.json +15 -12
  127. package/src/cc.ts +329 -30
  128. package/src/config.ts +6 -0
  129. package/src/constants.ts +16 -1
  130. package/src/index.ts +23 -5
  131. package/src/metrics/ai-docs/AGENTS.md +348 -0
  132. package/src/metrics/ai-docs/ARCHITECTURE.md +336 -0
  133. package/src/metrics/behavioral-events.ts +106 -0
  134. package/src/metrics/constants.ts +23 -4
  135. package/src/services/ApiAiAssistant.ts +104 -3
  136. package/src/services/UserPreference.ts +509 -0
  137. package/src/services/agent/ai-docs/AGENTS.md +238 -0
  138. package/src/services/agent/ai-docs/ARCHITECTURE.md +302 -0
  139. package/src/services/ai-docs/AGENTS.md +384 -0
  140. package/src/services/config/Util.ts +3 -3
  141. package/src/services/config/ai-docs/AGENTS.md +253 -0
  142. package/src/services/config/ai-docs/ARCHITECTURE.md +424 -0
  143. package/src/services/config/constants.ts +25 -1
  144. package/src/services/config/types.ts +174 -11
  145. package/src/services/core/Err.ts +2 -0
  146. package/src/services/core/Utils.ts +123 -37
  147. package/src/services/core/ai-docs/AGENTS.md +379 -0
  148. package/src/services/core/ai-docs/ARCHITECTURE.md +696 -0
  149. package/src/services/core/websocket/WebSocketManager.ts +2 -0
  150. package/src/services/core/websocket/types.ts +1 -1
  151. package/src/services/index.ts +1 -1
  152. package/src/services/task/Task.ts +837 -0
  153. package/src/services/task/TaskFactory.ts +55 -0
  154. package/src/services/task/TaskManager.ts +738 -613
  155. package/src/services/task/TaskUtils.ts +205 -25
  156. package/src/services/task/ai-docs/AGENTS.md +455 -0
  157. package/src/services/task/ai-docs/ARCHITECTURE.md +585 -0
  158. package/src/services/task/constants.ts +7 -0
  159. package/src/services/task/dialer.ts +80 -0
  160. package/src/services/task/digital/Digital.ts +95 -0
  161. package/src/services/task/state-machine/TaskStateMachine.ts +1077 -0
  162. package/src/services/task/state-machine/actions.ts +685 -0
  163. package/src/services/task/state-machine/ai-docs/AGENTS.md +495 -0
  164. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1135 -0
  165. package/src/services/task/state-machine/constants.ts +172 -0
  166. package/src/services/task/state-machine/guards.ts +406 -0
  167. package/src/services/task/state-machine/index.ts +28 -0
  168. package/src/services/task/state-machine/types.ts +241 -0
  169. package/src/services/task/state-machine/uiControlsComputer.ts +867 -0
  170. package/src/services/task/taskDataNormalizer.ts +137 -0
  171. package/src/services/task/types.ts +710 -71
  172. package/src/services/task/voice/Voice.ts +1267 -0
  173. package/src/services/task/voice/WebRTC.ts +187 -0
  174. package/src/types.ts +122 -2
  175. package/src/utils/AGENTS.md +276 -0
  176. package/src/webex.js +2 -0
  177. package/test/unit/spec/cc.ts +343 -23
  178. package/test/unit/spec/logger-proxy.ts +70 -0
  179. package/test/unit/spec/services/ApiAiAssistant.ts +122 -17
  180. package/test/unit/spec/services/UserPreference.ts +401 -0
  181. package/test/unit/spec/services/WebCallingService.ts +7 -1
  182. package/test/unit/spec/services/config/index.ts +30 -30
  183. package/test/unit/spec/services/core/Utils.ts +425 -8
  184. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +66 -40
  185. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  186. package/test/unit/spec/services/task/Task.ts +477 -0
  187. package/test/unit/spec/services/task/TaskFactory.ts +62 -0
  188. package/test/unit/spec/services/task/TaskManager.ts +832 -1702
  189. package/test/unit/spec/services/task/TaskUtils.ts +206 -0
  190. package/test/unit/spec/services/task/dialer.ts +190 -0
  191. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  192. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +1825 -0
  193. package/test/unit/spec/services/task/state-machine/guards.ts +479 -0
  194. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  195. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +2020 -0
  196. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  197. package/test/unit/spec/services/task/voice/Voice.ts +631 -0
  198. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  199. package/umd/contact-center.min.js +2 -2
  200. package/umd/contact-center.min.js.map +1 -1
  201. package/dist/services/task/index.js +0 -1525
  202. package/dist/services/task/index.js.map +0 -1
  203. package/dist/types/services/task/index.d.ts +0 -650
  204. package/src/services/task/index.ts +0 -1801
  205. 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,108 @@ describe('TaskManager', () => {
24
29
  let webex: WebexSDK;
25
30
  const taskId = '0ae913a4-c857-4705-8d49-76dd3dde75e4';
26
31
 
32
+ const createMockTask = (data = taskDataMock) => {
33
+ const task = new EventEmitter() as any;
34
+
35
+ const updateTaskData = jest.fn().mockImplementation((newData) => {
36
+ task.data = {...task.data, ...newData};
37
+ return task;
38
+ });
39
+
40
+ Object.assign(task, {
41
+ data,
42
+ accept: jest.fn(),
43
+ decline: jest.fn(),
44
+ updateTaskData,
45
+ unregisterWebCallListeners: jest.fn(),
46
+ cancelAutoWrapupTimer: jest.fn(),
47
+ });
48
+
49
+ const taskEventMap: Partial<Record<TaskEvent, string>> = {
50
+ [TaskEvent.TASK_INCOMING]: TASK_EVENTS.TASK_INCOMING,
51
+ [TaskEvent.TASK_OFFERED]: TASK_EVENTS.TASK_OFFER_CONTACT,
52
+ [TaskEvent.OFFER_CONSULT]: TASK_EVENTS.TASK_OFFER_CONSULT,
53
+ [TaskEvent.HYDRATE]: TASK_EVENTS.TASK_HYDRATE,
54
+ [TaskEvent.ASSIGN]: TASK_EVENTS.TASK_ASSIGNED,
55
+ [TaskEvent.HOLD_SUCCESS]: TASK_EVENTS.TASK_HOLD,
56
+ [TaskEvent.UNHOLD_SUCCESS]: TASK_EVENTS.TASK_RESUME,
57
+ [TaskEvent.CONSULT_CREATED]: TASK_EVENTS.TASK_CONSULT_CREATED,
58
+ [TaskEvent.CONSULTING_ACTIVE]: TASK_EVENTS.TASK_CONSULT_ACCEPTED,
59
+ [TaskEvent.CONSULT_END]: TASK_EVENTS.TASK_CONSULT_END,
60
+ [TaskEvent.CONSULT_FAILED]: CC_EVENTS.AGENT_CONSULT_FAILED,
61
+ [TaskEvent.CTQ_CANCEL]: TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED,
62
+ [TaskEvent.CTQ_CANCEL_FAILED]: TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED,
63
+ [TaskEvent.END]: TASK_EVENTS.TASK_END,
64
+ [TaskEvent.CONTACT_ENDED]: TASK_EVENTS.TASK_END,
65
+ [TaskEvent.ASSIGN_FAILED]: TASK_EVENTS.TASK_REJECT,
66
+ [TaskEvent.INVITE_FAILED]: TASK_EVENTS.TASK_REJECT,
67
+ [TaskEvent.RONA]: TASK_EVENTS.TASK_REJECT,
68
+ [TaskEvent.OUTBOUND_FAILED]: TASK_EVENTS.TASK_OUTDIAL_FAILED,
69
+ [TaskEvent.RECORDING_STARTED]: TASK_EVENTS.TASK_RECORDING_STARTED,
70
+ [TaskEvent.PAUSE_RECORDING]: TASK_EVENTS.TASK_RECORDING_PAUSED,
71
+ [TaskEvent.RESUME_RECORDING]: TASK_EVENTS.TASK_RECORDING_RESUMED,
72
+ [TaskEvent.WRAPUP_COMPLETE]: TASK_EVENTS.TASK_WRAPPEDUP,
73
+ };
74
+
75
+ task.sendStateMachineEvent = jest.fn().mockImplementation((event) => {
76
+ if (event.taskData) {
77
+ task.updateTaskData(event.taskData);
78
+ }
79
+
80
+ const mappedEvent = taskEventMap[event.type as TaskEvent];
81
+ if (mappedEvent) {
82
+ if (
83
+ [TaskEvent.ASSIGN_FAILED, TaskEvent.RONA, TaskEvent.INVITE_FAILED].includes(
84
+ event.type as TaskEvent
85
+ )
86
+ ) {
87
+ task.emit(mappedEvent, event.reason ?? event.taskData?.reason);
88
+ } else if (event.type === TaskEvent.OUTBOUND_FAILED) {
89
+ task.emit(mappedEvent, event.reason);
90
+ } else {
91
+ task.emit(mappedEvent, task);
92
+ }
93
+ }
94
+
95
+ if ([TaskEvent.ASSIGN, TaskEvent.CONSULTING_ACTIVE].includes(event.type as TaskEvent)) {
96
+ task.emit(TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE, task);
97
+ }
98
+
99
+ // Auto-answer is now handled at the Task layer (triggered by state machine actions)
100
+ if (
101
+ [TaskEvent.TASK_OFFERED, TaskEvent.OFFER_CONSULT].includes(event.type as TaskEvent) &&
102
+ (event.taskData?.isAutoAnswering === true || event.taskData?.isAutoAnswering === 'true')
103
+ ) {
104
+ Promise.resolve(task.accept())
105
+ .then(() => {
106
+ task.emit(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
107
+ })
108
+ .catch(() => undefined);
109
+ }
110
+
111
+ // Cleanup is now emitted by state machine actions (Task layer).
112
+ // Simulate the TASK_CLEANUP emission for unit tests using mock tasks.
113
+ const eventType = event.type as TaskEvent;
114
+ const shouldCleanup =
115
+ eventType === TaskEvent.CONTACT_ENDED ||
116
+ eventType === TaskEvent.END ||
117
+ eventType === TaskEvent.TASK_WRAPUP ||
118
+ eventType === TaskEvent.WRAPUP_COMPLETE ||
119
+ eventType === TaskEvent.ASSIGN_FAILED ||
120
+ eventType === TaskEvent.INVITE_FAILED ||
121
+ eventType === TaskEvent.RONA ||
122
+ eventType === TaskEvent.OUTBOUND_FAILED ||
123
+ (eventType === TaskEvent.CONSULT_END && task.data?.isConsulted === true);
124
+
125
+ if (shouldCleanup) {
126
+ const removeFromCollection = eventType !== TaskEvent.CONTACT_ENDED;
127
+ task.emit(TASK_EVENTS.TASK_CLEANUP, task, {removeFromCollection});
128
+ }
129
+ });
130
+
131
+ return task;
132
+ };
133
+
27
134
  taskDataMock = {
28
135
  type: CC_EVENTS.AGENT_CONTACT_RESERVED,
29
136
  agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
@@ -43,18 +150,21 @@ describe('TaskManager', () => {
43
150
  data: taskDataMock,
44
151
  };
45
152
 
153
+ const expectLastStateMachineEvent = (
154
+ spy: jest.SpyInstance | jest.Mock,
155
+ expectedType: TaskEvent
156
+ ) => {
157
+ expect(spy).toHaveBeenCalled();
158
+ const lastCall = spy.mock.calls[spy.mock.calls.length - 1] || [];
159
+ const event = lastCall[3]?.type ? lastCall[3] : lastCall[0];
160
+ expect(event?.type).toBe(expectedType);
161
+ return event;
162
+ };
163
+
46
164
  beforeEach(() => {
47
165
  contactMock = contact;
48
166
  webSocketManagerMock = new EventEmitter();
49
- mockApiAIAssistant = {
50
- sendEvent: jest.fn().mockResolvedValue({}),
51
- setAIFeatureFlags: jest.fn(),
52
- aiFeature: {
53
- realtimeTranscripts: {
54
- enable: true,
55
- },
56
- },
57
- };
167
+ rtdWebSocketManagerMock = new EventEmitter();
58
168
 
59
169
  webex = {
60
170
  logger: {
@@ -84,20 +194,25 @@ describe('TaskManager', () => {
84
194
  onSpy = jest.spyOn(webCallingService, 'on');
85
195
  offSpy = jest.spyOn(webCallingService, 'off');
86
196
 
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,
197
+ mockApiAIAssistant = {
198
+ sendEvent: jest.fn().mockResolvedValue({}),
97
199
  };
98
- taskManager.taskCollection[taskId] = taskMock;
99
- taskManager.agentId = 'test-agent-id';
200
+
201
+ taskManager = new TaskManager(
202
+ mockApiAIAssistant as any,
203
+ contactMock,
204
+ webCallingService,
205
+ webSocketManagerMock as any,
206
+ rtdWebSocketManagerMock as any
207
+ );
208
+ taskManager.taskCollection[taskId] = createMockTask(taskDataMock);
209
+ (taskManager as any).setupTaskListeners?.(taskManager.taskCollection[taskId]);
100
210
  taskManager.call = mockCall;
211
+ taskManager.setAgentId('test-agent-id');
212
+
213
+ jest
214
+ .spyOn(TaskFactory, 'createTask')
215
+ .mockImplementation((contact, webCallingService, data, configFlags) => createMockTask(data));
101
216
  });
102
217
 
103
218
  afterEach(() => {
@@ -108,7 +223,8 @@ describe('TaskManager', () => {
108
223
  it('should initialize TaskManager and register listeners', () => {
109
224
  webSocketManagerMock.emit('message', JSON.stringify({data: taskDataMock}));
110
225
  const incomingCallCb = onSpy.mock.calls[0][1];
111
- const taskEmitSpy = jest.spyOn(taskManager, 'emit');
226
+ const incomingHandler = jest.fn();
227
+ taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
112
228
 
113
229
  expect(taskManager).toBeInstanceOf(TaskManager);
114
230
  expect(webCallingService.listenerCount(LINE_EVENTS.INCOMING_CALL)).toBe(1);
@@ -117,10 +233,8 @@ describe('TaskManager', () => {
117
233
 
118
234
  incomingCallCb(mockCall);
119
235
 
120
- expect(taskEmitSpy).toHaveBeenCalledWith(
121
- TASK_EVENTS.TASK_INCOMING,
122
- taskManager.getTask(taskId)
123
- );
236
+ expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(taskId));
237
+ taskManager.off(TASK_EVENTS.TASK_INCOMING, incomingHandler);
124
238
  });
125
239
 
126
240
  it('should re-emit task related events', () => {
@@ -136,15 +250,32 @@ describe('TaskManager', () => {
136
250
 
137
251
  webSocketManagerMock.emit('message', JSON.stringify(dummyPayload));
138
252
 
139
- expect(taskEmitSpy).toHaveBeenCalledWith(dummyPayload.data.type, dummyPayload.data);
253
+ expect(taskEmitSpy).toHaveBeenCalledWith(
254
+ TASK_EVENTS.TASK_CONSULT_ACCEPTED,
255
+ taskManager.getTask(taskId)
256
+ );
140
257
  });
141
258
 
142
259
  it('should invoke sendEvent for configured start/stop backend events', () => {
260
+ taskManager.setConfigFlags({
261
+ isEndTaskEnabled: true,
262
+ isEndConsultEnabled: true,
263
+ webRtcEnabled: true,
264
+ autoWrapup: false,
265
+ aiFeature: {
266
+ id: 'ai-feature-1',
267
+ realtimeTranscripts: {
268
+ enable: true,
269
+ },
270
+ },
271
+ });
272
+
273
+ const interactionId = taskId;
143
274
  const message = (type: CC_EVENTS) =>
144
275
  JSON.stringify({
145
276
  data: {
146
277
  ...taskDataMock,
147
- taskId,
278
+ interactionId,
148
279
  type,
149
280
  },
150
281
  });
@@ -153,67 +284,114 @@ describe('TaskManager', () => {
153
284
  webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
154
285
  webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_CONFERENCED));
155
286
  webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_ENDED));
156
- webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
157
287
  webSocketManagerMock.emit('message', message(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE));
288
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
158
289
 
159
290
  expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledTimes(6);
160
291
  expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledWith(
161
292
  'test-agent-id',
162
- taskId,
293
+ interactionId,
163
294
  'CUSTOM_EVENT',
164
295
  'GET_TRANSCRIPTS',
165
296
  'START'
166
297
  );
167
298
  expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledWith(
168
299
  'test-agent-id',
169
- taskId,
300
+ interactionId,
170
301
  'CUSTOM_EVENT',
171
302
  'GET_TRANSCRIPTS',
172
303
  'STOP'
173
304
  );
174
305
  });
175
306
 
176
- it('should not invoke sendEvent for transcript events when realtime transcript feature is disabled', () => {
177
- mockApiAIAssistant.aiFeature = {
178
- realtimeTranscripts: {
179
- enable: false,
307
+ it('should not invoke sendEvent when realtime transcripts are disabled in aiFeature', () => {
308
+ taskManager.setConfigFlags({
309
+ isEndTaskEnabled: true,
310
+ isEndConsultEnabled: true,
311
+ webRtcEnabled: true,
312
+ autoWrapup: false,
313
+ aiFeature: {
314
+ id: 'ai-feature-1',
315
+ realtimeTranscripts: {
316
+ enable: false,
317
+ },
180
318
  },
181
- };
182
- mockApiAIAssistant.setAIFeatureFlags(mockApiAIAssistant.aiFeature);
319
+ });
183
320
 
184
321
  const message = (type: CC_EVENTS) =>
185
322
  JSON.stringify({
186
323
  data: {
187
324
  ...taskDataMock,
188
- taskId,
325
+ interactionId: taskId,
326
+ type,
327
+ },
328
+ });
329
+
330
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONTACT_ASSIGNED));
331
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
332
+
333
+ expect(mockApiAIAssistant.sendEvent).not.toHaveBeenCalled();
334
+ });
335
+
336
+ it('should not invoke sendEvent when realtime transcripts config is missing', () => {
337
+ taskManager.setConfigFlags({
338
+ isEndTaskEnabled: true,
339
+ isEndConsultEnabled: true,
340
+ webRtcEnabled: true,
341
+ autoWrapup: false,
342
+ aiFeature: {
343
+ id: 'ai-feature-1',
344
+ suggestedResponses: {
345
+ enable: true,
346
+ },
347
+ },
348
+ });
349
+
350
+ const message = (type: CC_EVENTS) =>
351
+ JSON.stringify({
352
+ data: {
353
+ ...taskDataMock,
354
+ interactionId: taskId,
189
355
  type,
190
356
  },
191
357
  });
192
358
 
193
359
  webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONTACT_ASSIGNED));
194
360
  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
361
 
200
362
  expect(mockApiAIAssistant.sendEvent).not.toHaveBeenCalled();
201
363
  });
202
364
 
203
- it('should emit REAL_TIME_TRANSCRIPTION from task object', () => {
365
+ it('should emit REAL_TIME_TRANSCRIPTION from RTD websocket payload', () => {
204
366
  const task = taskManager.getTask(taskId);
205
367
  const taskEmitSpy = jest.spyOn(task, 'emit');
206
368
  const realtimePayload = {
207
369
  data: {
208
- ...taskDataMock,
209
- type: CC_EVENTS.REAL_TIME_TRANSCRIPTION,
370
+ agentId: 'test-agent-id',
210
371
  data: {
211
- content: 'hello from transcript',
372
+ content: 'Thank you. Okay.',
373
+ conversationId: taskId,
374
+ isFinal: true,
375
+ languageCode: 'en-US',
376
+ messageId: '1',
377
+ orgId: 'org-id',
378
+ publishTimestamp: 1773807297475,
379
+ role: 'AGENT',
380
+ trackingId: 'tracking-id',
381
+ utteranceId: 'utterance-id',
212
382
  },
383
+ notifDetails: {
384
+ actionEvent: 'REAL_TIME_TRANSCRIPTION',
385
+ },
386
+ notifType: 'REAL_TIME_TRANSCRIPTION',
387
+ orgId: 'org-id',
213
388
  },
389
+ orgId: 'org-id',
390
+ trackingId: 'notifs_tracking-id',
391
+ type: 'REAL_TIME_TRANSCRIPTION',
214
392
  };
215
393
 
216
- webSocketManagerMock.emit('message', JSON.stringify(realtimePayload));
394
+ taskManager.handleRealtimeWebsocketEvent(JSON.stringify(realtimePayload));
217
395
 
218
396
  expect(taskEmitSpy).toHaveBeenCalledWith(
219
397
  CC_EVENTS.REAL_TIME_TRANSCRIPTION,
@@ -221,22 +399,37 @@ describe('TaskManager', () => {
221
399
  );
222
400
  });
223
401
 
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');
402
+ it('should ignore RTD transcript events when task is not found', () => {
227
403
  const realtimePayload = {
228
404
  data: {
229
- notifType: CC_EVENTS.REAL_TIME_TRANSCRIPTION,
230
405
  data: {
231
- conversationId: taskId,
232
- content: 'hello from rtd websocket',
406
+ content: 'Thank you. Okay.',
407
+ conversationId: 'missing-task-id',
408
+ isFinal: true,
409
+ languageCode: 'en-US',
410
+ messageId: '1',
411
+ orgId: 'org-id',
412
+ publishTimestamp: 1773807297475,
413
+ role: 'AGENT',
414
+ trackingId: 'tracking-id',
415
+ utteranceId: 'utterance-id',
416
+ },
417
+ notifDetails: {
418
+ actionEvent: 'REAL_TIME_TRANSCRIPTION',
233
419
  },
420
+ notifType: 'REAL_TIME_TRANSCRIPTION',
421
+ orgId: 'org-id',
234
422
  },
423
+ orgId: 'org-id',
424
+ trackingId: 'notifs_tracking-id',
425
+ type: 'REAL_TIME_TRANSCRIPTION',
235
426
  };
236
427
 
428
+ const existingTaskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
429
+
237
430
  taskManager.handleRealtimeWebsocketEvent(JSON.stringify(realtimePayload));
238
431
 
239
- expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.REAL_TIME_TRANSCRIPTION, realtimePayload.data);
432
+ expect(existingTaskEmitSpy).not.toHaveBeenCalled();
240
433
  });
241
434
 
242
435
  it('should not re-emit agent related events', () => {
@@ -254,7 +447,7 @@ describe('TaskManager', () => {
254
447
 
255
448
  webSocketManagerMock.emit('message', JSON.stringify(dummyPayload));
256
449
 
257
- expect(taskEmitSpy).not.toHaveBeenCalledWith(dummyPayload.data.type, dummyPayload.data);
450
+ expect(taskEmitSpy).not.toHaveBeenCalled();
258
451
  });
259
452
 
260
453
  it('should handle WebSocket message for AGENT_CONTACT_RESERVED and emit task:incoming for browser case', () => {
@@ -275,14 +468,12 @@ describe('TaskManager', () => {
275
468
  },
276
469
  };
277
470
 
278
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
471
+ const incomingHandler = jest.fn();
472
+ taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
279
473
 
280
474
  webSocketManagerMock.emit('message', JSON.stringify(payload));
281
475
 
282
- expect(taskIncomingSpy).toHaveBeenCalledWith(
283
- TASK_EVENTS.TASK_INCOMING,
284
- taskManager.getTask(payload.data.interactionId)
285
- );
476
+ expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(payload.data.interactionId));
286
477
  expect(taskManager.getTask(payload.data.interactionId)).toBe(taskManager.getTask(taskId));
287
478
  expect(taskManager.getAllTasks()).toHaveProperty(payload.data.interactionId);
288
479
 
@@ -307,6 +498,7 @@ describe('TaskManager', () => {
307
498
  taskManager.getTask(payload.data.interactionId),
308
499
  'emit'
309
500
  );
501
+ const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
310
502
 
311
503
  webSocketManagerMock.emit('message', JSON.stringify(assignedPayload));
312
504
 
@@ -314,6 +506,10 @@ describe('TaskManager', () => {
314
506
  TASK_EVENTS.TASK_ASSIGNED,
315
507
  taskManager.getTask(taskId)
316
508
  );
509
+ expect(taskManagerEmitSpy).toHaveBeenCalledWith(
510
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
511
+ taskManager.getTask(taskId)
512
+ );
317
513
  });
318
514
 
319
515
  it('should handle WebSocket message for AGENT_CONTACT_RESERVED and emit task:incoming for extension case', () => {
@@ -335,16 +531,64 @@ describe('TaskManager', () => {
335
531
  },
336
532
  };
337
533
 
338
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
534
+ const incomingHandler = jest.fn();
535
+ taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
339
536
 
340
537
  webSocketManagerMock.emit('message', JSON.stringify(payload));
341
538
 
342
- expect(taskIncomingSpy).toHaveBeenCalledWith(
343
- TASK_EVENTS.TASK_INCOMING,
344
- taskManager.getTask(taskId)
345
- );
539
+ expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(taskId));
346
540
  expect(taskManager.getTask(payload.data.interactionId)).toBe(taskManager.getTask(taskId));
347
541
  expect(taskManager.getAllTasks()).toHaveProperty(payload.data.interactionId);
542
+ taskManager.off(TASK_EVENTS.TASK_INCOMING, incomingHandler);
543
+ });
544
+
545
+ it('should send mapped events through the state machine without duplicate updates', () => {
546
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
547
+ const task = taskManager.getTask(taskId);
548
+ const updateSpy = task.updateTaskData as jest.Mock;
549
+ updateSpy.mockClear();
550
+ const sendSpy = task.sendStateMachineEvent as jest.Mock;
551
+ sendSpy.mockClear();
552
+ const cleanupSpy = jest.spyOn(taskManager as any, 'handleTaskCleanup');
553
+
554
+ const assignFailedPayload = {
555
+ data: {
556
+ ...initalPayload.data,
557
+ type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
558
+ reason: 'ASSIGN_FAILED',
559
+ },
560
+ };
561
+
562
+ webSocketManagerMock.emit('message', JSON.stringify(assignFailedPayload));
563
+
564
+ const stateMachineEvent = expectLastStateMachineEvent(sendSpy, TaskEvent.ASSIGN_FAILED);
565
+ expect(stateMachineEvent).toEqual({
566
+ type: TaskEvent.ASSIGN_FAILED,
567
+ reason: assignFailedPayload.data.reason,
568
+ });
569
+ expect(updateSpy).toHaveBeenCalledWith(assignFailedPayload.data);
570
+ expect(cleanupSpy).toHaveBeenCalledWith(task);
571
+ });
572
+
573
+ it('should update task data directly when no state machine mapping exists', () => {
574
+ webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
575
+ const task = taskManager.getTask(taskId);
576
+ const updateSpy = task.updateTaskData as jest.Mock;
577
+ updateSpy.mockClear();
578
+ const sendSpy = task.sendStateMachineEvent as jest.Mock;
579
+ sendSpy.mockClear();
580
+
581
+ const participantMovedPayload = {
582
+ data: {
583
+ ...initalPayload.data,
584
+ type: CC_EVENTS.CONSULTED_PARTICIPANT_MOVING,
585
+ },
586
+ };
587
+
588
+ webSocketManagerMock.emit('message', JSON.stringify(participantMovedPayload));
589
+
590
+ expect(sendSpy).not.toHaveBeenCalled();
591
+ expect(updateSpy).toHaveBeenCalledWith(participantMovedPayload.data);
348
592
  });
349
593
 
350
594
  it('should return task by ID', () => {
@@ -429,12 +673,30 @@ describe('TaskManager', () => {
429
673
  it('test call listeners being switched off on call end', () => {
430
674
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
431
675
 
432
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
433
- const webCallListenerSpy = jest.spyOn(
434
- taskManager.getTask(taskId),
435
- 'unregisterWebCallListeners'
436
- );
676
+ const webrtcTask = new WebRTC(contactMock, webCallingService, taskDataMock, {
677
+ isEndTaskEnabled: true,
678
+ isEndConsultEnabled: true,
679
+ });
680
+ (taskManager as any).taskCollection[taskId] = webrtcTask;
681
+ // TaskManager must listen to task-level cleanup events emitted by the state machine.
682
+ // This is normally wired when TaskManager creates the task via TaskFactory.
683
+ (taskManager as any).setupTaskListeners(webrtcTask);
684
+
685
+ const task = taskManager.getTask(taskId)!;
686
+ // This test doesn't validate UI controls; avoid requiring full interaction.media
687
+ // shape for WebRTC UI controls computation.
688
+ jest.spyOn(task as any, 'updateUiControls').mockImplementation(() => undefined);
689
+ const originalEmit = task.emit;
690
+ jest.spyOn(task, 'emit').mockImplementation((event, arg) => {
691
+ if (event === CC_EVENTS.CONTACT_ENDED) {
692
+ return;
693
+ }
694
+ return originalEmit.call(task, event, arg);
695
+ });
696
+
697
+ const webCallListenerSpy = jest.spyOn(task, 'unregisterWebCallListeners');
437
698
  const callOffSpy = jest.spyOn(mockCall, 'off');
699
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
438
700
  const payload = {
439
701
  data: {
440
702
  type: CC_EVENTS.CONTACT_ENDED,
@@ -452,10 +714,25 @@ describe('TaskManager', () => {
452
714
  },
453
715
  };
454
716
 
717
+ // Ensure the state machine is hydrated into a connected state before CONTACT_ENDED
718
+ const hydratePayload = {
719
+ data: {
720
+ ...payload.data,
721
+ type: CC_EVENTS.AGENT_CONTACT,
722
+ interaction: {state: 'connected', mediaType: 'telephony'},
723
+ },
724
+ };
725
+
726
+ taskManager.getTask(taskId).data = hydratePayload.data;
727
+ webSocketManagerMock.emit('message', JSON.stringify(hydratePayload));
728
+
455
729
  taskManager.getTask(taskId).data = payload.data;
456
- const task = taskManager.getTask(taskId);
457
730
  webSocketManagerMock.emit('message', JSON.stringify(payload));
458
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
731
+ const stateMachineEvent = expectLastStateMachineEvent(
732
+ sendStateMachineEventSpy,
733
+ TaskEvent.CONTACT_ENDED
734
+ );
735
+ expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(false);
459
736
  expect(webCallListenerSpy).toHaveBeenCalledWith();
460
737
  expect(callOffSpy).toHaveBeenCalledWith(
461
738
  CALL_EVENT_KEYS.REMOTE_MEDIA,
@@ -466,12 +743,13 @@ describe('TaskManager', () => {
466
743
  expect(offSpy.mock.calls.length).toBe(2); // 1 for incoming call and 1 for remote media
467
744
  expect(offSpy).toHaveBeenCalledWith(CALL_EVENT_KEYS.REMOTE_MEDIA, offSpy.mock.calls[0][1]);
468
745
  expect(offSpy).toHaveBeenCalledWith(LINE_EVENTS.INCOMING_CALL, offSpy.mock.calls[1][1]);
746
+ sendStateMachineEventSpy.mockRestore();
469
747
  });
470
748
 
471
749
  it('should emit TASK_END event with wrapupRequired on regular call end', () => {
472
750
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
473
-
474
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
751
+ const task = taskManager.getTask(taskId);
752
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
475
753
  const payload = {
476
754
  data: {
477
755
  type: CC_EVENTS.CONTACT_ENDED,
@@ -486,20 +764,25 @@ describe('TaskManager', () => {
486
764
  destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
487
765
  owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
488
766
  queueMgr: 'aqm',
767
+ agentsPendingWrapUp: ['test-agent-id'],
489
768
  },
490
769
  };
491
770
 
492
- taskManager.getTask(taskId).updateTaskData(payload.data);
771
+ task.updateTaskData(payload.data);
493
772
  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));
773
+ const stateMachineEvent = expectLastStateMachineEvent(
774
+ sendStateMachineEventSpy,
775
+ TaskEvent.CONTACT_ENDED
776
+ );
777
+ expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(true);
778
+ sendStateMachineEventSpy.mockRestore();
496
779
  });
497
780
 
498
781
  it('should emit TASK_REJECT event on AGENT_INVITE_FAILED event', () => {
499
782
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
500
783
 
501
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
502
- const metricsTrackSpy = jest.spyOn(taskManager.metricsManager, 'trackEvent');
784
+ const task = taskManager.getTask(taskId);
785
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
503
786
  const payload = {
504
787
  data: {
505
788
  type: CC_EVENTS.AGENT_INVITE_FAILED,
@@ -518,35 +801,40 @@ describe('TaskManager', () => {
518
801
  },
519
802
  };
520
803
 
521
- taskManager.getTask(taskId).updateTaskData(payload.data);
804
+ task.updateTaskData(payload.data);
522
805
  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');
806
+ const stateMachineEvent = expectLastStateMachineEvent(
807
+ sendStateMachineEventSpy,
808
+ TaskEvent.INVITE_FAILED
809
+ );
810
+ expect(stateMachineEvent?.reason).toBe(payload.data.reason);
811
+ sendStateMachineEventSpy.mockRestore();
528
812
  });
529
813
 
530
- it('should not emit TASK_HYDRATE if task is already present in taskManager', () => {
814
+ it('should emit TASK_HYDRATE even if task is already present in taskManager', () => {
531
815
  const payload = {
532
816
  data: {
533
817
  ...initalPayload.data,
534
818
  type: CC_EVENTS.AGENT_CONTACT,
535
819
  },
536
820
  };
537
- const taskEmitSpy = jest.spyOn(taskManager, 'emit');
821
+ const existingTask = taskManager.getTask(taskId);
822
+ const sendStateMachineEventSpy = jest.spyOn(existingTask, 'sendStateMachineEvent');
538
823
  webSocketManagerMock.emit('message', JSON.stringify(payload));
539
824
 
540
- expect(taskEmitSpy).not.toHaveBeenCalledWith(
541
- TASK_EVENTS.TASK_HYDRATE,
542
- taskManager.getTask(taskId)
825
+ const stateMachineEvent = expectLastStateMachineEvent(
826
+ sendStateMachineEventSpy,
827
+ TaskEvent.HYDRATE
543
828
  );
829
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
830
+ expect(existingTask).toBe(taskManager.getTask(taskId));
544
831
  expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
545
832
  taskManager.getTask(taskId)
546
833
  );
834
+ sendStateMachineEventSpy.mockRestore();
547
835
  });
548
836
 
549
- it('should emit TASK_INCOMING event on AGENT_CONTACT event if task is new and not in the taskManager ', () => {
837
+ it('should emit TASK_HYDRATE event on AGENT_CONTACT when task is created from payload', () => {
550
838
  taskManager.taskCollection = [];
551
839
  const payload = {
552
840
  data: {
@@ -556,13 +844,15 @@ describe('TaskManager', () => {
556
844
  },
557
845
  };
558
846
 
559
- const taskEmitSpy = jest.spyOn(taskManager, 'emit');
560
847
  webSocketManagerMock.emit('message', JSON.stringify(payload));
561
848
 
562
- expect(taskEmitSpy).toHaveBeenCalledWith(
563
- TASK_EVENTS.TASK_INCOMING,
564
- taskManager.getTask(taskId)
849
+ const createdTask = taskManager.getTask(taskId);
850
+ const sendStateMachineEventSpy = createdTask.sendStateMachineEvent as jest.Mock;
851
+ const stateMachineEvent = expectLastStateMachineEvent(
852
+ sendStateMachineEventSpy,
853
+ TaskEvent.HYDRATE
565
854
  );
855
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
566
856
  expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
567
857
  taskManager.getTask(taskId)
568
858
  );
@@ -577,10 +867,15 @@ describe('TaskManager', () => {
577
867
  },
578
868
  };
579
869
 
580
- const taskEmitSpy = jest.spyOn(taskManager, 'emit');
581
870
  webSocketManagerMock.emit('message', JSON.stringify(payload));
582
871
 
583
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_HYDRATE, taskManager.getTask(taskId));
872
+ const createdTask = taskManager.getTask(taskId);
873
+ const sendStateMachineEventSpy = createdTask.sendStateMachineEvent as jest.Mock;
874
+ const stateMachineEvent = expectLastStateMachineEvent(
875
+ sendStateMachineEventSpy,
876
+ TaskEvent.HYDRATE
877
+ );
878
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
584
879
  expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
585
880
  taskManager.getTask(taskId)
586
881
  );
@@ -653,7 +948,7 @@ describe('TaskManager', () => {
653
948
  expect(createdTask.data.isConferenceInProgress).toBe(false);
654
949
  });
655
950
 
656
- it('should emit TASK_END event on AGENT_WRAPUP event', () => {
951
+ it('should emit TASK_WRAPUP event on AGENT_WRAPUP event', () => {
657
952
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
658
953
 
659
954
  const wrapupPayload = {
@@ -675,13 +970,15 @@ describe('TaskManager', () => {
675
970
  };
676
971
 
677
972
  const task = taskManager.getTask(taskId);
678
- const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
679
- const taskEmitSpy = jest.spyOn(task, 'emit');
973
+ const updateTaskDataSpy = task.updateTaskData as jest.Mock;
974
+ updateTaskDataSpy.mockClear();
975
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
680
976
 
681
977
  webSocketManagerMock.emit('message', JSON.stringify(wrapupPayload));
682
978
 
683
979
  expect(updateTaskDataSpy).toHaveBeenCalledWith(wrapupPayload.data);
684
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
980
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TASK_WRAPUP);
981
+ sendStateMachineEventSpy.mockRestore();
685
982
  });
686
983
 
687
984
  it('should emit TASK_HOLD event on AGENT_CONTACT_HELD event', () => {
@@ -704,13 +1001,20 @@ describe('TaskManager', () => {
704
1001
  },
705
1002
  };
706
1003
 
707
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
708
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1004
+ const task = taskManager.getTask(taskId);
1005
+ const taskUpdateTaskDataSpy = task.updateTaskData as jest.Mock;
1006
+ taskUpdateTaskDataSpy.mockClear();
1007
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
709
1008
 
710
1009
  webSocketManagerMock.emit('message', JSON.stringify(payload));
711
1010
 
712
1011
  expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
713
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_HOLD, taskManager.getTask(taskId));
1012
+ const stateMachineEvent = expectLastStateMachineEvent(
1013
+ sendStateMachineEventSpy,
1014
+ TaskEvent.HOLD_SUCCESS
1015
+ );
1016
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1017
+ sendStateMachineEventSpy.mockRestore();
714
1018
  });
715
1019
 
716
1020
  it('should emit TASK_RESUME event on AGENT_CONTACT_UNHELD event', () => {
@@ -733,11 +1037,18 @@ describe('TaskManager', () => {
733
1037
  },
734
1038
  };
735
1039
 
736
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
737
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1040
+ const task = taskManager.getTask(taskId);
1041
+ const taskUpdateTaskDataSpy = task.updateTaskData as jest.Mock;
1042
+ taskUpdateTaskDataSpy.mockClear();
1043
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
738
1044
  webSocketManagerMock.emit('message', JSON.stringify(payload));
739
1045
  expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
740
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_RESUME, taskManager.getTask(taskId));
1046
+ const stateMachineEvent = expectLastStateMachineEvent(
1047
+ sendStateMachineEventSpy,
1048
+ TaskEvent.UNHOLD_SUCCESS
1049
+ );
1050
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1051
+ sendStateMachineEventSpy.mockRestore();
741
1052
  });
742
1053
 
743
1054
  it('handle AGENT_CONSULT_CREATED event', () => {
@@ -750,17 +1061,16 @@ describe('TaskManager', () => {
750
1061
 
751
1062
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
752
1063
  const task = taskManager.getTask(taskId);
753
- const taskUpdateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
754
- const taskEmitSpy = jest.spyOn(task, 'emit');
1064
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
755
1065
 
756
1066
  webSocketManagerMock.emit('message', JSON.stringify(payload));
757
1067
 
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);
1068
+ const stateMachineEvent = expectLastStateMachineEvent(
1069
+ sendStateMachineEventSpy,
1070
+ TaskEvent.CONSULT_CREATED
1071
+ );
1072
+ expect(stateMachineEvent?.taskData).toEqual({...payload.data, isConsulted: false});
1073
+ sendStateMachineEventSpy.mockRestore();
764
1074
  });
765
1075
 
766
1076
  it('handle AGENT_OFFER_CONTACT event', () => {
@@ -773,11 +1083,17 @@ describe('TaskManager', () => {
773
1083
 
774
1084
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
775
1085
 
776
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1086
+ const task = taskManager.getTask(taskId);
1087
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
777
1088
 
778
1089
  webSocketManagerMock.emit('message', JSON.stringify(payload));
779
1090
 
780
- expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
1091
+ const stateMachineEvent = expectLastStateMachineEvent(
1092
+ sendStateMachineEventSpy,
1093
+ TaskEvent.TASK_OFFERED
1094
+ );
1095
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1096
+ sendStateMachineEventSpy.mockRestore();
781
1097
  });
782
1098
 
783
1099
  describe('Auto-Answer Functionality', () => {
@@ -787,8 +1103,8 @@ describe('TaskManager', () => {
787
1103
 
788
1104
  const task = taskManager.getTask(taskId);
789
1105
  const taskEmitSpy = jest.spyOn(task, 'emit');
790
- const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
791
1106
  const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
1107
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
792
1108
 
793
1109
  // Step 2: Trigger AGENT_OFFER_CONTACT with auto-answer
794
1110
  const autoAnswerPayload = {
@@ -812,9 +1128,14 @@ describe('TaskManager', () => {
812
1128
  // Verify accept was called
813
1129
  expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
814
1130
 
815
- // Verify BOTH events were emitted
816
- expect(taskManagerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OFFER_CONTACT, task);
1131
+ const stateMachineEvent = expectLastStateMachineEvent(
1132
+ sendStateMachineEventSpy,
1133
+ TaskEvent.TASK_OFFERED
1134
+ );
1135
+ expect(stateMachineEvent?.taskData).toEqual(autoAnswerPayload.data);
1136
+ // Verify task auto-answer event was emitted
817
1137
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
1138
+ sendStateMachineEventSpy.mockRestore();
818
1139
  });
819
1140
 
820
1141
  it('should NOT emit TASK_AUTO_ANSWERED event when auto-answer fails', async () => {
@@ -860,6 +1181,7 @@ describe('TaskManager', () => {
860
1181
  const task = taskManager.getTask(taskId);
861
1182
  const taskEmitSpy = jest.spyOn(task, 'emit');
862
1183
  const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
1184
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
863
1185
 
864
1186
  // Step 2: Trigger AGENT_OFFER_CONSULT with auto-answer
865
1187
  const consultAutoAnswerPayload = {
@@ -884,12 +1206,20 @@ describe('TaskManager', () => {
884
1206
  // Verify accept was called
885
1207
  expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
886
1208
 
887
- // Verify BOTH events were emitted
888
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OFFER_CONSULT, task);
1209
+ const stateMachineEvent = expectLastStateMachineEvent(
1210
+ sendStateMachineEventSpy,
1211
+ TaskEvent.OFFER_CONSULT
1212
+ );
1213
+ expect(stateMachineEvent?.taskData).toEqual({
1214
+ ...consultAutoAnswerPayload.data,
1215
+ isConsulted: true,
1216
+ });
1217
+ // Verify task auto-answer event was emitted
889
1218
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
890
1219
 
891
1220
  // Verify isConsulted flag is set correctly
892
1221
  expect(task.data.isConsulted).toBe(true);
1222
+ sendStateMachineEventSpy.mockRestore();
893
1223
  });
894
1224
 
895
1225
  it('should NOT emit TASK_AUTO_ANSWERED when isAutoAnswering is false', async () => {
@@ -930,24 +1260,20 @@ describe('TaskManager', () => {
930
1260
  });
931
1261
  });
932
1262
 
933
- it('should NOT remove OUTDIAL task from taskCollection on AGENT_OUTBOUND_FAILED when terminated (wrap-up flow)', () => {
1263
+ it('should remove OUTDIAL task from taskCollection on AGENT_OUTBOUND_FAILED when terminated', () => {
934
1264
  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;
1265
+ Object.assign(task.data, {
1266
+ interaction: {
1267
+ ...task.data.interaction,
1268
+ outboundType: 'OUTDIAL',
1269
+ state: 'new',
1270
+ isTerminated: true,
1271
+ },
1272
+ agentsPendingWrapUp: ['agent-123'],
948
1273
  });
949
1274
  task.unregisterWebCallListeners = jest.fn();
950
1275
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1276
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
951
1277
 
952
1278
  const payload = {
953
1279
  data: {
@@ -960,6 +1286,7 @@ describe('TaskManager', () => {
960
1286
  state: 'new',
961
1287
  isTerminated: true,
962
1288
  },
1289
+ agentsPendingWrapUp: ['agent-123'],
963
1290
  interactionId: taskId,
964
1291
  orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
965
1292
  trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
@@ -974,23 +1301,61 @@ describe('TaskManager', () => {
974
1301
 
975
1302
  webSocketManagerMock.emit('message', JSON.stringify(payload));
976
1303
 
977
- expect(taskManager.getTask(taskId)).toBeDefined();
978
- expect(removeTaskSpy).not.toHaveBeenCalled();
1304
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1305
+
1306
+ expect(taskManager.getTask(taskId)).toBeUndefined();
1307
+ expect(removeTaskSpy).toHaveBeenCalled();
1308
+ const stateMachineEvent = expectLastStateMachineEvent(
1309
+ sendStateMachineEventSpy,
1310
+ TaskEvent.OUTBOUND_FAILED
1311
+ );
1312
+ expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
1313
+ sendStateMachineEventSpy.mockRestore();
979
1314
  });
980
1315
 
981
1316
  it('should emit TASK_OUTDIAL_FAILED event on AGENT_OUTBOUND_FAILED', () => {
982
1317
  const task = taskManager.getTask(taskId);
983
- task.updateTaskData = jest.fn().mockReturnValue(task);
984
- const taskEmitSpy = jest.spyOn(task, 'emit');
1318
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1319
+ const payload = {
1320
+ data: {
1321
+ type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
1322
+ interactionId: taskId,
1323
+ reason: 'CUSTOMER_BUSY',
1324
+ },
1325
+ };
1326
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1327
+ const stateMachineEvent = expectLastStateMachineEvent(
1328
+ sendStateMachineEventSpy,
1329
+ TaskEvent.OUTBOUND_FAILED
1330
+ );
1331
+ expect(stateMachineEvent?.reason).toBe('CUSTOMER_BUSY');
1332
+ sendStateMachineEventSpy.mockRestore();
1333
+ });
1334
+
1335
+ it('should pass taskData in OUTBOUND_FAILED event for shouldWrapUp guard evaluation', () => {
1336
+ const task = taskManager.getTask(taskId);
1337
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
985
1338
  const payload = {
986
1339
  data: {
987
1340
  type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
988
1341
  interactionId: taskId,
989
1342
  reason: 'CUSTOMER_BUSY',
1343
+ agentsPendingWrapUp: ['agent-123'],
1344
+ interaction: {
1345
+ outboundType: 'OUTDIAL',
1346
+ isTerminated: true,
1347
+ },
990
1348
  },
991
1349
  };
992
1350
  webSocketManagerMock.emit('message', JSON.stringify(payload));
993
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_OUTDIAL_FAILED, 'CUSTOMER_BUSY');
1351
+ const stateMachineEvent = expectLastStateMachineEvent(
1352
+ sendStateMachineEventSpy,
1353
+ TaskEvent.OUTBOUND_FAILED
1354
+ );
1355
+ expect(stateMachineEvent?.taskData).toBeDefined();
1356
+ expect(stateMachineEvent?.taskData?.agentsPendingWrapUp).toEqual(['agent-123']);
1357
+ expect(stateMachineEvent?.taskData?.interaction?.outboundType).toBe('OUTDIAL');
1358
+ sendStateMachineEventSpy.mockRestore();
994
1359
  });
995
1360
 
996
1361
  it('should handle AGENT_OUTBOUND_FAILED gracefully when task is undefined', () => {
@@ -1008,23 +1373,15 @@ describe('TaskManager', () => {
1008
1373
  });
1009
1374
 
1010
1375
  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
1376
  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;
1377
+ Object.assign(task.data, {
1378
+ interaction: {
1379
+ ...task.data.interaction,
1380
+ outboundType: 'OUTDIAL',
1381
+ state: 'new',
1382
+ mediaType: 'telephony',
1383
+ },
1384
+ agentsPendingWrapUp: ['test-agent-id'],
1028
1385
  });
1029
1386
  task.unregisterWebCallListeners = jest.fn();
1030
1387
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -1038,7 +1395,7 @@ describe('TaskManager', () => {
1038
1395
  state: 'new',
1039
1396
  mediaType: 'telephony',
1040
1397
  },
1041
- agentsPendingWrapUp: [agentId],
1398
+ agentsPendingWrapUp: ['test-agent-id'],
1042
1399
  },
1043
1400
  };
1044
1401
 
@@ -1050,19 +1407,14 @@ describe('TaskManager', () => {
1050
1407
 
1051
1408
  it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is empty', () => {
1052
1409
  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;
1410
+ Object.assign(task.data, {
1411
+ interaction: {
1412
+ ...task.data.interaction,
1413
+ outboundType: 'OUTDIAL',
1414
+ state: 'new',
1415
+ mediaType: 'telephony',
1416
+ },
1417
+ agentsPendingWrapUp: [],
1066
1418
  });
1067
1419
  task.unregisterWebCallListeners = jest.fn();
1068
1420
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -1087,19 +1439,13 @@ describe('TaskManager', () => {
1087
1439
 
1088
1440
  it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is undefined', () => {
1089
1441
  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;
1442
+ Object.assign(task.data, {
1443
+ interaction: {
1444
+ ...task.data.interaction,
1445
+ outboundType: 'OUTDIAL',
1446
+ state: 'new',
1447
+ mediaType: 'telephony',
1448
+ },
1103
1449
  });
1104
1450
  task.unregisterWebCallListeners = jest.fn();
1105
1451
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -1138,298 +1484,29 @@ describe('TaskManager', () => {
1138
1484
  }).not.toThrow();
1139
1485
  });
1140
1486
 
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);
1487
+ it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
1488
+ const task = taskManager.getTask(taskId);
1489
+ Object.assign(task.data, {
1490
+ interaction: {
1491
+ ...task.data.interaction,
1492
+ outboundType: 'OUTDIAL',
1493
+ state: 'new',
1494
+ isTerminated: false,
1495
+ },
1147
1496
  });
1497
+ task.unregisterWebCallListeners = jest.fn();
1498
+ const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1148
1499
 
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'],
1169
- },
1170
- };
1171
-
1172
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1173
-
1174
- expect(task.updateTaskData).toHaveBeenCalledWith(
1175
- expect.objectContaining({
1176
- wrapUpRequired: true,
1177
- })
1178
- );
1179
- });
1180
-
1181
- it('should set wrapUpRequired to false when agent is not in agentsPendingWrapUp array', () => {
1182
- const task = taskManager.getTask(taskId);
1183
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1184
- task.data = {
1185
- ...task.data,
1186
- ...newData,
1187
- };
1188
- return task;
1189
- });
1190
- task.unregisterWebCallListeners = jest.fn();
1191
-
1192
- const payload = {
1193
- data: {
1194
- type: CC_EVENTS.CONTACT_ENDED,
1195
- interactionId: taskId,
1196
- interaction: {
1197
- state: 'connected',
1198
- mediaType: 'telephony',
1199
- },
1200
- agentsPendingWrapUp: ['other-agent-id', 'another-agent-id'],
1201
- },
1202
- };
1203
-
1204
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1205
-
1206
- expect(task.updateTaskData).toHaveBeenCalledWith(
1207
- expect.objectContaining({
1208
- wrapUpRequired: false,
1209
- })
1210
- );
1211
- });
1212
-
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,
1500
+ const payload = {
1501
+ data: {
1502
+ type: CC_EVENTS.AGENT_CONTACT_ASSIGN_FAILED,
1503
+ agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
1504
+ eventTime: 1733211616959,
1505
+ eventType: 'RoutingMessage',
1506
+ interaction: {
1507
+ outboundType: 'OUTDIAL',
1508
+ state: 'new',
1509
+ isTerminated: false,
1433
1510
  },
1434
1511
  interactionId: taskId,
1435
1512
  orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
@@ -1459,20 +1536,16 @@ describe('TaskManager', () => {
1459
1536
 
1460
1537
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1461
1538
  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');
1539
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1467
1540
 
1468
1541
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1469
1542
 
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);
1543
+ const stateMachineEvent = expectLastStateMachineEvent(
1544
+ sendStateMachineEventSpy,
1545
+ TaskEvent.OFFER_CONSULT
1546
+ );
1547
+ expect(stateMachineEvent?.taskData).toEqual({...payload.data, isConsulted: true});
1548
+ sendStateMachineEventSpy.mockRestore();
1476
1549
  });
1477
1550
 
1478
1551
  it('should emit TASK_CONSULT_ACCEPTED event on AGENT_CONSULTING event', () => {
@@ -1491,19 +1564,18 @@ describe('TaskManager', () => {
1491
1564
  };
1492
1565
 
1493
1566
  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
1567
 
1499
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1568
+ const task = taskManager.getTask(taskId);
1569
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1570
+
1500
1571
  webSocketManagerMock.emit('message', JSON.stringify(initialConsultingPayload));
1501
1572
  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)
1573
+ const stateMachineEvent = expectLastStateMachineEvent(
1574
+ sendStateMachineEventSpy,
1575
+ TaskEvent.CONSULTING_ACTIVE
1506
1576
  );
1577
+ expect(stateMachineEvent?.taskData).toEqual(consultingPayload.data);
1578
+ sendStateMachineEventSpy.mockRestore();
1507
1579
  });
1508
1580
 
1509
1581
  it('should emit TASK_CONSULT_ENDED event on AGENT_CONSULT_ENDED event', () => {
@@ -1515,14 +1587,16 @@ describe('TaskManager', () => {
1515
1587
  };
1516
1588
 
1517
1589
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1518
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1519
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1590
+ taskManager.getTask(taskId).data.isConsulted = true;
1591
+ const task = taskManager.getTask(taskId);
1592
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1520
1593
  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)
1594
+ const stateMachineEvent = expectLastStateMachineEvent(
1595
+ sendStateMachineEventSpy,
1596
+ TaskEvent.CONSULT_END
1525
1597
  );
1598
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1599
+ sendStateMachineEventSpy.mockRestore();
1526
1600
  });
1527
1601
 
1528
1602
  it('should emit TASK_CONSULT_ENDED event and remove currentTask when on AGENT_CONSULT_ENDED event when requested for a consult', () => {
@@ -1535,19 +1609,14 @@ describe('TaskManager', () => {
1535
1609
 
1536
1610
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1537
1611
 
1538
- taskManager.getTask(taskId).updateTaskData = jest.fn().mockImplementation((newData) => {
1539
- taskManager.getTask(taskId).data = {...newData, isConsulted: true};
1540
- return taskManager.getTask(taskId);
1541
- });
1612
+ taskManager.getTask(taskId).data.isConsulted = true;
1542
1613
  const task = taskManager.getTask(taskId);
1543
1614
 
1544
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1545
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1615
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1546
1616
  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);
1617
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONSULT_END);
1550
1618
  expect(taskManager.getTask(taskId)).toBeUndefined(); // Ensure task is removed from the task collection after the consult ends
1619
+ sendStateMachineEventSpy.mockRestore();
1551
1620
  });
1552
1621
 
1553
1622
  it('should emit TASK_CANCELLED event on AGENT_CTQ_CANCELLED event', () => {
@@ -1559,14 +1628,15 @@ describe('TaskManager', () => {
1559
1628
  };
1560
1629
 
1561
1630
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1562
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1563
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1631
+ const task = taskManager.getTask(taskId);
1632
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1564
1633
  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)
1634
+ const stateMachineEvent = expectLastStateMachineEvent(
1635
+ sendStateMachineEventSpy,
1636
+ TaskEvent.CTQ_CANCEL
1569
1637
  );
1638
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1639
+ sendStateMachineEventSpy.mockRestore();
1570
1640
  });
1571
1641
 
1572
1642
  it('should handle AGENT_CONSULT_FAILED event', () => {
@@ -1580,9 +1650,15 @@ describe('TaskManager', () => {
1580
1650
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1581
1651
 
1582
1652
  // Always spy on the updated task object after CONTACT_RESERVED is emitted
1583
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1653
+ const task = taskManager.getTask(taskId);
1654
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1584
1655
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1585
- expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
1656
+ const stateMachineEvent = expectLastStateMachineEvent(
1657
+ sendStateMachineEventSpy,
1658
+ TaskEvent.CONSULT_FAILED
1659
+ );
1660
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1661
+ sendStateMachineEventSpy.mockRestore();
1586
1662
  });
1587
1663
 
1588
1664
  it('should emit TASK_CONSULT_QUEUE_FAILED on AGENT_CTQ_CANCEL_FAILED event', () => {
@@ -1594,14 +1670,15 @@ describe('TaskManager', () => {
1594
1670
  };
1595
1671
 
1596
1672
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1597
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1598
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1673
+ const task = taskManager.getTask(taskId);
1674
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1599
1675
  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)
1676
+ const stateMachineEvent = expectLastStateMachineEvent(
1677
+ sendStateMachineEventSpy,
1678
+ TaskEvent.CTQ_CANCEL_FAILED
1604
1679
  );
1680
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1681
+ sendStateMachineEventSpy.mockRestore();
1605
1682
  });
1606
1683
 
1607
1684
  it('should emit TASK_REJECT event on AGENT_CONTACT_OFFER_RONA event', () => {
@@ -1644,15 +1721,14 @@ describe('TaskManager', () => {
1644
1721
  };
1645
1722
 
1646
1723
  taskManager.taskCollection[taskId] = taskManager.getTask(taskId);
1647
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1648
- const metricsTrackSpy = jest.spyOn(taskManager.metricsManager, 'trackEvent');
1724
+ const task = taskManager.getTask(taskId);
1725
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1649
1726
 
1650
1727
  webSocketManagerMock.emit('message', JSON.stringify(ronaPayload));
1651
1728
 
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');
1729
+ const stateMachineEvent = expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.RONA);
1730
+ expect(stateMachineEvent?.reason).toBe(ronaPayload.data.reason);
1731
+ sendStateMachineEventSpy.mockRestore();
1656
1732
  });
1657
1733
 
1658
1734
  it('should emit TASK_REJECT event on AGENT_CONTACT_ASSIGN_FAILED event', () => {
@@ -1696,20 +1772,16 @@ describe('TaskManager', () => {
1696
1772
 
1697
1773
  taskManager.taskCollection[taskId] = taskManager.getTask(taskId);
1698
1774
  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');
1775
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1702
1776
 
1703
1777
  webSocketManagerMock.emit('message', JSON.stringify(assignFailedPayload));
1704
1778
 
1705
- expect(taskUpdateDataSpy).toHaveBeenCalledWith(assignFailedPayload.data);
1706
- expect(taskEmitSpy).toHaveBeenCalledWith(
1707
- TASK_EVENTS.TASK_REJECT,
1708
- assignFailedPayload.data.reason
1779
+ const stateMachineEvent = expectLastStateMachineEvent(
1780
+ sendStateMachineEventSpy,
1781
+ TaskEvent.ASSIGN_FAILED
1709
1782
  );
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');
1783
+ expect(stateMachineEvent?.reason).toBe(assignFailedPayload.data.reason);
1784
+ sendStateMachineEventSpy.mockRestore();
1713
1785
  });
1714
1786
 
1715
1787
  it('should remove currentTask from taskCollection on AGENT_WRAPPEDUP event', () => {
@@ -1732,11 +1804,12 @@ describe('TaskManager', () => {
1732
1804
 
1733
1805
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1734
1806
  const task = taskManager.getTask(taskId);
1735
- const taskEmitSpy = jest.spyOn(task, 'emit');
1807
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1736
1808
 
1737
1809
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1738
1810
 
1739
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WRAPPEDUP, task);
1811
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.WRAPUP_COMPLETE);
1812
+ sendStateMachineEventSpy.mockRestore();
1740
1813
  expect(taskManager.getTask(taskId)).toBeUndefined();
1741
1814
  });
1742
1815
 
@@ -1762,7 +1835,8 @@ describe('TaskManager', () => {
1762
1835
  };
1763
1836
 
1764
1837
  const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1765
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1838
+ const taskUpdateTaskDataSpy = taskManager.getTask(taskId).updateTaskData as jest.Mock;
1839
+ taskUpdateTaskDataSpy.mockClear();
1766
1840
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1767
1841
  expect(taskEmitSpy).not.toHaveBeenCalled();
1768
1842
  expect(taskUpdateTaskDataSpy).not.toHaveBeenCalled();
@@ -1771,7 +1845,9 @@ describe('TaskManager', () => {
1771
1845
  it('should emit TASK_CONSULTING event when agent is consulting', () => {
1772
1846
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1773
1847
  taskManager.getTask(taskId).data.isConsulted = false;
1774
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1848
+ const task = taskManager.getTask(taskId);
1849
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1850
+ const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
1775
1851
  const consultingPayload = {
1776
1852
  data: {
1777
1853
  ...initalPayload.data,
@@ -1780,16 +1856,21 @@ describe('TaskManager', () => {
1780
1856
  },
1781
1857
  };
1782
1858
  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,
1859
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONSULTING_ACTIVE);
1860
+ expect(taskManagerEmitSpy).toHaveBeenCalledWith(
1861
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
1786
1862
  taskManager.getTask(taskId)
1787
1863
  );
1864
+ sendStateMachineEventSpy.mockRestore();
1788
1865
  });
1789
1866
 
1790
- it('should emit TASK_END event on AGENT_CONTACT_UNASSIGNED', () => {
1867
+ it('should update task data on AGENT_CONTACT_UNASSIGNED', () => {
1791
1868
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1792
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1869
+ const task = taskManager.getTask(taskId);
1870
+ const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
1871
+ sendStateMachineEventSpy.mockClear();
1872
+ const updateTaskDataSpy = task.updateTaskData as jest.Mock;
1873
+ updateTaskDataSpy.mockClear();
1793
1874
  const unassignedPayload = {
1794
1875
  data: {
1795
1876
  type: CC_EVENTS.AGENT_CONTACT_UNASSIGNED,
@@ -1807,11 +1888,76 @@ describe('TaskManager', () => {
1807
1888
  },
1808
1889
  };
1809
1890
  webSocketManagerMock.emit('message', JSON.stringify(unassignedPayload));
1810
- expect(taskEmitSpy).toHaveBeenCalledWith(
1811
- CC_EVENTS.AGENT_CONTACT_UNASSIGNED,
1812
- unassignedPayload.data
1891
+ expect(sendStateMachineEventSpy).not.toHaveBeenCalled();
1892
+ expect(updateTaskDataSpy).toHaveBeenCalledWith(unassignedPayload.data);
1893
+ });
1894
+
1895
+ it('preserves consult fields from state context during consulting payload refresh', () => {
1896
+ const task = createMockTask({
1897
+ ...taskDataMock,
1898
+ interaction: {state: 'consulting'} as any,
1899
+ interactionId: taskId,
1900
+ });
1901
+ task.stateMachineService = {
1902
+ getSnapshot: () => ({
1903
+ value: 'CONSULTING',
1904
+ context: {
1905
+ consultDestinationAgentId: 'agent-preserved',
1906
+ consultDestinationType: 'agent',
1907
+ },
1908
+ }),
1909
+ };
1910
+
1911
+ const incomingTaskData = {
1912
+ ...taskDataMock,
1913
+ interaction: {state: 'consulting'} as any,
1914
+ interactionId: taskId,
1915
+ destAgentId: null,
1916
+ destinationType: null,
1917
+ };
1918
+
1919
+ (taskManager as any).updateTaskData(task, incomingTaskData);
1920
+
1921
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1922
+ expect.objectContaining({
1923
+ destAgentId: 'agent-preserved',
1924
+ destinationType: 'agent',
1925
+ })
1926
+ );
1927
+ });
1928
+
1929
+ it('does not preserve stale consult fields once consult is no longer active', () => {
1930
+ const task = createMockTask({
1931
+ ...taskDataMock,
1932
+ interaction: {state: 'connected'} as any,
1933
+ interactionId: taskId,
1934
+ });
1935
+ task.stateMachineService = {
1936
+ getSnapshot: () => ({
1937
+ value: 'CONNECTED',
1938
+ context: {
1939
+ consultDestinationAgentId: 'agent-stale',
1940
+ consultDestinationType: 'agent',
1941
+ },
1942
+ }),
1943
+ };
1944
+
1945
+ const incomingTaskData = {
1946
+ ...taskDataMock,
1947
+ interaction: {state: 'connected'} as any,
1948
+ interactionId: taskId,
1949
+ destAgentId: null,
1950
+ destinationType: null,
1951
+ };
1952
+
1953
+ (taskManager as any).updateTaskData(task, incomingTaskData);
1954
+
1955
+ expect(task.updateTaskData).toHaveBeenCalledWith(
1956
+ expect.objectContaining({
1957
+ destAgentId: null,
1958
+ destinationType: null,
1959
+ })
1813
1960
  );
1814
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, taskManager.getTask(taskId));
1815
1961
  });
1816
1962
 
1817
1963
  it('should handle chat interaction and emit TASK_INCOMING immediately', () => {
@@ -1823,16 +1969,16 @@ describe('TaskManager', () => {
1823
1969
  },
1824
1970
  };
1825
1971
 
1826
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
1827
-
1828
1972
  // Simulate receiving a chat task
1829
1973
  webSocketManagerMock.emit('message', JSON.stringify(chatPayload));
1830
1974
 
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)
1975
+ const chatTask = taskManager.getTask(chatPayload.data.interactionId);
1976
+ const sendStateMachineEventSpy = chatTask.sendStateMachineEvent as jest.Mock;
1977
+ const stateMachineEvent = expectLastStateMachineEvent(
1978
+ sendStateMachineEventSpy,
1979
+ TaskEvent.TASK_INCOMING
1835
1980
  );
1981
+ expect(stateMachineEvent?.taskData).toEqual(chatPayload.data);
1836
1982
  expect(taskManager.getAllTasks()).toHaveProperty(chatPayload.data.interactionId);
1837
1983
  });
1838
1984
 
@@ -1845,16 +1991,16 @@ describe('TaskManager', () => {
1845
1991
  },
1846
1992
  };
1847
1993
 
1848
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
1849
-
1850
1994
  // Simulate receiving an email task
1851
1995
  webSocketManagerMock.emit('message', JSON.stringify(emailPayload));
1852
1996
 
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)
1997
+ const emailTask = taskManager.getTask(emailPayload.data.interactionId);
1998
+ const sendStateMachineEventSpy = emailTask.sendStateMachineEvent as jest.Mock;
1999
+ const stateMachineEvent = expectLastStateMachineEvent(
2000
+ sendStateMachineEventSpy,
2001
+ TaskEvent.TASK_INCOMING
1857
2002
  );
2003
+ expect(stateMachineEvent?.taskData).toEqual(emailPayload.data);
1858
2004
  expect(taskManager.getAllTasks()).toHaveProperty(emailPayload.data.interactionId);
1859
2005
  });
1860
2006
 
@@ -1868,13 +2014,11 @@ describe('TaskManager', () => {
1868
2014
  },
1869
2015
  };
1870
2016
 
1871
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
1872
2017
  webSocketManagerMock.emit('message', JSON.stringify(chatReservedPayload));
2018
+ const task = taskManager.getTask(chatReservedPayload.data.interactionId);
2019
+ const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
1873
2020
 
1874
- expect(taskIncomingSpy).toHaveBeenCalledWith(
1875
- TASK_EVENTS.TASK_INCOMING,
1876
- taskManager.getTask(chatReservedPayload.data.interactionId)
1877
- );
2021
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TASK_INCOMING);
1878
2022
 
1879
2023
  // 2. Chat task is assigned
1880
2024
  const chatAssignedPayload = {
@@ -1884,12 +2028,9 @@ describe('TaskManager', () => {
1884
2028
  },
1885
2029
  };
1886
2030
 
1887
- const task = taskManager.getTask(chatReservedPayload.data.interactionId);
1888
- const taskEmitSpy = jest.spyOn(task, 'emit');
1889
-
1890
2031
  webSocketManagerMock.emit('message', JSON.stringify(chatAssignedPayload));
1891
2032
 
1892
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, task);
2033
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.ASSIGN);
1893
2034
 
1894
2035
  // 3. Chat task is ended with state 'new' to trigger cleanup
1895
2036
  const chatEndedPayload = {
@@ -1897,12 +2038,15 @@ describe('TaskManager', () => {
1897
2038
  ...chatReservedPayload.data,
1898
2039
  type: CC_EVENTS.CONTACT_ENDED,
1899
2040
  interaction: {mediaType: 'chat', state: 'new'}, // Change to 'new' state
2041
+ wrapUpRequired: false,
1900
2042
  },
1901
2043
  };
1902
2044
 
2045
+ // Simulate state on the task to allow cleanup logic
2046
+ task.data.interaction.state = 'new';
1903
2047
  webSocketManagerMock.emit('message', JSON.stringify(chatEndedPayload));
1904
2048
 
1905
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
2049
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONTACT_ENDED);
1906
2050
  // Verify task is removed since it was in a 'new' state
1907
2051
  expect(taskManager.getTask(chatReservedPayload.data.interactionId)).toBeUndefined();
1908
2052
  });
@@ -1991,13 +2135,8 @@ describe('TaskManager', () => {
1991
2135
  expect(taskManager.getAllTasks()).toHaveProperty(task2Payload.data.interactionId);
1992
2136
  expect(taskManager.getAllTasks()).toHaveProperty(task3Payload.data.interactionId);
1993
2137
 
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
2138
  const task2 = taskManager.getTask(task2Payload.data.interactionId);
2139
+ const task2SendStateMachineEventSpy = task2.sendStateMachineEvent as jest.Mock;
2001
2140
 
2002
2141
  // End only the second task (chat task)
2003
2142
  const chatEndedPayload = {
@@ -2005,15 +2144,18 @@ describe('TaskManager', () => {
2005
2144
  ...task2Payload.data,
2006
2145
  type: CC_EVENTS.CONTACT_ENDED,
2007
2146
  interaction: {mediaType: 'chat', state: 'new'}, // Using 'new' to trigger cleanup
2147
+ wrapUpRequired: false,
2008
2148
  },
2009
2149
  };
2010
2150
 
2151
+ task2.data.interaction.state = 'new';
2011
2152
  webSocketManagerMock.emit('message', JSON.stringify(chatEndedPayload));
2012
2153
 
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);
2154
+ const firstEndEvent = expectLastStateMachineEvent(
2155
+ task2SendStateMachineEventSpy,
2156
+ TaskEvent.CONTACT_ENDED
2157
+ );
2158
+ expect(firstEndEvent?.taskData).toEqual(chatEndedPayload.data);
2017
2159
 
2018
2160
  // Verify task2 was removed from collection (since state was 'new')
2019
2161
  expect(taskManager.getTask(task2Payload.data.interactionId)).toBeUndefined();
@@ -2024,6 +2166,7 @@ describe('TaskManager', () => {
2024
2166
 
2025
2167
  // Store reference to task3 before we end it
2026
2168
  const task3 = taskManager.getTask(task3Payload.data.interactionId);
2169
+ const task3SendStateMachineEventSpy = task3.sendStateMachineEvent as jest.Mock;
2027
2170
 
2028
2171
  // Now end task3 with a state that doesn't trigger cleanup
2029
2172
  const emailEndedPayload = {
@@ -2031,31 +2174,32 @@ describe('TaskManager', () => {
2031
2174
  ...task3Payload.data,
2032
2175
  type: CC_EVENTS.CONTACT_ENDED,
2033
2176
  interaction: {mediaType: 'email', state: 'connected'}, // Using 'connected' to NOT trigger cleanup
2177
+ wrapUpRequired: true,
2178
+ agentsPendingWrapUp: ['test-agent-id'],
2034
2179
  },
2035
2180
  };
2036
2181
 
2182
+ task3.data.interaction.state = 'connected';
2037
2183
  webSocketManagerMock.emit('message', JSON.stringify(emailEndedPayload));
2038
2184
 
2039
- // Verify task3 emitted TASK_END
2040
- expect(task3EmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task3);
2185
+ const secondEndEvent = expectLastStateMachineEvent(
2186
+ task3SendStateMachineEventSpy,
2187
+ TaskEvent.CONTACT_ENDED
2188
+ );
2189
+ expect(secondEndEvent?.taskData).toEqual(emailEndedPayload.data);
2041
2190
 
2042
2191
  // Verify task3 is still in collection (since state was 'connected')
2043
2192
  expect(taskManager.getTask(task3Payload.data.interactionId)).toBeDefined();
2044
2193
 
2045
2194
  // Verify task1 remains unaffected
2046
- expect(task1EmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END);
2047
2195
  expect(taskManager.getTask(task1Payload.data.interactionId)).toBeDefined();
2048
2196
  });
2049
2197
 
2050
- it('should emit TASK_END event on AGENT_VTEAM_TRANSFERRED event', () => {
2198
+ it('should emit TRANSFER_SUCCESS event on AGENT_VTEAM_TRANSFERRED event', () => {
2051
2199
  // First create a task by emitting the initial payload
2052
2200
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
2053
-
2054
- // Get a reference to the task from taskCollection
2055
2201
  const task = taskManager.getTask(taskId);
2056
-
2057
- // Now spy on the task's emit method
2058
- const taskEmitSpy = jest.spyOn(task, 'emit');
2202
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
2059
2203
 
2060
2204
  const vteamTransferredPayload = {
2061
2205
  data: {
@@ -2079,8 +2223,9 @@ describe('TaskManager', () => {
2079
2223
 
2080
2224
  webSocketManagerMock.emit('message', JSON.stringify(vteamTransferredPayload));
2081
2225
 
2082
- // Check that task.emit was called with TASK_END event
2083
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
2226
+ // Check that the state machine received the END event
2227
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TRANSFER_SUCCESS);
2228
+ sendStateMachineEventSpy.mockRestore();
2084
2229
 
2085
2230
  // The task should still exist in the collection based on current implementation
2086
2231
  expect(taskManager.getTask(taskId)).toBeDefined();
@@ -2095,12 +2240,16 @@ describe('TaskManager', () => {
2095
2240
  },
2096
2241
  };
2097
2242
  const task = taskManager.getTask(taskId);
2098
- const updateSpy = jest.spyOn(task, 'updateTaskData').mockImplementation((data) => {
2099
- task.data = {...(task.data || {}), ...(data || {})};
2100
- return task;
2101
- });
2243
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
2102
2244
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2103
- expect(updateSpy).toHaveBeenCalledWith(payload.data);
2245
+ expect(sendStateMachineEventSpy).toHaveBeenCalled();
2246
+ const stateMachineEvent = sendStateMachineEventSpy.mock.calls.at(-1)?.[0];
2247
+ expect(stateMachineEvent?.type).toBe(TaskEvent.TASK_WRAPUP);
2248
+ expect(stateMachineEvent?.taskData).toEqual({
2249
+ ...payload.data,
2250
+ wrapUpRequired: true,
2251
+ });
2252
+ sendStateMachineEventSpy.mockRestore();
2104
2253
  });
2105
2254
 
2106
2255
  it('should not attempt cleanup twice when AGENT_CONTACT_UNASSIGNED is followed by AGENT_WRAPUP', () => {
@@ -2176,1176 +2325,157 @@ describe('TaskManager', () => {
2176
2325
  });
2177
2326
 
2178
2327
  describe('should emit appropriate task events for recording events', () => {
2179
- ['PAUSED', 'PAUSE_FAILED', 'RESUMED', 'RESUME_FAILED'].forEach((suffix) => {
2328
+ const eventMap: Record<string, TaskEvent | null> = {
2329
+ STARTED: TaskEvent.RECORDING_STARTED,
2330
+ PAUSED: TaskEvent.PAUSE_RECORDING,
2331
+ PAUSE_FAILED: null,
2332
+ RESUMED: TaskEvent.RESUME_RECORDING,
2333
+ RESUME_FAILED: null,
2334
+ };
2335
+
2336
+ ['STARTED', 'PAUSED', 'PAUSE_FAILED', 'RESUMED', 'RESUME_FAILED'].forEach((suffix) => {
2180
2337
  const ccEvent = CC_EVENTS[`CONTACT_RECORDING_${suffix}`];
2181
- const taskEvent = TASK_EVENTS[`TASK_RECORDING_${suffix}`];
2182
- it(`should emit ${taskEvent} on ${ccEvent} event`, () => {
2338
+ const expectedTaskEvent = eventMap[suffix];
2339
+ it(`should ${expectedTaskEvent ? 'send' : 'not send'} ${
2340
+ expectedTaskEvent ?? 'a'
2341
+ } state machine event on ${ccEvent} event`, () => {
2183
2342
  const payload = {data: {...initalPayload.data, type: ccEvent}};
2184
2343
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
2185
2344
  const task = taskManager.getTask(taskId);
2186
- const spy = jest.spyOn(task, 'emit');
2345
+ const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
2346
+ sendStateMachineEventSpy.mockClear();
2187
2347
 
2188
2348
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2189
- expect(spy).toHaveBeenCalledWith(taskEvent, task);
2349
+ if (expectedTaskEvent) {
2350
+ const stateMachineEvent = expectLastStateMachineEvent(
2351
+ sendStateMachineEventSpy,
2352
+ expectedTaskEvent
2353
+ );
2354
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
2355
+ } else {
2356
+ expect(sendStateMachineEventSpy).not.toHaveBeenCalled();
2357
+ }
2190
2358
  });
2191
2359
  });
2192
2360
  });
2193
2361
 
2194
2362
  describe('Conference event handling', () => {
2195
2363
  let task;
2196
- const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
2197
2364
 
2198
2365
  beforeEach(() => {
2199
- // Set the agentId on taskManager before tests run
2200
- taskManager.setAgentId(agentId);
2201
-
2202
2366
  task = {
2203
2367
  data: {interactionId: taskId},
2204
2368
  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
- }),
2369
+ updateTaskData: jest.fn(),
2370
+ sendStateMachineEvent: jest.fn(),
2210
2371
  };
2211
- taskManager.taskCollection[taskId] = task;
2372
+ taskManager.taskCollection[taskId] = task as any;
2212
2373
  });
2213
2374
 
2214
- it('should handle AGENT_CONSULT_CONFERENCED event', () => {
2375
+ it('sends AGENT_CONSULT_CONFERENCED to state machine as CONFERENCE_START', () => {
2215
2376
  const payload = {
2216
- data: {
2217
- type: CC_EVENTS.AGENT_CONSULT_CONFERENCED,
2218
- interactionId: taskId,
2219
- isConferencing: true,
2220
- },
2377
+ data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCED, interactionId: taskId},
2221
2378
  };
2222
-
2223
2379
  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);
2380
+ expect(task.sendStateMachineEvent).toHaveBeenCalled();
2381
+ const call = task.sendStateMachineEvent.mock.calls[0][0];
2382
+ expect(call.type).toBe(TaskEvent.CONFERENCE_START);
2227
2383
  });
2228
2384
 
2229
- it('should handle AGENT_CONSULT_CONFERENCING event', () => {
2385
+ it('sends PARTICIPANT_JOINED_CONFERENCE to state machine as CONFERENCE_START', () => {
2230
2386
  const payload = {
2231
- data: {
2232
- type: CC_EVENTS.AGENT_CONSULT_CONFERENCING,
2233
- interactionId: taskId,
2234
- isConferencing: true,
2235
- },
2387
+ data: {type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE, interactionId: taskId},
2236
2388
  };
2237
-
2238
2389
  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);
2390
+ expect(task.sendStateMachineEvent).toHaveBeenCalled();
2391
+ const call = task.sendStateMachineEvent.mock.calls[0][0];
2392
+ expect(call.type).toBe(TaskEvent.CONFERENCE_START);
2243
2393
  });
2244
2394
 
2245
- it('should handle AGENT_CONSULT_CONFERENCE_FAILED event', () => {
2395
+ it('sends AGENT_CONSULT_CONFERENCE_FAILED to state machine as CONFERENCE_FAILED', () => {
2246
2396
  const payload = {
2247
- data: {
2248
- type: CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED,
2249
- interactionId: taskId,
2250
- reason: 'Network error',
2251
- },
2397
+ data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED, interactionId: taskId},
2252
2398
  };
2253
-
2254
2399
  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
2400
+ expect(task.sendStateMachineEvent).toHaveBeenCalled();
2401
+ const call = task.sendStateMachineEvent.mock.calls[0][0];
2402
+ expect(call.type).toBe(TaskEvent.CONFERENCE_FAILED);
2258
2403
  });
2259
2404
 
2260
- it('should handle PARTICIPANT_JOINED_CONFERENCE event', () => {
2405
+ it('sends PARTICIPANT_LEFT_CONFERENCE to state machine as PARTICIPANT_LEAVE', () => {
2261
2406
  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
- },
2407
+ data: {type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, interactionId: taskId},
2280
2408
  };
2281
-
2282
2409
  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
2410
+ expect(task.sendStateMachineEvent).toHaveBeenCalled();
2411
+ const call = task.sendStateMachineEvent.mock.calls[0][0];
2412
+ expect(call.type).toBe(TaskEvent.PARTICIPANT_LEAVE);
2287
2413
  });
2288
2414
 
2289
- it('should call updateTaskData only once for PARTICIPANT_JOINED_CONFERENCE with pre-calculated isConferenceInProgress', () => {
2415
+ it('handles AGENT_CONSULT_CONFERENCING event without errors', () => {
2290
2416
  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
- },
2417
+ data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCING, interactionId: taskId},
2310
2418
  };
2419
+ expect(() => {
2420
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2421
+ }).not.toThrow();
2422
+ });
2311
2423
 
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
-
2384
- 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
- });
2622
- });
2623
-
2624
- it('should handle PARTICIPANT_LEFT_CONFERENCE_FAILED event', () => {
2424
+ it('handles PARTICIPANT_LEFT_CONFERENCE_FAILED event without errors', () => {
2625
2425
  const payload = {
2626
- data: {
2627
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED,
2628
- interactionId: taskId,
2629
- reason: 'Exit failed',
2630
- },
2426
+ data: {type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED, interactionId: taskId},
2631
2427
  };
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
2428
+ expect(() => {
2429
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2430
+ }).not.toThrow();
2637
2431
  });
2638
2432
 
2639
- it('should only update task for matching interactionId', () => {
2433
+ it('only routes conference events to matching tasks', () => {
2640
2434
  const otherTaskId = 'other-task-id';
2641
- const otherTask = {
2435
+ const otherTask: any = {
2642
2436
  data: {interactionId: otherTaskId},
2643
2437
  emit: jest.fn(),
2438
+ updateTaskData: jest.fn(),
2439
+ sendStateMachineEvent: jest.fn(),
2644
2440
  };
2645
2441
  taskManager.taskCollection[otherTaskId] = otherTask;
2646
2442
 
2647
2443
  const payload = {
2648
- data: {
2649
- type: CC_EVENTS.AGENT_CONSULT_CONFERENCED,
2650
- interactionId: taskId,
2651
- isConferencing: true,
2652
- },
2444
+ data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCED, interactionId: taskId},
2653
2445
  };
2654
-
2655
2446
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2656
2447
 
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();
2448
+ expect(task.sendStateMachineEvent).toHaveBeenCalled();
2449
+ expect(otherTask.sendStateMachineEvent).not.toHaveBeenCalled();
2664
2450
  });
2665
2451
  });
2666
2452
 
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
- });
2453
+ describe('state machine integration', () => {
2454
+ it('maps CC events to task state machine events using normalized payload', () => {
2455
+ const mapped = (TaskManager as any).mapEventToTaskStateMachineEvent(
2456
+ CC_EVENTS.AGENT_CONTACT_ASSIGNED,
2457
+ {...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED}
2458
+ );
2787
2459
 
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;
2460
+ expect(mapped).toEqual({
2461
+ type: TaskEvent.ASSIGN,
2462
+ taskData: {...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED},
2803
2463
  });
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
2464
  });
2825
2465
 
2826
- it('should NOT remove OUTDIAL task when needsWrapUp is true (current agent in agentsPendingWrapUp) even if state is WRAPUP', () => {
2466
+ it('sends mapped events to the task state machine service', () => {
2467
+ const payload = {...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED};
2827
2468
  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');
2469
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
2844
2470
 
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));
2471
+ webSocketManagerMock.emit('message', JSON.stringify({data: payload}));
2859
2472
 
2860
- expect(removeTaskSpy).not.toHaveBeenCalled();
2861
- expect(taskManager.getTask(taskId)).toBeDefined();
2862
- });
2863
-
2864
- it('should remove non-OUTDIAL task when state is new regardless of agentsPendingWrapUp', () => {
2865
- const task = taskManager.getTask(taskId);
2866
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
2867
- task.data = {
2868
- ...task.data,
2869
- ...newData,
2870
- interaction: {
2871
- ...task.data.interaction,
2872
- outboundType: 'PREVIEW', // Not OUTDIAL
2873
- state: 'new',
2874
- mediaType: 'telephony',
2875
- },
2876
- agentsPendingWrapUp: [agentId],
2877
- };
2878
- return task;
2473
+ expect(sendStateMachineEventSpy).toHaveBeenCalledWith({
2474
+ type: TaskEvent.ASSIGN,
2475
+ taskData: payload,
2879
2476
  });
2880
- task.unregisterWebCallListeners = jest.fn();
2881
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2882
-
2883
- const payload = {
2884
- data: {
2885
- type: CC_EVENTS.CONTACT_ENDED,
2886
- interactionId: taskId,
2887
- interaction: {
2888
- outboundType: 'PREVIEW',
2889
- state: 'new',
2890
- mediaType: 'telephony',
2891
- },
2892
- agentsPendingWrapUp: [agentId],
2893
- },
2894
- };
2895
-
2896
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2897
-
2898
- expect(removeTaskSpy).toHaveBeenCalled();
2899
- expect(taskManager.getTask(taskId)).toBeUndefined();
2900
- });
2901
-
2902
- it('should handle agentsPendingWrapUp with multiple agents correctly - remove if current agent not in list', () => {
2903
- const task = taskManager.getTask(taskId);
2904
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
2905
- task.data = {
2906
- ...task.data,
2907
- ...newData,
2908
- interaction: {
2909
- ...task.data.interaction,
2910
- outboundType: 'OUTDIAL',
2911
- state: 'new',
2912
- mediaType: 'telephony',
2913
- },
2914
- agentsPendingWrapUp: ['agent-1', 'agent-2', 'agent-3'], // Current agent not in the list
2915
- };
2916
- return task;
2917
- });
2918
- task.unregisterWebCallListeners = jest.fn();
2919
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2920
-
2921
- const payload = {
2922
- data: {
2923
- type: CC_EVENTS.CONTACT_ENDED,
2924
- interactionId: taskId,
2925
- interaction: {
2926
- outboundType: 'OUTDIAL',
2927
- state: 'new',
2928
- mediaType: 'telephony',
2929
- },
2930
- agentsPendingWrapUp: ['agent-1', 'agent-2', 'agent-3'],
2931
- },
2932
- };
2933
-
2934
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2935
-
2936
- expect(removeTaskSpy).toHaveBeenCalled();
2937
- expect(taskManager.getTask(taskId)).toBeUndefined();
2938
- });
2939
-
2940
- it('should handle agentsPendingWrapUp with multiple agents correctly - keep if current agent is in list', () => {
2941
- const task = taskManager.getTask(taskId);
2942
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
2943
- task.data = {
2944
- ...task.data,
2945
- ...newData,
2946
- interaction: {
2947
- ...task.data.interaction,
2948
- outboundType: 'OUTDIAL',
2949
- state: 'new',
2950
- mediaType: 'telephony',
2951
- },
2952
- agentsPendingWrapUp: ['agent-1', agentId, 'agent-3'], // Current agent IS in the list
2953
- };
2954
- return task;
2955
- });
2956
- task.unregisterWebCallListeners = jest.fn();
2957
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2958
-
2959
- const payload = {
2960
- data: {
2961
- type: CC_EVENTS.CONTACT_ENDED,
2962
- interactionId: taskId,
2963
- interaction: {
2964
- outboundType: 'OUTDIAL',
2965
- state: 'new',
2966
- mediaType: 'telephony',
2967
- },
2968
- agentsPendingWrapUp: ['agent-1', agentId, 'agent-3'],
2969
- },
2970
- };
2971
-
2972
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2973
-
2974
- expect(removeTaskSpy).not.toHaveBeenCalled();
2975
- expect(taskManager.getTask(taskId)).toBeDefined();
2976
- });
2977
- });
2978
-
2979
- describe('CONTACT_MERGED event handling', () => {
2980
- let task;
2981
- let taskEmitSpy;
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
2477
 
3347
- // TASK_END should NOT have been emitted
3348
- expect(taskEmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END, expect.anything());
2478
+ sendStateMachineEventSpy.mockRestore();
3349
2479
  });
3350
2480
  });
3351
2481
  });