@webex/contact-center 3.11.0 → 3.12.0-llmrefactor.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/.sdd/manifest.json +882 -0
  2. package/AGENTS.md +94 -0
  3. package/ai-docs/ARCHITECTURE.md +168 -0
  4. package/ai-docs/CONTRACTS.md +46 -0
  5. package/ai-docs/GETTING_STARTED.md +168 -0
  6. package/ai-docs/GLOSSARY.md +43 -0
  7. package/ai-docs/README.md +138 -0
  8. package/ai-docs/REVIEW_CHECKLIST.md +41 -0
  9. package/ai-docs/RULES.md +444 -0
  10. package/ai-docs/SECURITY.md +52 -0
  11. package/ai-docs/SERVICE_STATE.md +48 -0
  12. package/ai-docs/SPEC_INDEX.md +65 -0
  13. package/ai-docs/adr/0001-spec-source-policy.md +55 -0
  14. package/ai-docs/adr/README.md +8 -0
  15. package/ai-docs/adr/_adr-template.md +31 -0
  16. package/ai-docs/contact-center-spec.md +341 -0
  17. package/ai-docs/features/generated-spec-conformance-fidelity-remediation/spec/feature-spec.md +117 -0
  18. package/ai-docs/features/residual-warning-coverage-completion/spec/feature-spec.md +203 -0
  19. package/ai-docs/features/validator-code-fidelity-drift-fix/spec/feature-spec.md +315 -0
  20. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  21. package/ai-docs/patterns/testing-patterns.md +480 -0
  22. package/ai-docs/patterns/typescript-patterns.md +365 -0
  23. package/ai-docs/templates/README.md +102 -0
  24. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  25. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  26. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  27. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  28. package/ai-docs/templates/new-method/00-master.md +80 -0
  29. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  30. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  31. package/ai-docs/templates/new-method/03-tests.md +201 -0
  32. package/ai-docs/templates/new-method/04-validation.md +141 -0
  33. package/ai-docs/templates/new-service/00-master.md +109 -0
  34. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  35. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  36. package/ai-docs/templates/new-service/03-integration.md +178 -0
  37. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  38. package/ai-docs/templates/new-service/05-validation.md +145 -0
  39. package/dist/cc.js +379 -50
  40. package/dist/cc.js.map +1 -1
  41. package/dist/config.js +6 -0
  42. package/dist/config.js.map +1 -1
  43. package/dist/constants.js +22 -2
  44. package/dist/constants.js.map +1 -1
  45. package/dist/index.js +27 -5
  46. package/dist/index.js.map +1 -1
  47. package/dist/metrics/behavioral-events.js +114 -0
  48. package/dist/metrics/behavioral-events.js.map +1 -1
  49. package/dist/metrics/constants.js +32 -3
  50. package/dist/metrics/constants.js.map +1 -1
  51. package/dist/services/ApiAiAssistant.js +318 -0
  52. package/dist/services/ApiAiAssistant.js.map +1 -0
  53. package/dist/services/UserPreference.js +427 -0
  54. package/dist/services/UserPreference.js.map +1 -0
  55. package/dist/services/agent/types.js.map +1 -1
  56. package/dist/services/config/Util.js +8 -4
  57. package/dist/services/config/Util.js.map +1 -1
  58. package/dist/services/config/constants.js +35 -2
  59. package/dist/services/config/constants.js.map +1 -1
  60. package/dist/services/config/index.js +41 -2
  61. package/dist/services/config/index.js.map +1 -1
  62. package/dist/services/config/types.js +66 -8
  63. package/dist/services/config/types.js.map +1 -1
  64. package/dist/services/constants.js +27 -1
  65. package/dist/services/constants.js.map +1 -1
  66. package/dist/services/core/Err.js.map +1 -1
  67. package/dist/services/core/Utils.js +122 -25
  68. package/dist/services/core/Utils.js.map +1 -1
  69. package/dist/services/core/aqm-reqs.js +92 -17
  70. package/dist/services/core/aqm-reqs.js.map +1 -1
  71. package/dist/services/core/websocket/WebSocketManager.js +22 -6
  72. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  73. package/dist/services/core/websocket/connection-service.js +3 -1
  74. package/dist/services/core/websocket/connection-service.js.map +1 -1
  75. package/dist/services/core/websocket/types.js.map +1 -1
  76. package/dist/services/index.js +6 -0
  77. package/dist/services/index.js.map +1 -1
  78. package/dist/services/task/Task.js +688 -0
  79. package/dist/services/task/Task.js.map +1 -0
  80. package/dist/services/task/TaskFactory.js +45 -0
  81. package/dist/services/task/TaskFactory.js.map +1 -0
  82. package/dist/services/task/TaskManager.js +751 -457
  83. package/dist/services/task/TaskManager.js.map +1 -1
  84. package/dist/services/task/TaskUtils.js +220 -23
  85. package/dist/services/task/TaskUtils.js.map +1 -1
  86. package/dist/services/task/constants.js +23 -2
  87. package/dist/services/task/constants.js.map +1 -1
  88. package/dist/services/task/dialer.js +129 -0
  89. package/dist/services/task/dialer.js.map +1 -1
  90. package/dist/services/task/digital/Digital.js +77 -0
  91. package/dist/services/task/digital/Digital.js.map +1 -0
  92. package/dist/services/task/state-machine/TaskStateMachine.js +873 -0
  93. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  94. package/dist/services/task/state-machine/actions.js +567 -0
  95. package/dist/services/task/state-machine/actions.js.map +1 -0
  96. package/dist/services/task/state-machine/constants.js +161 -0
  97. package/dist/services/task/state-machine/constants.js.map +1 -0
  98. package/dist/services/task/state-machine/guards.js +382 -0
  99. package/dist/services/task/state-machine/guards.js.map +1 -0
  100. package/dist/services/task/state-machine/index.js +53 -0
  101. package/dist/services/task/state-machine/index.js.map +1 -0
  102. package/dist/services/task/state-machine/types.js +54 -0
  103. package/dist/services/task/state-machine/types.js.map +1 -0
  104. package/dist/services/task/state-machine/uiControlsComputer.js +603 -0
  105. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  106. package/dist/services/task/taskDataNormalizer.js +99 -0
  107. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  108. package/dist/services/task/types.js +227 -4
  109. package/dist/services/task/types.js.map +1 -1
  110. package/dist/services/task/voice/Voice.js +1044 -0
  111. package/dist/services/task/voice/Voice.js.map +1 -0
  112. package/dist/services/task/voice/WebRTC.js +149 -0
  113. package/dist/services/task/voice/WebRTC.js.map +1 -0
  114. package/dist/types/cc.d.ts +894 -0
  115. package/dist/types/config.d.ts +72 -0
  116. package/dist/types/constants.d.ts +66 -0
  117. package/dist/types/index.d.ts +199 -0
  118. package/dist/types/logger-proxy.d.ts +71 -0
  119. package/dist/types/metrics/MetricsManager.d.ts +223 -0
  120. package/dist/types/metrics/behavioral-events.d.ts +29 -0
  121. package/dist/types/metrics/constants.d.ts +181 -0
  122. package/dist/types/services/AddressBook.d.ts +74 -0
  123. package/dist/types/services/ApiAiAssistant.d.ts +49 -0
  124. package/dist/types/services/EntryPoint.d.ts +67 -0
  125. package/dist/types/services/Queue.d.ts +76 -0
  126. package/dist/types/services/UserPreference.d.ts +118 -0
  127. package/dist/types/services/WebCallingService.d.ts +1 -0
  128. package/dist/types/services/agent/index.d.ts +46 -0
  129. package/dist/types/services/agent/types.d.ts +413 -0
  130. package/dist/types/services/config/Util.d.ts +20 -0
  131. package/dist/types/services/config/constants.d.ts +270 -0
  132. package/dist/types/services/config/index.d.ts +177 -0
  133. package/dist/types/services/config/types.d.ts +1368 -0
  134. package/dist/types/services/constants.d.ts +110 -0
  135. package/dist/types/services/core/Err.d.ts +125 -0
  136. package/dist/types/services/core/GlobalTypes.d.ts +58 -0
  137. package/dist/types/services/core/Utils.d.ts +121 -0
  138. package/dist/types/services/core/WebexRequest.d.ts +22 -0
  139. package/dist/types/services/core/aqm-reqs.d.ts +65 -0
  140. package/dist/types/services/core/constants.d.ts +99 -0
  141. package/dist/types/services/core/types.d.ts +47 -0
  142. package/dist/types/services/core/websocket/WebSocketManager.d.ts +36 -0
  143. package/dist/types/services/core/websocket/connection-service.d.ts +27 -0
  144. package/dist/types/services/core/websocket/keepalive.worker.d.ts +2 -0
  145. package/dist/types/services/core/websocket/types.d.ts +37 -0
  146. package/dist/types/services/index.d.ts +54 -0
  147. package/dist/types/services/task/AutoWrapup.d.ts +40 -0
  148. package/dist/types/services/task/Task.d.ts +157 -0
  149. package/dist/types/services/task/TaskFactory.d.ts +12 -0
  150. package/dist/types/services/task/TaskManager.d.ts +1 -0
  151. package/dist/types/services/task/TaskUtils.d.ts +138 -0
  152. package/dist/types/services/task/constants.d.ts +91 -0
  153. package/dist/types/services/task/contact.d.ts +69 -0
  154. package/dist/types/services/task/dialer.d.ts +73 -0
  155. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  156. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1194 -0
  157. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  158. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  159. package/dist/types/services/task/state-machine/guards.d.ts +102 -0
  160. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  161. package/dist/types/services/task/state-machine/types.d.ts +269 -0
  162. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  163. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  164. package/dist/types/services/task/types.d.ts +1856 -0
  165. package/dist/types/services/task/voice/Voice.d.ts +184 -0
  166. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  167. package/dist/types/types.d.ts +778 -0
  168. package/dist/types/utils/PageCache.d.ts +173 -0
  169. package/dist/types/webex-config.d.ts +53 -0
  170. package/dist/types/webex.d.ts +8 -0
  171. package/dist/types.js +130 -1
  172. package/dist/types.js.map +1 -1
  173. package/dist/webex.js +14 -2
  174. package/dist/webex.js.map +1 -1
  175. package/package.json +16 -12
  176. package/src/cc.ts +477 -51
  177. package/src/config.ts +6 -0
  178. package/src/constants.ts +21 -1
  179. package/src/index.ts +24 -5
  180. package/src/metrics/ai-docs/AGENTS.md +350 -0
  181. package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
  182. package/src/metrics/ai-docs/metrics-spec.md +854 -0
  183. package/src/metrics/behavioral-events.ts +120 -0
  184. package/src/metrics/constants.ts +37 -3
  185. package/src/services/ApiAiAssistant.ts +412 -0
  186. package/src/services/UserPreference.ts +509 -0
  187. package/src/services/agent/ai-docs/AGENTS.md +240 -0
  188. package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
  189. package/src/services/agent/ai-docs/agent-spec.md +504 -0
  190. package/src/services/agent/types.ts +1 -1
  191. package/src/services/ai-docs/AGENTS.md +386 -0
  192. package/src/services/ai-docs/services-spec.md +492 -0
  193. package/src/services/config/Util.ts +10 -2
  194. package/src/services/config/ai-docs/AGENTS.md +255 -0
  195. package/src/services/config/ai-docs/ARCHITECTURE.md +426 -0
  196. package/src/services/config/ai-docs/config-spec.md +669 -0
  197. package/src/services/config/constants.ts +37 -1
  198. package/src/services/config/index.ts +45 -1
  199. package/src/services/config/types.ts +241 -11
  200. package/src/services/constants.ts +29 -0
  201. package/src/services/core/Err.ts +3 -0
  202. package/src/services/core/Utils.ts +143 -30
  203. package/src/services/core/ai-docs/AGENTS.md +381 -0
  204. package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
  205. package/src/services/core/ai-docs/core-spec.md +783 -0
  206. package/src/services/core/aqm-reqs.ts +100 -22
  207. package/src/services/core/websocket/WebSocketManager.ts +23 -6
  208. package/src/services/core/websocket/connection-service.ts +5 -1
  209. package/src/services/core/websocket/types.ts +1 -1
  210. package/src/services/index.ts +4 -0
  211. package/src/services/task/Task.ts +837 -0
  212. package/src/services/task/TaskFactory.ts +55 -0
  213. package/src/services/task/TaskManager.ts +793 -521
  214. package/src/services/task/TaskUtils.ts +314 -24
  215. package/src/services/task/ai-docs/AGENTS.md +457 -0
  216. package/src/services/task/ai-docs/ARCHITECTURE.md +594 -0
  217. package/src/services/task/ai-docs/task-spec.md +1319 -0
  218. package/src/services/task/constants.ts +23 -0
  219. package/src/services/task/dialer.ts +136 -1
  220. package/src/services/task/digital/Digital.ts +95 -0
  221. package/src/services/task/state-machine/TaskStateMachine.ts +1166 -0
  222. package/src/services/task/state-machine/actions.ts +738 -0
  223. package/src/services/task/state-machine/ai-docs/AGENTS.md +458 -0
  224. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1137 -0
  225. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2177 -0
  226. package/src/services/task/state-machine/constants.ts +172 -0
  227. package/src/services/task/state-machine/guards.ts +445 -0
  228. package/src/services/task/state-machine/index.ts +28 -0
  229. package/src/services/task/state-machine/types.ts +243 -0
  230. package/src/services/task/state-machine/uiControlsComputer.ts +961 -0
  231. package/src/services/task/taskDataNormalizer.ts +137 -0
  232. package/src/services/task/types.ts +734 -71
  233. package/src/services/task/voice/Voice.ts +1270 -0
  234. package/src/services/task/voice/WebRTC.ts +187 -0
  235. package/src/types.ts +205 -2
  236. package/src/utils/AGENTS.md +278 -0
  237. package/src/utils/ai-docs/utils-spec.md +381 -0
  238. package/src/webex.js +2 -0
  239. package/test/unit/spec/cc.ts +503 -43
  240. package/test/unit/spec/logger-proxy.ts +70 -0
  241. package/test/unit/spec/services/ApiAiAssistant.ts +273 -0
  242. package/test/unit/spec/services/UserPreference.ts +401 -0
  243. package/test/unit/spec/services/WebCallingService.ts +7 -1
  244. package/test/unit/spec/services/config/index.ts +85 -29
  245. package/test/unit/spec/services/core/Utils.ts +481 -2
  246. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +137 -41
  247. package/test/unit/spec/services/core/websocket/connection-service.ts +3 -1
  248. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  249. package/test/unit/spec/services/task/Task.ts +477 -0
  250. package/test/unit/spec/services/task/TaskFactory.ts +62 -0
  251. package/test/unit/spec/services/task/TaskManager.ts +1001 -1003
  252. package/test/unit/spec/services/task/TaskUtils.ts +235 -0
  253. package/test/unit/spec/services/task/dialer.ts +372 -96
  254. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  255. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +2651 -0
  256. package/test/unit/spec/services/task/state-machine/guards.ts +637 -0
  257. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  258. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +2663 -0
  259. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  260. package/test/unit/spec/services/task/voice/Voice.ts +649 -0
  261. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  262. package/umd/contact-center.min.js +2 -2
  263. package/umd/contact-center.min.js.map +1 -1
  264. package/dist/services/task/index.js +0 -1525
  265. package/dist/services/task/index.js.map +0 -1
  266. package/src/services/task/index.ts +0 -1801
  267. package/test/unit/spec/services/task/index.ts +0 -2184
@@ -5,15 +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;
20
+ let mockApiAIAssistant;
16
21
  let webSocketManagerMock;
22
+ let rtdWebSocketManagerMock;
17
23
  let onSpy;
18
24
  let offSpy;
19
25
  let taskManager;
@@ -23,6 +29,108 @@ describe('TaskManager', () => {
23
29
  let webex: WebexSDK;
24
30
  const taskId = '0ae913a4-c857-4705-8d49-76dd3dde75e4';
25
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
+
26
134
  taskDataMock = {
27
135
  type: CC_EVENTS.AGENT_CONTACT_RESERVED,
28
136
  agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
@@ -42,9 +150,21 @@ describe('TaskManager', () => {
42
150
  data: taskDataMock,
43
151
  };
44
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
+
45
164
  beforeEach(() => {
46
165
  contactMock = contact;
47
166
  webSocketManagerMock = new EventEmitter();
167
+ rtdWebSocketManagerMock = new EventEmitter();
48
168
 
49
169
  webex = {
50
170
  logger: {
@@ -74,15 +194,25 @@ describe('TaskManager', () => {
74
194
  onSpy = jest.spyOn(webCallingService, 'on');
75
195
  offSpy = jest.spyOn(webCallingService, 'off');
76
196
 
77
- taskManager = new TaskManager(contactMock, webCallingService, webSocketManagerMock);
78
- taskManager.taskCollection[taskId] = {
79
- emit: jest.fn(),
80
- accept: jest.fn(),
81
- decline: jest.fn(),
82
- updateTaskData: jest.fn(),
83
- data: taskDataMock,
197
+ mockApiAIAssistant = {
198
+ sendEvent: jest.fn().mockResolvedValue({}),
84
199
  };
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]);
85
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));
86
216
  });
87
217
 
88
218
  afterEach(() => {
@@ -93,7 +223,8 @@ describe('TaskManager', () => {
93
223
  it('should initialize TaskManager and register listeners', () => {
94
224
  webSocketManagerMock.emit('message', JSON.stringify({data: taskDataMock}));
95
225
  const incomingCallCb = onSpy.mock.calls[0][1];
96
- const taskEmitSpy = jest.spyOn(taskManager, 'emit');
226
+ const incomingHandler = jest.fn();
227
+ taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
97
228
 
98
229
  expect(taskManager).toBeInstanceOf(TaskManager);
99
230
  expect(webCallingService.listenerCount(LINE_EVENTS.INCOMING_CALL)).toBe(1);
@@ -102,14 +233,13 @@ describe('TaskManager', () => {
102
233
 
103
234
  incomingCallCb(mockCall);
104
235
 
105
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, taskManager.getTask(taskId));
236
+ expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(taskId));
237
+ taskManager.off(TASK_EVENTS.TASK_INCOMING, incomingHandler);
106
238
  });
107
239
 
108
240
  it('should re-emit task related events', () => {
109
241
  const dummyPayload = {
110
- data: {...taskDataMock,
111
- type: CC_TASK_EVENTS.AGENT_CONSULTING,
112
- },
242
+ data: {...taskDataMock, type: CC_TASK_EVENTS.AGENT_CONSULTING},
113
243
  };
114
244
  webSocketManagerMock.emit('message', JSON.stringify({data: taskDataMock}));
115
245
  const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
@@ -120,7 +250,186 @@ describe('TaskManager', () => {
120
250
 
121
251
  webSocketManagerMock.emit('message', JSON.stringify(dummyPayload));
122
252
 
123
- expect(taskEmitSpy).toHaveBeenCalledWith(dummyPayload.data.type, dummyPayload.data);
253
+ expect(taskEmitSpy).toHaveBeenCalledWith(
254
+ TASK_EVENTS.TASK_CONSULT_ACCEPTED,
255
+ taskManager.getTask(taskId)
256
+ );
257
+ });
258
+
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;
274
+ const message = (type: CC_EVENTS) =>
275
+ JSON.stringify({
276
+ data: {
277
+ ...taskDataMock,
278
+ interactionId,
279
+ type,
280
+ },
281
+ });
282
+
283
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONTACT_ASSIGNED));
284
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
285
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_CONFERENCED));
286
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULT_ENDED));
287
+ webSocketManagerMock.emit('message', message(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE));
288
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_WRAPUP));
289
+
290
+ expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledTimes(6);
291
+ expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledWith(
292
+ 'test-agent-id',
293
+ interactionId,
294
+ 'CUSTOM_EVENT',
295
+ 'GET_TRANSCRIPTS',
296
+ {action: 'START'}
297
+ );
298
+ expect(mockApiAIAssistant.sendEvent).toHaveBeenCalledWith(
299
+ 'test-agent-id',
300
+ interactionId,
301
+ 'CUSTOM_EVENT',
302
+ 'GET_TRANSCRIPTS',
303
+ {action: 'STOP'}
304
+ );
305
+ });
306
+
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
+ },
318
+ },
319
+ });
320
+
321
+ const message = (type: CC_EVENTS) =>
322
+ JSON.stringify({
323
+ data: {
324
+ ...taskDataMock,
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,
355
+ type,
356
+ },
357
+ });
358
+
359
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONTACT_ASSIGNED));
360
+ webSocketManagerMock.emit('message', message(CC_EVENTS.AGENT_CONSULTING));
361
+
362
+ expect(mockApiAIAssistant.sendEvent).not.toHaveBeenCalled();
363
+ });
364
+
365
+ it('should emit REAL_TIME_TRANSCRIPTION from RTD websocket payload', () => {
366
+ const task = taskManager.getTask(taskId);
367
+ const taskEmitSpy = jest.spyOn(task, 'emit');
368
+ const realtimePayload = {
369
+ data: {
370
+ agentId: 'test-agent-id',
371
+ data: {
372
+ content: 'Thank you. Okay.',
373
+ conversationId: taskId,
374
+ isFinal: true,
375
+ languageCode: 'en-US',
376
+ messageId: '1',
377
+ orgId: 'org-id',
378
+ publishTimestamp: 1773807297475,
379
+ role: 'AGENT',
380
+ trackingId: 'tracking-id',
381
+ utteranceId: 'utterance-id',
382
+ },
383
+ notifDetails: {
384
+ actionEvent: 'REAL_TIME_TRANSCRIPTION',
385
+ },
386
+ notifType: 'REAL_TIME_TRANSCRIPTION',
387
+ orgId: 'org-id',
388
+ },
389
+ orgId: 'org-id',
390
+ trackingId: 'notifs_tracking-id',
391
+ type: 'REAL_TIME_TRANSCRIPTION',
392
+ };
393
+
394
+ taskManager.handleRealtimeWebsocketEvent(JSON.stringify(realtimePayload));
395
+
396
+ expect(taskEmitSpy).toHaveBeenCalledWith(
397
+ CC_EVENTS.REAL_TIME_TRANSCRIPTION,
398
+ realtimePayload.data
399
+ );
400
+ });
401
+
402
+ it('should ignore RTD transcript events when task is not found', () => {
403
+ const realtimePayload = {
404
+ data: {
405
+ data: {
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',
419
+ },
420
+ notifType: 'REAL_TIME_TRANSCRIPTION',
421
+ orgId: 'org-id',
422
+ },
423
+ orgId: 'org-id',
424
+ trackingId: 'notifs_tracking-id',
425
+ type: 'REAL_TIME_TRANSCRIPTION',
426
+ };
427
+
428
+ const existingTaskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
429
+
430
+ taskManager.handleRealtimeWebsocketEvent(JSON.stringify(realtimePayload));
431
+
432
+ expect(existingTaskEmitSpy).not.toHaveBeenCalled();
124
433
  });
125
434
 
126
435
  it('should not re-emit agent related events', () => {
@@ -138,7 +447,7 @@ describe('TaskManager', () => {
138
447
 
139
448
  webSocketManagerMock.emit('message', JSON.stringify(dummyPayload));
140
449
 
141
- expect(taskEmitSpy).not.toHaveBeenCalledWith(dummyPayload.data.type, dummyPayload.data);
450
+ expect(taskEmitSpy).not.toHaveBeenCalled();
142
451
  });
143
452
 
144
453
  it('should handle WebSocket message for AGENT_CONTACT_RESERVED and emit task:incoming for browser case', () => {
@@ -159,14 +468,12 @@ describe('TaskManager', () => {
159
468
  },
160
469
  };
161
470
 
162
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
471
+ const incomingHandler = jest.fn();
472
+ taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
163
473
 
164
474
  webSocketManagerMock.emit('message', JSON.stringify(payload));
165
475
 
166
- expect(taskIncomingSpy).toHaveBeenCalledWith(
167
- TASK_EVENTS.TASK_INCOMING,
168
- taskManager.getTask(payload.data.interactionId)
169
- );
476
+ expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(payload.data.interactionId));
170
477
  expect(taskManager.getTask(payload.data.interactionId)).toBe(taskManager.getTask(taskId));
171
478
  expect(taskManager.getAllTasks()).toHaveProperty(payload.data.interactionId);
172
479
 
@@ -187,7 +494,11 @@ describe('TaskManager', () => {
187
494
  },
188
495
  };
189
496
 
190
- const currentTaskAssignedSpy = jest.spyOn(taskManager.getTask(payload.data.interactionId), 'emit');
497
+ const currentTaskAssignedSpy = jest.spyOn(
498
+ taskManager.getTask(payload.data.interactionId),
499
+ 'emit'
500
+ );
501
+ const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
191
502
 
192
503
  webSocketManagerMock.emit('message', JSON.stringify(assignedPayload));
193
504
 
@@ -195,6 +506,10 @@ describe('TaskManager', () => {
195
506
  TASK_EVENTS.TASK_ASSIGNED,
196
507
  taskManager.getTask(taskId)
197
508
  );
509
+ expect(taskManagerEmitSpy).toHaveBeenCalledWith(
510
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
511
+ taskManager.getTask(taskId)
512
+ );
198
513
  });
199
514
 
200
515
  it('should handle WebSocket message for AGENT_CONTACT_RESERVED and emit task:incoming for extension case', () => {
@@ -216,16 +531,64 @@ describe('TaskManager', () => {
216
531
  },
217
532
  };
218
533
 
219
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
534
+ const incomingHandler = jest.fn();
535
+ taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
220
536
 
221
537
  webSocketManagerMock.emit('message', JSON.stringify(payload));
222
538
 
223
- expect(taskIncomingSpy).toHaveBeenCalledWith(
224
- TASK_EVENTS.TASK_INCOMING,
225
- taskManager.getTask(taskId)
226
- );
539
+ expect(incomingHandler).toHaveBeenCalledWith(taskManager.getTask(taskId));
227
540
  expect(taskManager.getTask(payload.data.interactionId)).toBe(taskManager.getTask(taskId));
228
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);
229
592
  });
230
593
 
231
594
  it('should return task by ID', () => {
@@ -310,9 +673,30 @@ describe('TaskManager', () => {
310
673
  it('test call listeners being switched off on call end', () => {
311
674
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
312
675
 
313
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
314
- const webCallListenerSpy = jest.spyOn(taskManager.getTask(taskId), 'unregisterWebCallListeners');
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');
315
698
  const callOffSpy = jest.spyOn(mockCall, 'off');
699
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
316
700
  const payload = {
317
701
  data: {
318
702
  type: CC_EVENTS.CONTACT_ENDED,
@@ -330,12 +714,25 @@ describe('TaskManager', () => {
330
714
  },
331
715
  };
332
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
+
333
729
  taskManager.getTask(taskId).data = payload.data;
334
- const task = taskManager.getTask(taskId)
335
730
  webSocketManagerMock.emit('message', JSON.stringify(payload));
336
- expect(taskEmitSpy).toHaveBeenCalledWith(
337
- TASK_EVENTS.TASK_END, task
731
+ const stateMachineEvent = expectLastStateMachineEvent(
732
+ sendStateMachineEventSpy,
733
+ TaskEvent.CONTACT_ENDED
338
734
  );
735
+ expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(false);
339
736
  expect(webCallListenerSpy).toHaveBeenCalledWith();
340
737
  expect(callOffSpy).toHaveBeenCalledWith(
341
738
  CALL_EVENT_KEYS.REMOTE_MEDIA,
@@ -346,12 +743,13 @@ describe('TaskManager', () => {
346
743
  expect(offSpy.mock.calls.length).toBe(2); // 1 for incoming call and 1 for remote media
347
744
  expect(offSpy).toHaveBeenCalledWith(CALL_EVENT_KEYS.REMOTE_MEDIA, offSpy.mock.calls[0][1]);
348
745
  expect(offSpy).toHaveBeenCalledWith(LINE_EVENTS.INCOMING_CALL, offSpy.mock.calls[1][1]);
746
+ sendStateMachineEventSpy.mockRestore();
349
747
  });
350
748
 
351
749
  it('should emit TASK_END event with wrapupRequired on regular call end', () => {
352
750
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
353
-
354
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
751
+ const task = taskManager.getTask(taskId);
752
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
355
753
  const payload = {
356
754
  data: {
357
755
  type: CC_EVENTS.CONTACT_ENDED,
@@ -366,80 +764,77 @@ describe('TaskManager', () => {
366
764
  destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
367
765
  owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
368
766
  queueMgr: 'aqm',
767
+ agentsPendingWrapUp: ['test-agent-id'],
369
768
  },
370
769
  };
371
770
 
372
- taskManager.getTask(taskId).updateTaskData(payload.data);
771
+ task.updateTaskData(payload.data);
373
772
  webSocketManagerMock.emit('message', JSON.stringify(payload));
374
- expect(taskEmitSpy).toHaveBeenCalledWith(
375
- CC_EVENTS.CONTACT_ENDED,
376
- { ...payload.data}
377
- );
378
- expect(taskEmitSpy).toHaveBeenCalledWith(
379
- TASK_EVENTS.TASK_END,
380
- taskManager.getTask(taskId)
773
+ const stateMachineEvent = expectLastStateMachineEvent(
774
+ sendStateMachineEventSpy,
775
+ TaskEvent.CONTACT_ENDED
381
776
  );
777
+ expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(true);
778
+ sendStateMachineEventSpy.mockRestore();
382
779
  });
383
780
 
384
781
  it('should emit TASK_REJECT event on AGENT_INVITE_FAILED event', () => {
385
782
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
386
783
 
387
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
388
- const metricsTrackSpy = jest.spyOn(taskManager.metricsManager, 'trackEvent');
389
- const payload = {
390
- data: {
391
- type: CC_EVENTS.AGENT_INVITE_FAILED,
392
- agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
393
- eventTime: 1733211616959,
394
- eventType: 'RoutingMessage',
395
- interaction: {state: 'connected'},
396
- interactionId: taskId,
397
- orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
398
- trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
399
- mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
400
- destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
401
- owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
402
- queueMgr: 'aqm',
403
- reason: 'INVITE_FAILED',
404
- },
405
- };
784
+ const task = taskManager.getTask(taskId);
785
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
786
+ const payload = {
787
+ data: {
788
+ type: CC_EVENTS.AGENT_INVITE_FAILED,
789
+ agentId: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
790
+ eventTime: 1733211616959,
791
+ eventType: 'RoutingMessage',
792
+ interaction: {state: 'connected'},
793
+ interactionId: taskId,
794
+ orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
795
+ trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
796
+ mediaResourceId: '0ae913a4-c857-4705-8d49-76dd3dde75e4',
797
+ destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
798
+ owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
799
+ queueMgr: 'aqm',
800
+ reason: 'INVITE_FAILED',
801
+ },
802
+ };
406
803
 
407
- taskManager.getTask(taskId).updateTaskData(payload.data);
408
- webSocketManagerMock.emit('message', JSON.stringify(payload));
409
- expect(taskEmitSpy).toHaveBeenCalledWith(
410
- CC_EVENTS.AGENT_INVITE_FAILED,
411
- { ...payload.data}
412
- );
413
- expect(taskEmitSpy).toHaveBeenCalledWith(
414
- TASK_EVENTS.TASK_REJECT,
415
- payload.data.reason
416
- );
417
- // Verify the correct metric event name is used for AGENT_INVITE_FAILED
418
- expect(metricsTrackSpy).toHaveBeenCalled();
419
- expect(metricsTrackSpy.mock.calls[0][0]).toBe('Agent Invite Failed');
804
+ task.updateTaskData(payload.data);
805
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
806
+ const stateMachineEvent = expectLastStateMachineEvent(
807
+ sendStateMachineEventSpy,
808
+ TaskEvent.INVITE_FAILED
809
+ );
810
+ expect(stateMachineEvent?.reason).toBe(payload.data.reason);
811
+ sendStateMachineEventSpy.mockRestore();
420
812
  });
421
813
 
422
-
423
- 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', () => {
424
815
  const payload = {
425
816
  data: {
426
817
  ...initalPayload.data,
427
818
  type: CC_EVENTS.AGENT_CONTACT,
428
819
  },
429
820
  };
430
- const taskEmitSpy = jest.spyOn(taskManager, 'emit');
821
+ const existingTask = taskManager.getTask(taskId);
822
+ const sendStateMachineEventSpy = jest.spyOn(existingTask, 'sendStateMachineEvent');
431
823
  webSocketManagerMock.emit('message', JSON.stringify(payload));
432
824
 
433
- expect(taskEmitSpy).not.toHaveBeenCalledWith(
434
- TASK_EVENTS.TASK_HYDRATE,
435
- taskManager.getTask(taskId)
825
+ const stateMachineEvent = expectLastStateMachineEvent(
826
+ sendStateMachineEventSpy,
827
+ TaskEvent.HYDRATE
436
828
  );
829
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
830
+ expect(existingTask).toBe(taskManager.getTask(taskId));
437
831
  expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
438
832
  taskManager.getTask(taskId)
439
833
  );
834
+ sendStateMachineEventSpy.mockRestore();
440
835
  });
441
836
 
442
- 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', () => {
443
838
  taskManager.taskCollection = [];
444
839
  const payload = {
445
840
  data: {
@@ -449,13 +844,15 @@ describe('TaskManager', () => {
449
844
  },
450
845
  };
451
846
 
452
- const taskEmitSpy = jest.spyOn(taskManager, 'emit');
453
847
  webSocketManagerMock.emit('message', JSON.stringify(payload));
454
848
 
455
- expect(taskEmitSpy).toHaveBeenCalledWith(
456
- TASK_EVENTS.TASK_INCOMING,
457
- 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
458
854
  );
855
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
459
856
  expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
460
857
  taskManager.getTask(taskId)
461
858
  );
@@ -470,10 +867,15 @@ describe('TaskManager', () => {
470
867
  },
471
868
  };
472
869
 
473
- const taskEmitSpy = jest.spyOn(taskManager, 'emit');
474
870
  webSocketManagerMock.emit('message', JSON.stringify(payload));
475
871
 
476
- 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);
477
879
  expect(taskManager.taskCollection[payload.data.interactionId]).toBe(
478
880
  taskManager.getTask(taskId)
479
881
  );
@@ -483,7 +885,7 @@ describe('TaskManager', () => {
483
885
  const testAgentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
484
886
  taskManager.setAgentId(testAgentId);
485
887
  taskManager.taskCollection = [];
486
-
888
+
487
889
  const payload = {
488
890
  data: {
489
891
  ...initalPayload.data,
@@ -492,9 +894,9 @@ describe('TaskManager', () => {
492
894
  mediaType: 'telephony',
493
895
  state: 'conference',
494
896
  participants: {
495
- [testAgentId]: { pType: 'Agent', hasLeft: false },
496
- 'agent-2': { pType: 'Agent', hasLeft: false },
497
- 'customer-1': { pType: 'Customer', hasLeft: false },
897
+ [testAgentId]: {pType: 'Agent', hasLeft: false},
898
+ 'agent-2': {pType: 'Agent', hasLeft: false},
899
+ 'customer-1': {pType: 'Customer', hasLeft: false},
498
900
  },
499
901
  media: {
500
902
  [taskId]: {
@@ -517,7 +919,7 @@ describe('TaskManager', () => {
517
919
  const testAgentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
518
920
  taskManager.setAgentId(testAgentId);
519
921
  taskManager.taskCollection = [];
520
-
922
+
521
923
  const payload = {
522
924
  data: {
523
925
  ...initalPayload.data,
@@ -526,8 +928,8 @@ describe('TaskManager', () => {
526
928
  mediaType: 'telephony',
527
929
  state: 'connected',
528
930
  participants: {
529
- [testAgentId]: { pType: 'Agent', hasLeft: false },
530
- 'customer-1': { pType: 'Customer', hasLeft: false },
931
+ [testAgentId]: {pType: 'Agent', hasLeft: false},
932
+ 'customer-1': {pType: 'Customer', hasLeft: false},
531
933
  },
532
934
  media: {
533
935
  [taskId]: {
@@ -546,7 +948,7 @@ describe('TaskManager', () => {
546
948
  expect(createdTask.data.isConferenceInProgress).toBe(false);
547
949
  });
548
950
 
549
- it('should emit TASK_END event on AGENT_WRAPUP event', () => {
951
+ it('should emit TASK_WRAPUP event on AGENT_WRAPUP event', () => {
550
952
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
551
953
 
552
954
  const wrapupPayload = {
@@ -563,18 +965,20 @@ describe('TaskManager', () => {
563
965
  destAgentId: 'ebeb893b-ba67-4f36-8418-95c7492b28c2',
564
966
  owner: '723a8ffb-a26e-496d-b14a-ff44fb83b64f',
565
967
  queueMgr: 'aqm',
566
- wrapUpRequired: true
968
+ wrapUpRequired: true,
567
969
  },
568
970
  };
569
971
 
570
972
  const task = taskManager.getTask(taskId);
571
- const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
572
- const taskEmitSpy = jest.spyOn(task, 'emit');
973
+ const updateTaskDataSpy = task.updateTaskData as jest.Mock;
974
+ updateTaskDataSpy.mockClear();
975
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
573
976
 
574
977
  webSocketManagerMock.emit('message', JSON.stringify(wrapupPayload));
575
978
 
576
979
  expect(updateTaskDataSpy).toHaveBeenCalledWith(wrapupPayload.data);
577
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
980
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TASK_WRAPUP);
981
+ sendStateMachineEventSpy.mockRestore();
578
982
  });
579
983
 
580
984
  it('should emit TASK_HOLD event on AGENT_CONTACT_HELD event', () => {
@@ -597,13 +1001,20 @@ describe('TaskManager', () => {
597
1001
  },
598
1002
  };
599
1003
 
600
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
601
- 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');
602
1008
 
603
1009
  webSocketManagerMock.emit('message', JSON.stringify(payload));
604
1010
 
605
1011
  expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
606
- 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();
607
1018
  });
608
1019
 
609
1020
  it('should emit TASK_RESUME event on AGENT_CONTACT_UNHELD event', () => {
@@ -626,11 +1037,18 @@ describe('TaskManager', () => {
626
1037
  },
627
1038
  };
628
1039
 
629
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
630
- 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');
631
1044
  webSocketManagerMock.emit('message', JSON.stringify(payload));
632
1045
  expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
633
- 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();
634
1052
  });
635
1053
 
636
1054
  it('handle AGENT_CONSULT_CREATED event', () => {
@@ -643,17 +1061,16 @@ describe('TaskManager', () => {
643
1061
 
644
1062
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
645
1063
  const task = taskManager.getTask(taskId);
646
- const taskUpdateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
647
- const taskEmitSpy = jest.spyOn(task, 'emit');
1064
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
648
1065
 
649
1066
  webSocketManagerMock.emit('message', JSON.stringify(payload));
650
1067
 
651
- expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith({
652
- ...payload.data,
653
- isConsulted: false,
654
- });
655
- expect(task.data.isConsulted).toBe(false);
656
- 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();
657
1074
  });
658
1075
 
659
1076
  it('handle AGENT_OFFER_CONTACT event', () => {
@@ -666,11 +1083,17 @@ describe('TaskManager', () => {
666
1083
 
667
1084
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
668
1085
 
669
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1086
+ const task = taskManager.getTask(taskId);
1087
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
670
1088
 
671
1089
  webSocketManagerMock.emit('message', JSON.stringify(payload));
672
1090
 
673
- 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();
674
1097
  });
675
1098
 
676
1099
  describe('Auto-Answer Functionality', () => {
@@ -680,8 +1103,8 @@ describe('TaskManager', () => {
680
1103
 
681
1104
  const task = taskManager.getTask(taskId);
682
1105
  const taskEmitSpy = jest.spyOn(task, 'emit');
683
- const taskManagerEmitSpy = jest.spyOn(taskManager, 'emit');
684
1106
  const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
1107
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
685
1108
 
686
1109
  // Step 2: Trigger AGENT_OFFER_CONTACT with auto-answer
687
1110
  const autoAnswerPayload = {
@@ -705,9 +1128,14 @@ describe('TaskManager', () => {
705
1128
  // Verify accept was called
706
1129
  expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
707
1130
 
708
- // Verify BOTH events were emitted
709
- 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
710
1137
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
1138
+ sendStateMachineEventSpy.mockRestore();
711
1139
  });
712
1140
 
713
1141
  it('should NOT emit TASK_AUTO_ANSWERED event when auto-answer fails', async () => {
@@ -716,7 +1144,9 @@ describe('TaskManager', () => {
716
1144
 
717
1145
  const task = taskManager.getTask(taskId);
718
1146
  const taskEmitSpy = jest.spyOn(task, 'emit');
719
- const taskAcceptSpy = jest.spyOn(task, 'accept').mockRejectedValue(new Error('Accept failed'));
1147
+ const taskAcceptSpy = jest
1148
+ .spyOn(task, 'accept')
1149
+ .mockRejectedValue(new Error('Accept failed'));
720
1150
 
721
1151
  // Step 2: Trigger AGENT_OFFER_CONTACT with auto-answer (will fail)
722
1152
  const autoAnswerPayload = {
@@ -751,6 +1181,7 @@ describe('TaskManager', () => {
751
1181
  const task = taskManager.getTask(taskId);
752
1182
  const taskEmitSpy = jest.spyOn(task, 'emit');
753
1183
  const taskAcceptSpy = jest.spyOn(task, 'accept').mockResolvedValue(undefined);
1184
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
754
1185
 
755
1186
  // Step 2: Trigger AGENT_OFFER_CONSULT with auto-answer
756
1187
  const consultAutoAnswerPayload = {
@@ -775,12 +1206,20 @@ describe('TaskManager', () => {
775
1206
  // Verify accept was called
776
1207
  expect(taskAcceptSpy).toHaveBeenCalledTimes(1);
777
1208
 
778
- // Verify BOTH events were emitted
779
- 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
780
1218
  expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, task);
781
-
1219
+
782
1220
  // Verify isConsulted flag is set correctly
783
1221
  expect(task.data.isConsulted).toBe(true);
1222
+ sendStateMachineEventSpy.mockRestore();
784
1223
  });
785
1224
 
786
1225
  it('should NOT emit TASK_AUTO_ANSWERED when isAutoAnswering is false', async () => {
@@ -814,28 +1253,27 @@ describe('TaskManager', () => {
814
1253
  expect(taskAcceptSpy).not.toHaveBeenCalled();
815
1254
 
816
1255
  // Verify TASK_AUTO_ANSWERED event was NOT emitted
817
- expect(taskEmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_AUTO_ANSWERED, expect.anything());
1256
+ expect(taskEmitSpy).not.toHaveBeenCalledWith(
1257
+ TASK_EVENTS.TASK_AUTO_ANSWERED,
1258
+ expect.anything()
1259
+ );
818
1260
  });
819
1261
  });
820
1262
 
821
- 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', () => {
822
1264
  const task = taskManager.getTask(taskId);
823
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
824
- task.data = {
825
- ...task.data,
826
- ...newData,
827
- interaction: {
828
- ...task.data.interaction,
829
- ...newData.interaction,
830
- outboundType: 'OUTDIAL',
831
- state: 'new',
832
- isTerminated: true,
833
- },
834
- };
835
- 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'],
836
1273
  });
837
1274
  task.unregisterWebCallListeners = jest.fn();
838
1275
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
1276
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
839
1277
 
840
1278
  const payload = {
841
1279
  data: {
@@ -848,6 +1286,7 @@ describe('TaskManager', () => {
848
1286
  state: 'new',
849
1287
  isTerminated: true,
850
1288
  },
1289
+ agentsPendingWrapUp: ['agent-123'],
851
1290
  interactionId: taskId,
852
1291
  orgId: '6ecef209-9a34-4ed1-a07a-7ddd1dbe925a',
853
1292
  trackingId: '575c0ec2-618c-42af-a61c-53aeb0a221ee',
@@ -862,23 +1301,61 @@ describe('TaskManager', () => {
862
1301
 
863
1302
  webSocketManagerMock.emit('message', JSON.stringify(payload));
864
1303
 
865
- expect(taskManager.getTask(taskId)).toBeDefined();
866
- 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();
867
1314
  });
868
1315
 
869
1316
  it('should emit TASK_OUTDIAL_FAILED event on AGENT_OUTBOUND_FAILED', () => {
870
1317
  const task = taskManager.getTask(taskId);
871
- task.updateTaskData = jest.fn().mockReturnValue(task);
872
- 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');
873
1338
  const payload = {
874
1339
  data: {
875
1340
  type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
876
1341
  interactionId: taskId,
877
1342
  reason: 'CUSTOMER_BUSY',
1343
+ agentsPendingWrapUp: ['agent-123'],
1344
+ interaction: {
1345
+ outboundType: 'OUTDIAL',
1346
+ isTerminated: true,
1347
+ },
878
1348
  },
879
1349
  };
880
1350
  webSocketManagerMock.emit('message', JSON.stringify(payload));
881
- 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();
882
1359
  });
883
1360
 
884
1361
  it('should handle AGENT_OUTBOUND_FAILED gracefully when task is undefined', () => {
@@ -897,19 +1374,14 @@ describe('TaskManager', () => {
897
1374
 
898
1375
  it('should NOT remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp exists', () => {
899
1376
  const task = taskManager.getTask(taskId);
900
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
901
- task.data = {
902
- ...task.data,
903
- ...newData,
904
- interaction: {
905
- ...task.data.interaction,
906
- outboundType: 'OUTDIAL',
907
- state: 'new',
908
- mediaType: 'telephony',
909
- },
910
- agentsPendingWrapUp: ['agent-123'],
911
- };
912
- 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'],
913
1385
  });
914
1386
  task.unregisterWebCallListeners = jest.fn();
915
1387
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -923,7 +1395,7 @@ describe('TaskManager', () => {
923
1395
  state: 'new',
924
1396
  mediaType: 'telephony',
925
1397
  },
926
- agentsPendingWrapUp: ['agent-123'],
1398
+ agentsPendingWrapUp: ['test-agent-id'],
927
1399
  },
928
1400
  };
929
1401
 
@@ -935,19 +1407,14 @@ describe('TaskManager', () => {
935
1407
 
936
1408
  it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is empty', () => {
937
1409
  const task = taskManager.getTask(taskId);
938
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
939
- task.data = {
940
- ...task.data,
941
- ...newData,
942
- interaction: {
943
- ...task.data.interaction,
944
- outboundType: 'OUTDIAL',
945
- state: 'new',
946
- mediaType: 'telephony',
947
- },
948
- agentsPendingWrapUp: [],
949
- };
950
- 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: [],
951
1418
  });
952
1419
  task.unregisterWebCallListeners = jest.fn();
953
1420
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -972,19 +1439,13 @@ describe('TaskManager', () => {
972
1439
 
973
1440
  it('should remove OUTDIAL task on CONTACT_ENDED when agentsPendingWrapUp is undefined', () => {
974
1441
  const task = taskManager.getTask(taskId);
975
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
976
- task.data = {
977
- ...task.data,
978
- ...newData,
979
- interaction: {
980
- ...task.data.interaction,
981
- outboundType: 'OUTDIAL',
982
- state: 'new',
983
- mediaType: 'telephony',
984
- },
985
- // agentsPendingWrapUp is undefined
986
- };
987
- return task;
1442
+ Object.assign(task.data, {
1443
+ interaction: {
1444
+ ...task.data.interaction,
1445
+ outboundType: 'OUTDIAL',
1446
+ state: 'new',
1447
+ mediaType: 'telephony',
1448
+ },
988
1449
  });
989
1450
  task.unregisterWebCallListeners = jest.fn();
990
1451
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -1025,19 +1486,13 @@ describe('TaskManager', () => {
1025
1486
 
1026
1487
  it('should remove OUTDIAL task from taskCollection on AGENT_CONTACT_ASSIGN_FAILED when NOT terminated (user-declined)', () => {
1027
1488
  const task = taskManager.getTask(taskId);
1028
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1029
- task.data = {
1030
- ...task.data,
1031
- ...newData,
1032
- interaction: {
1033
- ...task.data.interaction,
1034
- ...newData.interaction,
1035
- outboundType: 'OUTDIAL',
1036
- state: 'new',
1037
- isTerminated: false,
1038
- },
1039
- };
1040
- return task;
1489
+ Object.assign(task.data, {
1490
+ interaction: {
1491
+ ...task.data.interaction,
1492
+ outboundType: 'OUTDIAL',
1493
+ state: 'new',
1494
+ isTerminated: false,
1495
+ },
1041
1496
  });
1042
1497
  task.unregisterWebCallListeners = jest.fn();
1043
1498
  const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
@@ -1081,20 +1536,16 @@ describe('TaskManager', () => {
1081
1536
 
1082
1537
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1083
1538
  const task = taskManager.getTask(taskId);
1084
- task.updateTaskData = jest.fn().mockImplementation((newData) => {
1085
- task.data = {...newData, isConsulted: true};
1086
- return task;
1087
- });
1088
- const taskEmitSpy = jest.spyOn(task, 'emit');
1539
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1089
1540
 
1090
1541
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1091
1542
 
1092
- expect(task.updateTaskData).toHaveBeenCalledWith({
1093
- ...payload.data,
1094
- isConsulted: true,
1095
- });
1096
- expect(task.data.isConsulted).toBe(true);
1097
- 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();
1098
1549
  });
1099
1550
 
1100
1551
  it('should emit TASK_CONSULT_ACCEPTED event on AGENT_CONSULTING event', () => {
@@ -1113,19 +1564,18 @@ describe('TaskManager', () => {
1113
1564
  };
1114
1565
 
1115
1566
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1116
- taskManager.getTask(taskId).updateTaskData = jest.fn().mockImplementation((newData) => {
1117
- taskManager.getTask(taskId).data = {...newData, isConsulted: true};
1118
- return taskManager.getTask(taskId);
1119
- });
1120
1567
 
1121
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1568
+ const task = taskManager.getTask(taskId);
1569
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1570
+
1122
1571
  webSocketManagerMock.emit('message', JSON.stringify(initialConsultingPayload));
1123
1572
  webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
1124
- expect(taskManager.getTask(taskId).data.isConsulted).toBe(true);
1125
- expect(taskEmitSpy).toHaveBeenCalledWith(
1126
- TASK_EVENTS.TASK_CONSULT_ACCEPTED,
1127
- taskManager.getTask(taskId)
1573
+ const stateMachineEvent = expectLastStateMachineEvent(
1574
+ sendStateMachineEventSpy,
1575
+ TaskEvent.CONSULTING_ACTIVE
1128
1576
  );
1577
+ expect(stateMachineEvent?.taskData).toEqual(consultingPayload.data);
1578
+ sendStateMachineEventSpy.mockRestore();
1129
1579
  });
1130
1580
 
1131
1581
  it('should emit TASK_CONSULT_ENDED event on AGENT_CONSULT_ENDED event', () => {
@@ -1137,11 +1587,16 @@ describe('TaskManager', () => {
1137
1587
  };
1138
1588
 
1139
1589
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1140
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1141
- 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');
1142
1593
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1143
- expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
1144
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONSULT_END, taskManager.getTask(taskId));
1594
+ const stateMachineEvent = expectLastStateMachineEvent(
1595
+ sendStateMachineEventSpy,
1596
+ TaskEvent.CONSULT_END
1597
+ );
1598
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1599
+ sendStateMachineEventSpy.mockRestore();
1145
1600
  });
1146
1601
 
1147
1602
  it('should emit TASK_CONSULT_ENDED event and remove currentTask when on AGENT_CONSULT_ENDED event when requested for a consult', () => {
@@ -1154,19 +1609,14 @@ describe('TaskManager', () => {
1154
1609
 
1155
1610
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1156
1611
 
1157
- taskManager.getTask(taskId).updateTaskData = jest.fn().mockImplementation((newData) => {
1158
- taskManager.getTask(taskId).data = {...newData, isConsulted: true};
1159
- return taskManager.getTask(taskId);
1160
- });
1612
+ taskManager.getTask(taskId).data.isConsulted = true;
1161
1613
  const task = taskManager.getTask(taskId);
1162
1614
 
1163
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1164
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1615
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1165
1616
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1166
- expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
1167
- expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.AGENT_CONSULT_ENDED, payload.data);
1168
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONSULT_END, task);
1617
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONSULT_END);
1169
1618
  expect(taskManager.getTask(taskId)).toBeUndefined(); // Ensure task is removed from the task collection after the consult ends
1619
+ sendStateMachineEventSpy.mockRestore();
1170
1620
  });
1171
1621
 
1172
1622
  it('should emit TASK_CANCELLED event on AGENT_CTQ_CANCELLED event', () => {
@@ -1178,14 +1628,15 @@ describe('TaskManager', () => {
1178
1628
  };
1179
1629
 
1180
1630
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1181
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1182
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1631
+ const task = taskManager.getTask(taskId);
1632
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1183
1633
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1184
- expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
1185
- expect(taskEmitSpy).toHaveBeenCalledWith(
1186
- TASK_EVENTS.TASK_CONSULT_QUEUE_CANCELLED,
1187
- taskManager.getTask(taskId)
1634
+ const stateMachineEvent = expectLastStateMachineEvent(
1635
+ sendStateMachineEventSpy,
1636
+ TaskEvent.CTQ_CANCEL
1188
1637
  );
1638
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1639
+ sendStateMachineEventSpy.mockRestore();
1189
1640
  });
1190
1641
 
1191
1642
  it('should handle AGENT_CONSULT_FAILED event', () => {
@@ -1199,9 +1650,15 @@ describe('TaskManager', () => {
1199
1650
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1200
1651
 
1201
1652
  // Always spy on the updated task object after CONTACT_RESERVED is emitted
1202
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1653
+ const task = taskManager.getTask(taskId);
1654
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1203
1655
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1204
- 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();
1205
1662
  });
1206
1663
 
1207
1664
  it('should emit TASK_CONSULT_QUEUE_FAILED on AGENT_CTQ_CANCEL_FAILED event', () => {
@@ -1213,14 +1670,15 @@ describe('TaskManager', () => {
1213
1670
  };
1214
1671
 
1215
1672
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1216
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1217
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1673
+ const task = taskManager.getTask(taskId);
1674
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1218
1675
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1219
- expect(taskUpdateTaskDataSpy).toHaveBeenCalledWith(payload.data);
1220
- expect(taskEmitSpy).toHaveBeenCalledWith(
1221
- TASK_EVENTS.TASK_CONSULT_QUEUE_FAILED,
1222
- taskManager.getTask(taskId)
1676
+ const stateMachineEvent = expectLastStateMachineEvent(
1677
+ sendStateMachineEventSpy,
1678
+ TaskEvent.CTQ_CANCEL_FAILED
1223
1679
  );
1680
+ expect(stateMachineEvent?.taskData).toEqual(payload.data);
1681
+ sendStateMachineEventSpy.mockRestore();
1224
1682
  });
1225
1683
 
1226
1684
  it('should emit TASK_REJECT event on AGENT_CONTACT_OFFER_RONA event', () => {
@@ -1263,15 +1721,14 @@ describe('TaskManager', () => {
1263
1721
  };
1264
1722
 
1265
1723
  taskManager.taskCollection[taskId] = taskManager.getTask(taskId);
1266
- const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1267
- const metricsTrackSpy = jest.spyOn(taskManager.metricsManager, 'trackEvent');
1724
+ const task = taskManager.getTask(taskId);
1725
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1268
1726
 
1269
1727
  webSocketManagerMock.emit('message', JSON.stringify(ronaPayload));
1270
1728
 
1271
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_REJECT, ronaPayload.data.reason);
1272
- // Verify the correct metric event name is used for AGENT_CONTACT_OFFER_RONA
1273
- expect(metricsTrackSpy).toHaveBeenCalled();
1274
- 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();
1275
1732
  });
1276
1733
 
1277
1734
  it('should emit TASK_REJECT event on AGENT_CONTACT_ASSIGN_FAILED event', () => {
@@ -1315,17 +1772,16 @@ describe('TaskManager', () => {
1315
1772
 
1316
1773
  taskManager.taskCollection[taskId] = taskManager.getTask(taskId);
1317
1774
  const task = taskManager.getTask(taskId);
1318
- const taskEmitSpy = jest.spyOn(task, 'emit');
1319
- const taskUpdateDataSpy = jest.spyOn(task, 'updateTaskData');
1320
- const metricsTrackSpy = jest.spyOn(taskManager.metricsManager, 'trackEvent');
1775
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1321
1776
 
1322
1777
  webSocketManagerMock.emit('message', JSON.stringify(assignFailedPayload));
1323
1778
 
1324
- expect(taskUpdateDataSpy).toHaveBeenCalledWith(assignFailedPayload.data);
1325
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_REJECT, assignFailedPayload.data.reason);
1326
- // Verify the correct metric event name is used for AGENT_CONTACT_ASSIGN_FAILED
1327
- expect(metricsTrackSpy).toHaveBeenCalled();
1328
- expect(metricsTrackSpy.mock.calls[0][0]).toBe('Agent Contact Assign Failed');
1779
+ const stateMachineEvent = expectLastStateMachineEvent(
1780
+ sendStateMachineEventSpy,
1781
+ TaskEvent.ASSIGN_FAILED
1782
+ );
1783
+ expect(stateMachineEvent?.reason).toBe(assignFailedPayload.data.reason);
1784
+ sendStateMachineEventSpy.mockRestore();
1329
1785
  });
1330
1786
 
1331
1787
  it('should remove currentTask from taskCollection on AGENT_WRAPPEDUP event', () => {
@@ -1348,11 +1804,12 @@ describe('TaskManager', () => {
1348
1804
 
1349
1805
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1350
1806
  const task = taskManager.getTask(taskId);
1351
- const taskEmitSpy = jest.spyOn(task, 'emit');
1807
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1352
1808
 
1353
1809
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1354
1810
 
1355
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WRAPPEDUP, task);
1811
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.WRAPUP_COMPLETE);
1812
+ sendStateMachineEventSpy.mockRestore();
1356
1813
  expect(taskManager.getTask(taskId)).toBeUndefined();
1357
1814
  });
1358
1815
 
@@ -1378,7 +1835,8 @@ describe('TaskManager', () => {
1378
1835
  };
1379
1836
 
1380
1837
  const taskEmitSpy = jest.spyOn(taskManager.getTask(taskId), 'emit');
1381
- const taskUpdateTaskDataSpy = jest.spyOn(taskManager.getTask(taskId), 'updateTaskData');
1838
+ const taskUpdateTaskDataSpy = taskManager.getTask(taskId).updateTaskData as jest.Mock;
1839
+ taskUpdateTaskDataSpy.mockClear();
1382
1840
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1383
1841
  expect(taskEmitSpy).not.toHaveBeenCalled();
1384
1842
  expect(taskUpdateTaskDataSpy).not.toHaveBeenCalled();
@@ -1387,7 +1845,9 @@ describe('TaskManager', () => {
1387
1845
  it('should emit TASK_CONSULTING event when agent is consulting', () => {
1388
1846
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1389
1847
  taskManager.getTask(taskId).data.isConsulted = false;
1390
- 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');
1391
1851
  const consultingPayload = {
1392
1852
  data: {
1393
1853
  ...initalPayload.data,
@@ -1396,13 +1856,21 @@ describe('TaskManager', () => {
1396
1856
  },
1397
1857
  };
1398
1858
  webSocketManagerMock.emit('message', JSON.stringify(consultingPayload));
1399
- expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.AGENT_CONSULTING, consultingPayload.data);
1400
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONSULTING, taskManager.getTask(taskId));
1859
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONSULTING_ACTIVE);
1860
+ expect(taskManagerEmitSpy).toHaveBeenCalledWith(
1861
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
1862
+ taskManager.getTask(taskId)
1863
+ );
1864
+ sendStateMachineEventSpy.mockRestore();
1401
1865
  });
1402
1866
 
1403
- it('should emit TASK_END event on AGENT_CONTACT_UNASSIGNED', () => {
1867
+ it('should update task data on AGENT_CONTACT_UNASSIGNED', () => {
1404
1868
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1405
- 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();
1406
1874
  const unassignedPayload = {
1407
1875
  data: {
1408
1876
  type: CC_EVENTS.AGENT_CONTACT_UNASSIGNED,
@@ -1420,8 +1888,76 @@ describe('TaskManager', () => {
1420
1888
  },
1421
1889
  };
1422
1890
  webSocketManagerMock.emit('message', JSON.stringify(unassignedPayload));
1423
- expect(taskEmitSpy).toHaveBeenCalledWith(CC_EVENTS.AGENT_CONTACT_UNASSIGNED, unassignedPayload.data);
1424
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, taskManager.getTask(taskId));
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
+ })
1960
+ );
1425
1961
  });
1426
1962
 
1427
1963
  it('should handle chat interaction and emit TASK_INCOMING immediately', () => {
@@ -1429,20 +1965,20 @@ describe('TaskManager', () => {
1429
1965
  const chatPayload = {
1430
1966
  data: {
1431
1967
  ...initalPayload.data,
1432
- interaction: { mediaType: 'chat' },
1968
+ interaction: {mediaType: 'chat'},
1433
1969
  },
1434
1970
  };
1435
1971
 
1436
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
1437
-
1438
1972
  // Simulate receiving a chat task
1439
1973
  webSocketManagerMock.emit('message', JSON.stringify(chatPayload));
1440
1974
 
1441
- // For non-telephony tasks, TASK_INCOMING should be emitted immediately
1442
- expect(taskIncomingSpy).toHaveBeenCalledWith(
1443
- TASK_EVENTS.TASK_INCOMING,
1444
- 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
1445
1980
  );
1981
+ expect(stateMachineEvent?.taskData).toEqual(chatPayload.data);
1446
1982
  expect(taskManager.getAllTasks()).toHaveProperty(chatPayload.data.interactionId);
1447
1983
  });
1448
1984
 
@@ -1451,20 +1987,20 @@ describe('TaskManager', () => {
1451
1987
  const emailPayload = {
1452
1988
  data: {
1453
1989
  ...initalPayload.data,
1454
- interaction: { mediaType: 'email' },
1990
+ interaction: {mediaType: 'email'},
1455
1991
  },
1456
1992
  };
1457
1993
 
1458
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
1459
-
1460
1994
  // Simulate receiving an email task
1461
1995
  webSocketManagerMock.emit('message', JSON.stringify(emailPayload));
1462
1996
 
1463
- // For non-telephony tasks, TASK_INCOMING should be emitted immediately
1464
- expect(taskIncomingSpy).toHaveBeenCalledWith(
1465
- TASK_EVENTS.TASK_INCOMING,
1466
- 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
1467
2002
  );
2003
+ expect(stateMachineEvent?.taskData).toEqual(emailPayload.data);
1468
2004
  expect(taskManager.getAllTasks()).toHaveProperty(emailPayload.data.interactionId);
1469
2005
  });
1470
2006
 
@@ -1474,18 +2010,16 @@ describe('TaskManager', () => {
1474
2010
  data: {
1475
2011
  ...initalPayload.data,
1476
2012
  type: CC_EVENTS.AGENT_CONTACT_RESERVED,
1477
- interaction: { mediaType: 'chat' },
2013
+ interaction: {mediaType: 'chat'},
1478
2014
  },
1479
2015
  };
1480
-
1481
- const taskIncomingSpy = jest.spyOn(taskManager, 'emit');
2016
+
1482
2017
  webSocketManagerMock.emit('message', JSON.stringify(chatReservedPayload));
1483
-
1484
- expect(taskIncomingSpy).toHaveBeenCalledWith(
1485
- TASK_EVENTS.TASK_INCOMING,
1486
- taskManager.getTask(chatReservedPayload.data.interactionId)
1487
- );
1488
-
2018
+ const task = taskManager.getTask(chatReservedPayload.data.interactionId);
2019
+ const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
2020
+
2021
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TASK_INCOMING);
2022
+
1489
2023
  // 2. Chat task is assigned
1490
2024
  const chatAssignedPayload = {
1491
2025
  data: {
@@ -1493,26 +2027,26 @@ describe('TaskManager', () => {
1493
2027
  type: CC_EVENTS.AGENT_CONTACT_ASSIGNED,
1494
2028
  },
1495
2029
  };
1496
-
1497
- const task = taskManager.getTask(chatReservedPayload.data.interactionId);
1498
- const taskEmitSpy = jest.spyOn(task, 'emit');
1499
-
2030
+
1500
2031
  webSocketManagerMock.emit('message', JSON.stringify(chatAssignedPayload));
1501
-
1502
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_ASSIGNED, task);
1503
-
2032
+
2033
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.ASSIGN);
2034
+
1504
2035
  // 3. Chat task is ended with state 'new' to trigger cleanup
1505
2036
  const chatEndedPayload = {
1506
2037
  data: {
1507
2038
  ...chatReservedPayload.data,
1508
2039
  type: CC_EVENTS.CONTACT_ENDED,
1509
- interaction: { mediaType: 'chat', state: 'new' }, // Change to 'new' state
2040
+ interaction: {mediaType: 'chat', state: 'new'}, // Change to 'new' state
2041
+ wrapUpRequired: false,
1510
2042
  },
1511
2043
  };
1512
2044
 
2045
+ // Simulate state on the task to allow cleanup logic
2046
+ task.data.interaction.state = 'new';
1513
2047
  webSocketManagerMock.emit('message', JSON.stringify(chatEndedPayload));
1514
2048
 
1515
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
2049
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONTACT_ENDED);
1516
2050
  // Verify task is removed since it was in a 'new' state
1517
2051
  expect(taskManager.getTask(chatReservedPayload.data.interactionId)).toBeUndefined();
1518
2052
  });
@@ -1523,40 +2057,46 @@ describe('TaskManager', () => {
1523
2057
  data: {
1524
2058
  ...initalPayload.data,
1525
2059
  interactionId: 'telephony-task-id',
1526
- interaction: { mediaType: 'telephony' },
2060
+ interaction: {mediaType: 'telephony'},
1527
2061
  },
1528
2062
  };
1529
-
2063
+
1530
2064
  const chatPayload = {
1531
2065
  data: {
1532
2066
  ...initalPayload.data,
1533
2067
  interactionId: 'chat-task-id',
1534
- interaction: { mediaType: 'chat' },
2068
+ interaction: {mediaType: 'chat'},
1535
2069
  },
1536
2070
  };
1537
-
2071
+
1538
2072
  const emailPayload = {
1539
2073
  data: {
1540
2074
  ...initalPayload.data,
1541
2075
  interactionId: 'email-task-id',
1542
- interaction: { mediaType: 'email' },
2076
+ interaction: {mediaType: 'email'},
1543
2077
  },
1544
2078
  };
1545
-
2079
+
1546
2080
  // Simulate receiving tasks of different types
1547
2081
  webSocketManagerMock.emit('message', JSON.stringify(telephonyPayload));
1548
2082
  webSocketManagerMock.emit('message', JSON.stringify(chatPayload));
1549
2083
  webSocketManagerMock.emit('message', JSON.stringify(emailPayload));
1550
-
2084
+
1551
2085
  // Verify all tasks are in the collection
1552
2086
  expect(taskManager.getAllTasks()).toHaveProperty(telephonyPayload.data.interactionId);
1553
2087
  expect(taskManager.getAllTasks()).toHaveProperty(chatPayload.data.interactionId);
1554
2088
  expect(taskManager.getAllTasks()).toHaveProperty(emailPayload.data.interactionId);
1555
-
2089
+
1556
2090
  // Verify the task media types are correctly set
1557
- expect(taskManager.getTask(telephonyPayload.data.interactionId).data.interaction.mediaType).toBe('telephony');
1558
- expect(taskManager.getTask(chatPayload.data.interactionId).data.interaction.mediaType).toBe('chat');
1559
- expect(taskManager.getTask(emailPayload.data.interactionId).data.interaction.mediaType).toBe('email');
2091
+ expect(
2092
+ taskManager.getTask(telephonyPayload.data.interactionId).data.interaction.mediaType
2093
+ ).toBe('telephony');
2094
+ expect(taskManager.getTask(chatPayload.data.interactionId).data.interaction.mediaType).toBe(
2095
+ 'chat'
2096
+ );
2097
+ expect(taskManager.getTask(emailPayload.data.interactionId).data.interaction.mediaType).toBe(
2098
+ 'email'
2099
+ );
1560
2100
  });
1561
2101
 
1562
2102
  it('should properly handle one task ending when multiple tasks are active', () => {
@@ -1565,102 +2105,102 @@ describe('TaskManager', () => {
1565
2105
  data: {
1566
2106
  ...initalPayload.data,
1567
2107
  interactionId: 'task-id-1',
1568
- interaction: { mediaType: 'telephony' },
2108
+ interaction: {mediaType: 'telephony'},
1569
2109
  },
1570
2110
  };
1571
-
2111
+
1572
2112
  const task2Payload = {
1573
2113
  data: {
1574
2114
  ...initalPayload.data,
1575
2115
  interactionId: 'task-id-2',
1576
- interaction: { mediaType: 'chat' },
2116
+ interaction: {mediaType: 'chat'},
1577
2117
  },
1578
2118
  };
1579
-
2119
+
1580
2120
  const task3Payload = {
1581
2121
  data: {
1582
2122
  ...initalPayload.data,
1583
2123
  interactionId: 'task-id-3',
1584
- interaction: { mediaType: 'email' },
2124
+ interaction: {mediaType: 'email'},
1585
2125
  },
1586
2126
  };
1587
-
2127
+
1588
2128
  // Initialize all tasks
1589
2129
  webSocketManagerMock.emit('message', JSON.stringify(task1Payload));
1590
2130
  webSocketManagerMock.emit('message', JSON.stringify(task2Payload));
1591
2131
  webSocketManagerMock.emit('message', JSON.stringify(task3Payload));
1592
-
2132
+
1593
2133
  // Verify all tasks are in the collection
1594
2134
  expect(taskManager.getAllTasks()).toHaveProperty(task1Payload.data.interactionId);
1595
2135
  expect(taskManager.getAllTasks()).toHaveProperty(task2Payload.data.interactionId);
1596
2136
  expect(taskManager.getAllTasks()).toHaveProperty(task3Payload.data.interactionId);
1597
-
1598
- // Create spies for all tasks
1599
- const task1EmitSpy = jest.spyOn(taskManager.getTask(task1Payload.data.interactionId), 'emit');
1600
- const task2EmitSpy = jest.spyOn(taskManager.getTask(task2Payload.data.interactionId), 'emit');
1601
- const task3EmitSpy = jest.spyOn(taskManager.getTask(task3Payload.data.interactionId), 'emit');
1602
-
1603
- // Store reference to task2 before it gets removed
2137
+
1604
2138
  const task2 = taskManager.getTask(task2Payload.data.interactionId);
1605
-
2139
+ const task2SendStateMachineEventSpy = task2.sendStateMachineEvent as jest.Mock;
2140
+
1606
2141
  // End only the second task (chat task)
1607
2142
  const chatEndedPayload = {
1608
2143
  data: {
1609
2144
  ...task2Payload.data,
1610
2145
  type: CC_EVENTS.CONTACT_ENDED,
1611
- interaction: { mediaType: 'chat', state: 'new' }, // Using 'new' to trigger cleanup
2146
+ interaction: {mediaType: 'chat', state: 'new'}, // Using 'new' to trigger cleanup
2147
+ wrapUpRequired: false,
1612
2148
  },
1613
2149
  };
1614
-
2150
+
2151
+ task2.data.interaction.state = 'new';
1615
2152
  webSocketManagerMock.emit('message', JSON.stringify(chatEndedPayload));
1616
-
1617
- // Verify only task2 emitted TASK_END
1618
- expect(task1EmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END);
1619
- expect(task2EmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task2);
1620
- expect(task3EmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END);
1621
-
2153
+
2154
+ const firstEndEvent = expectLastStateMachineEvent(
2155
+ task2SendStateMachineEventSpy,
2156
+ TaskEvent.CONTACT_ENDED
2157
+ );
2158
+ expect(firstEndEvent?.taskData).toEqual(chatEndedPayload.data);
2159
+
1622
2160
  // Verify task2 was removed from collection (since state was 'new')
1623
2161
  expect(taskManager.getTask(task2Payload.data.interactionId)).toBeUndefined();
1624
-
2162
+
1625
2163
  // Verify other tasks remain in the collection
1626
2164
  expect(taskManager.getTask(task1Payload.data.interactionId)).toBeDefined();
1627
2165
  expect(taskManager.getTask(task3Payload.data.interactionId)).toBeDefined();
1628
-
2166
+
1629
2167
  // Store reference to task3 before we end it
1630
2168
  const task3 = taskManager.getTask(task3Payload.data.interactionId);
1631
-
2169
+ const task3SendStateMachineEventSpy = task3.sendStateMachineEvent as jest.Mock;
2170
+
1632
2171
  // Now end task3 with a state that doesn't trigger cleanup
1633
2172
  const emailEndedPayload = {
1634
2173
  data: {
1635
2174
  ...task3Payload.data,
1636
2175
  type: CC_EVENTS.CONTACT_ENDED,
1637
- interaction: { mediaType: 'email', state: 'connected' }, // Using 'connected' to NOT trigger cleanup
2176
+ interaction: {mediaType: 'email', state: 'connected'}, // Using 'connected' to NOT trigger cleanup
2177
+ wrapUpRequired: true,
2178
+ agentsPendingWrapUp: ['test-agent-id'],
1638
2179
  },
1639
2180
  };
1640
-
2181
+
2182
+ task3.data.interaction.state = 'connected';
1641
2183
  webSocketManagerMock.emit('message', JSON.stringify(emailEndedPayload));
1642
-
1643
- // Verify task3 emitted TASK_END
1644
- expect(task3EmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task3);
1645
-
2184
+
2185
+ const secondEndEvent = expectLastStateMachineEvent(
2186
+ task3SendStateMachineEventSpy,
2187
+ TaskEvent.CONTACT_ENDED
2188
+ );
2189
+ expect(secondEndEvent?.taskData).toEqual(emailEndedPayload.data);
2190
+
1646
2191
  // Verify task3 is still in collection (since state was 'connected')
1647
2192
  expect(taskManager.getTask(task3Payload.data.interactionId)).toBeDefined();
1648
-
2193
+
1649
2194
  // Verify task1 remains unaffected
1650
- expect(task1EmitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_END);
1651
2195
  expect(taskManager.getTask(task1Payload.data.interactionId)).toBeDefined();
1652
2196
  });
1653
2197
 
1654
- it('should emit TASK_END event on AGENT_VTEAM_TRANSFERRED event', () => {
2198
+ it('should emit TRANSFER_SUCCESS event on AGENT_VTEAM_TRANSFERRED event', () => {
1655
2199
  // First create a task by emitting the initial payload
1656
2200
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1657
-
1658
- // Get a reference to the task from taskCollection
1659
2201
  const task = taskManager.getTask(taskId);
1660
-
1661
- // Now spy on the task's emit method
1662
- const taskEmitSpy = jest.spyOn(task, 'emit');
1663
-
2202
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
2203
+
1664
2204
  const vteamTransferredPayload = {
1665
2205
  data: {
1666
2206
  type: CC_EVENTS.AGENT_VTEAM_TRANSFERRED,
@@ -1677,34 +2217,39 @@ describe('TaskManager', () => {
1677
2217
  queueMgr: initalPayload.data.queueMgr,
1678
2218
  },
1679
2219
  };
1680
-
2220
+
1681
2221
  // No need to explicitly set the task in the collection as it's already there
1682
2222
  // from the initial message processing
1683
-
2223
+
1684
2224
  webSocketManagerMock.emit('message', JSON.stringify(vteamTransferredPayload));
1685
-
1686
- // Check that task.emit was called with TASK_END event
1687
- expect(taskEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_END, task);
1688
-
2225
+
2226
+ // Check that the state machine received the END event
2227
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.TRANSFER_SUCCESS);
2228
+ sendStateMachineEventSpy.mockRestore();
2229
+
1689
2230
  // The task should still exist in the collection based on current implementation
1690
2231
  expect(taskManager.getTask(taskId)).toBeDefined();
1691
2232
  });
1692
2233
 
1693
2234
  it('should update task data on AGENT_WRAPUP event', () => {
1694
2235
  const payload = {
1695
- data: {
1696
- type: CC_EVENTS.AGENT_WRAPUP,
1697
- interactionId: taskId,
1698
- wrapUpRequired: true,
1699
- },
2236
+ data: {
2237
+ type: CC_EVENTS.AGENT_WRAPUP,
2238
+ interactionId: taskId,
2239
+ wrapUpRequired: true,
2240
+ },
1700
2241
  };
1701
2242
  const task = taskManager.getTask(taskId);
1702
- const updateSpy = jest.spyOn(task, 'updateTaskData').mockImplementation((data) => {
1703
- task.data = { ...(task.data || {}), ...(data || {}) };
1704
- return task;
1705
- });
2243
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1706
2244
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1707
- 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();
1708
2253
  });
1709
2254
 
1710
2255
  it('should not attempt cleanup twice when AGENT_CONTACT_UNASSIGNED is followed by AGENT_WRAPUP', () => {
@@ -1716,7 +2261,7 @@ describe('TaskManager', () => {
1716
2261
  data: {
1717
2262
  type: CC_EVENTS.AGENT_CONTACT_UNASSIGNED,
1718
2263
  agentId: initalPayload.data.agentId,
1719
- interaction: { mediaType: 'telephony' },
2264
+ interaction: {mediaType: 'telephony'},
1720
2265
  interactionId: initalPayload.data.interactionId,
1721
2266
  orgId: initalPayload.data.orgId,
1722
2267
  trackingId: initalPayload.data.trackingId,
@@ -1735,7 +2280,7 @@ describe('TaskManager', () => {
1735
2280
  data: {
1736
2281
  type: CC_EVENTS.AGENT_WRAPUP,
1737
2282
  interactionId: taskId,
1738
- interaction: { mediaType: 'telephony' },
2283
+ interaction: {mediaType: 'telephony'},
1739
2284
  },
1740
2285
  };
1741
2286
  webSocketManagerMock.emit('message', JSON.stringify(wrapupPayload));
@@ -1752,7 +2297,7 @@ describe('TaskManager', () => {
1752
2297
  data: {
1753
2298
  type: CC_EVENTS.AGENT_VTEAM_TRANSFERRED,
1754
2299
  agentId: initalPayload.data.agentId,
1755
- interaction: { mediaType: 'telephony' },
2300
+ interaction: {mediaType: 'telephony'},
1756
2301
  interactionId: initalPayload.data.interactionId,
1757
2302
  orgId: initalPayload.data.orgId,
1758
2303
  trackingId: initalPayload.data.trackingId,
@@ -1771,7 +2316,7 @@ describe('TaskManager', () => {
1771
2316
  data: {
1772
2317
  type: CC_EVENTS.AGENT_WRAPUP,
1773
2318
  interactionId: taskId,
1774
- interaction: { mediaType: 'telephony' },
2319
+ interaction: {mediaType: 'telephony'},
1775
2320
  },
1776
2321
  };
1777
2322
  webSocketManagerMock.emit('message', JSON.stringify(wrapupPayload));
@@ -1780,704 +2325,157 @@ describe('TaskManager', () => {
1780
2325
  });
1781
2326
 
1782
2327
  describe('should emit appropriate task events for recording events', () => {
1783
- ['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) => {
1784
2337
  const ccEvent = CC_EVENTS[`CONTACT_RECORDING_${suffix}`];
1785
- const taskEvent = TASK_EVENTS[`TASK_RECORDING_${suffix}`];
1786
- 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`, () => {
1787
2342
  const payload = {data: {...initalPayload.data, type: ccEvent}};
1788
2343
  webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
1789
2344
  const task = taskManager.getTask(taskId);
1790
- const spy = jest.spyOn(task, 'emit');
2345
+ const sendStateMachineEventSpy = task.sendStateMachineEvent as jest.Mock;
2346
+ sendStateMachineEventSpy.mockClear();
1791
2347
 
1792
2348
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1793
- 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
+ }
1794
2358
  });
1795
2359
  });
1796
- });
2360
+ });
1797
2361
 
1798
2362
  describe('Conference event handling', () => {
1799
2363
  let task;
1800
- const agentId = '723a8ffb-a26e-496d-b14a-ff44fb83b64f';
1801
-
2364
+
1802
2365
  beforeEach(() => {
1803
- // Set the agentId on taskManager before tests run
1804
- taskManager.setAgentId(agentId);
1805
-
1806
2366
  task = {
1807
- data: { interactionId: taskId },
2367
+ data: {interactionId: taskId},
1808
2368
  emit: jest.fn(),
1809
- updateTaskData: jest.fn().mockImplementation((updatedData) => {
1810
- // Mock the updateTaskData method to actually update task.data
1811
- task.data = { ...task.data, ...updatedData };
1812
- return task;
1813
- }),
2369
+ updateTaskData: jest.fn(),
2370
+ sendStateMachineEvent: jest.fn(),
1814
2371
  };
1815
- taskManager.taskCollection[taskId] = task;
2372
+ taskManager.taskCollection[taskId] = task as any;
1816
2373
  });
1817
2374
 
1818
- it('should handle AGENT_CONSULT_CONFERENCED event', () => {
2375
+ it('sends AGENT_CONSULT_CONFERENCED to state machine as CONFERENCE_START', () => {
1819
2376
  const payload = {
1820
- data: {
1821
- type: CC_EVENTS.AGENT_CONSULT_CONFERENCED,
1822
- interactionId: taskId,
1823
- isConferencing: true,
1824
- },
2377
+ data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCED, interactionId: taskId},
1825
2378
  };
1826
-
1827
2379
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1828
-
1829
- expect(task.data.isConferencing).toBe(true);
1830
- 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);
1831
2383
  });
1832
2384
 
1833
- it('should handle AGENT_CONSULT_CONFERENCING event', () => {
2385
+ it('sends PARTICIPANT_JOINED_CONFERENCE to state machine as CONFERENCE_START', () => {
1834
2386
  const payload = {
1835
- data: {
1836
- type: CC_EVENTS.AGENT_CONSULT_CONFERENCING,
1837
- interactionId: taskId,
1838
- isConferencing: true,
1839
- },
2387
+ data: {type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE, interactionId: taskId},
1840
2388
  };
1841
-
1842
2389
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1843
-
1844
- expect(task.data.isConferencing).toBe(true);
1845
- // No task event emission for conferencing - only for conferenced (completed)
1846
- 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);
1847
2393
  });
1848
2394
 
1849
- it('should handle AGENT_CONSULT_CONFERENCE_FAILED event', () => {
2395
+ it('sends AGENT_CONSULT_CONFERENCE_FAILED to state machine as CONFERENCE_FAILED', () => {
1850
2396
  const payload = {
1851
- data: {
1852
- type: CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED,
1853
- interactionId: taskId,
1854
- reason: 'Network error',
1855
- },
2397
+ data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCE_FAILED, interactionId: taskId},
1856
2398
  };
1857
-
1858
2399
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1859
-
1860
- expect(task.data.reason).toBe('Network error');
1861
- // 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);
1862
2403
  });
1863
2404
 
1864
- it('should handle PARTICIPANT_JOINED_CONFERENCE event', () => {
2405
+ it('sends PARTICIPANT_LEFT_CONFERENCE to state machine as PARTICIPANT_LEAVE', () => {
1865
2406
  const payload = {
1866
- data: {
1867
- type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE,
1868
- interactionId: taskId,
1869
- participantId: 'new-participant-123',
1870
- participantType: 'agent',
1871
- interaction: {
1872
- participants: {
1873
- [agentId]: { pType: 'Agent', hasLeft: false },
1874
- 'new-participant-123': { pType: 'Agent', hasLeft: false },
1875
- },
1876
- media: {
1877
- [taskId]: {
1878
- mType: 'mainCall',
1879
- participants: [agentId, 'new-participant-123'],
1880
- },
1881
- },
1882
- },
1883
- },
2407
+ data: {type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, interactionId: taskId},
1884
2408
  };
1885
-
1886
2409
  webSocketManagerMock.emit('message', JSON.stringify(payload));
1887
-
1888
- expect(task.data.participantId).toBe('new-participant-123');
1889
- expect(task.data.participantType).toBe('agent');
1890
- // 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);
1891
2413
  });
1892
2414
 
1893
- it('should call updateTaskData only once for PARTICIPANT_JOINED_CONFERENCE with pre-calculated isConferenceInProgress', () => {
2415
+ it('handles AGENT_CONSULT_CONFERENCING event without errors', () => {
1894
2416
  const payload = {
1895
- data: {
1896
- type: CC_EVENTS.PARTICIPANT_JOINED_CONFERENCE,
1897
- interactionId: taskId,
1898
- participantId: 'new-agent-789',
1899
- interaction: {
1900
- participants: {
1901
- [agentId]: { pType: 'Agent', hasLeft: false },
1902
- 'agent-2': { pType: 'Agent', hasLeft: false },
1903
- 'new-agent-789': { pType: 'Agent', hasLeft: false },
1904
- 'customer-1': { pType: 'Customer', hasLeft: false },
1905
- },
1906
- media: {
1907
- [taskId]: {
1908
- mType: 'mainCall',
1909
- participants: [agentId, 'agent-2', 'new-agent-789', 'customer-1'],
1910
- },
1911
- },
1912
- },
1913
- },
2417
+ data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCING, interactionId: taskId},
1914
2418
  };
1915
-
1916
- const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
1917
-
1918
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1919
-
1920
- // Verify updateTaskData was called exactly once
1921
- expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
1922
-
1923
- // Verify it was called with isConferenceInProgress already calculated
1924
- expect(updateTaskDataSpy).toHaveBeenCalledWith(
1925
- expect.objectContaining({
1926
- participantId: 'new-agent-789',
1927
- isConferenceInProgress: true, // 3 active agents
1928
- })
1929
- );
1930
-
1931
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_JOINED, task);
1932
- });
1933
-
1934
- describe('PARTICIPANT_LEFT_CONFERENCE event handling', () => {
1935
- it('should call updateTaskData only once for PARTICIPANT_LEFT_CONFERENCE with pre-calculated isConferenceInProgress', () => {
1936
- const payload = {
1937
- data: {
1938
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1939
- interactionId: taskId,
1940
- interaction: {
1941
- participants: {
1942
- [agentId]: { pType: 'Agent', hasLeft: false },
1943
- 'agent-2': { pType: 'Agent', hasLeft: true }, // This agent left
1944
- 'customer-1': { pType: 'Customer', hasLeft: false },
1945
- },
1946
- media: {
1947
- [taskId]: {
1948
- mType: 'mainCall',
1949
- participants: [agentId, 'customer-1'], // agent-2 removed from participants
1950
- },
1951
- },
1952
- },
1953
- },
1954
- };
1955
-
1956
- const updateTaskDataSpy = jest.spyOn(task, 'updateTaskData');
1957
-
1958
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1959
-
1960
- // Verify updateTaskData was called exactly once
1961
- expect(updateTaskDataSpy).toHaveBeenCalledTimes(1);
1962
-
1963
- // Verify it was called with isConferenceInProgress already calculated
1964
- expect(updateTaskDataSpy).toHaveBeenCalledWith(
1965
- expect.objectContaining({
1966
- isConferenceInProgress: false, // Only 1 active agent remains
1967
- })
1968
- );
1969
-
1970
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1971
- });
1972
-
1973
- it('should emit TASK_PARTICIPANT_LEFT event when participant leaves conference', () => {
1974
- const payload = {
1975
- data: {
1976
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1977
- interactionId: taskId,
1978
- interaction: {
1979
- participants: {
1980
- [agentId]: {
1981
- hasLeft: false,
1982
- },
1983
- },
1984
- },
1985
- },
1986
- };
1987
-
1988
- webSocketManagerMock.emit('message', JSON.stringify(payload));
1989
-
1990
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
1991
- });
1992
-
1993
- it('should NOT remove task when agent is still in interaction', () => {
1994
- const payload = {
1995
- data: {
1996
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
1997
- interactionId: taskId,
1998
- interaction: {
1999
- participants: {
2000
- [agentId]: {
2001
- hasLeft: false,
2002
- },
2003
- },
2004
- },
2005
- },
2006
- };
2007
-
2008
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2009
-
2010
- // Task should still exist in collection
2011
- expect(taskManager.getTask(taskId)).toBeDefined();
2012
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2013
- });
2014
-
2015
- it('should NOT remove task when agent left but is in main interaction', () => {
2016
- const payload = {
2017
- data: {
2018
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2019
- interactionId: taskId,
2020
- interaction: {
2021
- participants: {
2022
- [agentId]: {
2023
- hasLeft: true,
2024
- },
2025
- },
2026
- media: {
2027
- [taskId]: {
2028
- mType: 'mainCall',
2029
- participants: [agentId],
2030
- },
2031
- },
2032
- },
2033
- },
2034
- };
2035
-
2036
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2037
-
2038
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2039
-
2040
- // Task should still exist - not removed
2041
- expect(removeTaskSpy).not.toHaveBeenCalled();
2042
- expect(taskManager.getTask(taskId)).toBeDefined();
2043
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2044
- });
2045
-
2046
- it('should NOT remove task when agent left but is primary (owner)', () => {
2047
- const payload = {
2048
- data: {
2049
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2050
- interactionId: taskId,
2051
- interaction: {
2052
- participants: {
2053
- [agentId]: {
2054
- hasLeft: true,
2055
- },
2056
- },
2057
- owner: agentId,
2058
- media: {
2059
- [taskId]: {
2060
- mType: 'consultCall',
2061
- participants: ['other-agent'],
2062
- },
2063
- },
2064
- },
2065
- },
2066
- };
2067
-
2068
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2069
-
2419
+ expect(() => {
2070
2420
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2071
-
2072
- // Task should still exist - not removed because agent is primary
2073
- expect(removeTaskSpy).not.toHaveBeenCalled();
2074
- expect(taskManager.getTask(taskId)).toBeDefined();
2075
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2076
- });
2077
-
2078
- it('should remove task when agent left and is NOT in main interaction and is NOT primary', () => {
2079
- const payload = {
2080
- data: {
2081
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2082
- interactionId: taskId,
2083
- interaction: {
2084
- participants: {
2085
- [agentId]: {
2086
- hasLeft: true,
2087
- },
2088
- },
2089
- owner: 'another-agent-id',
2090
- media: {
2091
- [taskId]: {
2092
- mType: 'mainCall',
2093
- participants: ['another-agent-id'],
2094
- },
2095
- },
2096
- },
2097
- },
2098
- };
2099
-
2100
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2101
-
2102
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2103
-
2104
- // Task should be removed
2105
- expect(removeTaskSpy).toHaveBeenCalled();
2106
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2107
- });
2108
-
2109
- it('should remove task when agent is not in participants list', () => {
2110
- const payload = {
2111
- data: {
2112
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2113
- interactionId: taskId,
2114
- interaction: {
2115
- participants: {
2116
- 'other-agent-id': {
2117
- hasLeft: false,
2118
- },
2119
- },
2120
- owner: 'another-agent-id',
2121
- },
2122
- },
2123
- };
2124
-
2125
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2126
-
2127
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2128
-
2129
- // Task should be removed because agent is not in participants
2130
- expect(removeTaskSpy).toHaveBeenCalled();
2131
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2132
- });
2133
-
2134
- it('should update isConferenceInProgress based on remaining active agents', () => {
2135
- const payload = {
2136
- data: {
2137
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2138
- interactionId: taskId,
2139
- interaction: {
2140
- participants: {
2141
- [agentId]: {
2142
- hasLeft: false,
2143
- pType: 'Agent',
2144
- },
2145
- 'agent-2': {
2146
- hasLeft: false,
2147
- pType: 'Agent',
2148
- },
2149
- 'customer-1': {
2150
- hasLeft: false,
2151
- pType: 'Customer',
2152
- },
2153
- },
2154
- media: {
2155
- [taskId]: {
2156
- mType: 'mainCall',
2157
- participants: [agentId, 'agent-2', 'customer-1'],
2158
- },
2159
- },
2160
- },
2161
- },
2162
- };
2163
-
2164
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2165
-
2166
- // isConferenceInProgress should be true (2 active agents)
2167
- expect(task.data.isConferenceInProgress).toBe(true);
2168
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2169
- });
2170
-
2171
- it('should set isConferenceInProgress to false when only one agent remains', () => {
2172
- const payload = {
2173
- data: {
2174
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2175
- interactionId: taskId,
2176
- interaction: {
2177
- participants: {
2178
- [agentId]: {
2179
- hasLeft: false,
2180
- pType: 'Agent',
2181
- },
2182
- 'agent-2': {
2183
- hasLeft: true,
2184
- pType: 'Agent',
2185
- },
2186
- 'customer-1': {
2187
- hasLeft: false,
2188
- pType: 'Customer',
2189
- },
2190
- },
2191
- media: {
2192
- [taskId]: {
2193
- mType: 'mainCall',
2194
- participants: [agentId, 'customer-1'],
2195
- },
2196
- },
2197
- },
2198
- },
2199
- };
2200
-
2201
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2202
-
2203
- // isConferenceInProgress should be false (only 1 active agent)
2204
- expect(task.data.isConferenceInProgress).toBe(false);
2205
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2206
- });
2207
-
2208
- it('should handle participant left when no participants data exists', () => {
2209
- const payload = {
2210
- data: {
2211
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2212
- interactionId: taskId,
2213
- interaction: {},
2214
- },
2215
- };
2216
-
2217
- const removeTaskSpy = jest.spyOn(taskManager, 'removeTaskFromCollection');
2218
-
2219
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2220
-
2221
- // When no participants data exists, checkParticipantNotInInteraction returns true
2222
- // Since agent won't be in main interaction either, task should be removed
2223
- expect(removeTaskSpy).toHaveBeenCalled();
2224
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_PARTICIPANT_LEFT, task);
2225
- });
2421
+ }).not.toThrow();
2226
2422
  });
2227
2423
 
2228
- it('should handle PARTICIPANT_LEFT_CONFERENCE_FAILED event', () => {
2424
+ it('handles PARTICIPANT_LEFT_CONFERENCE_FAILED event without errors', () => {
2229
2425
  const payload = {
2230
- data: {
2231
- type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED,
2232
- interactionId: taskId,
2233
- reason: 'Exit failed',
2234
- },
2426
+ data: {type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE_FAILED, interactionId: taskId},
2235
2427
  };
2236
-
2237
- webSocketManagerMock.emit('message', JSON.stringify(payload));
2238
-
2239
- expect(task.data.reason).toBe('Exit failed');
2240
- // No event emission expected for failure - handled by contact method promise rejection
2428
+ expect(() => {
2429
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
2430
+ }).not.toThrow();
2241
2431
  });
2242
2432
 
2243
- it('should only update task for matching interactionId', () => {
2433
+ it('only routes conference events to matching tasks', () => {
2244
2434
  const otherTaskId = 'other-task-id';
2245
- const otherTask = {
2246
- data: { interactionId: otherTaskId },
2435
+ const otherTask: any = {
2436
+ data: {interactionId: otherTaskId},
2247
2437
  emit: jest.fn(),
2438
+ updateTaskData: jest.fn(),
2439
+ sendStateMachineEvent: jest.fn(),
2248
2440
  };
2249
2441
  taskManager.taskCollection[otherTaskId] = otherTask;
2250
2442
 
2251
2443
  const payload = {
2252
- data: {
2253
- type: CC_EVENTS.AGENT_CONSULT_CONFERENCED,
2254
- interactionId: taskId,
2255
- isConferencing: true,
2256
- },
2444
+ data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCED, interactionId: taskId},
2257
2445
  };
2258
-
2259
2446
  webSocketManagerMock.emit('message', JSON.stringify(payload));
2260
2447
 
2261
- // Only the matching task should be updated
2262
- expect(task.data.isConferencing).toBe(true);
2263
- expect(task.emit).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONFERENCE_STARTED, task);
2264
-
2265
- // Other task should not be affected
2266
- expect(otherTask.data.isConferencing).toBeUndefined();
2267
- expect(otherTask.emit).not.toHaveBeenCalled();
2448
+ expect(task.sendStateMachineEvent).toHaveBeenCalled();
2449
+ expect(otherTask.sendStateMachineEvent).not.toHaveBeenCalled();
2268
2450
  });
2269
2451
  });
2270
2452
 
2271
- describe('CONTACT_MERGED event handling', () => {
2272
- let task;
2273
- let taskEmitSpy;
2274
- let managerEmitSpy;
2275
-
2276
- beforeEach(() => {
2277
- // Create initial task
2278
- webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
2279
- task = taskManager.getTask(taskId);
2280
- taskEmitSpy = jest.spyOn(task, 'emit');
2281
- managerEmitSpy = jest.spyOn(taskManager, 'emit');
2282
- });
2283
-
2284
- it('should update existing task data and emit TASK_MERGED event when CONTACT_MERGED is received', () => {
2285
- const mergedPayload = {
2286
- data: {
2287
- type: CC_EVENTS.CONTACT_MERGED,
2288
- interactionId: taskId,
2289
- agentId: taskDataMock.agentId,
2290
- interaction: {
2291
- ...taskDataMock.interaction,
2292
- state: 'merged',
2293
- customField: 'updated-value',
2294
- },
2295
- },
2296
- };
2297
-
2298
- webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
2299
-
2300
- const updatedTask = taskManager.getTask(taskId);
2301
- expect(updatedTask).toBeDefined();
2302
- expect(updatedTask.data.interaction.customField).toBe('updated-value');
2303
- expect(updatedTask.data.interaction.state).toBe('merged');
2304
- expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, updatedTask);
2305
- });
2306
-
2307
- it('should create new task when CONTACT_MERGED is received for non-existing task', () => {
2308
- const newMergedTaskId = 'new-merged-task-id';
2309
- const mergedPayload = {
2310
- data: {
2311
- type: CC_EVENTS.CONTACT_MERGED,
2312
- interactionId: newMergedTaskId,
2313
- agentId: taskDataMock.agentId,
2314
- interaction: {
2315
- mediaType: 'telephony',
2316
- state: 'merged',
2317
- participants: {
2318
- [taskDataMock.agentId]: {
2319
- isWrapUp: false,
2320
- hasJoined: true,
2321
- },
2322
- },
2323
- },
2324
- },
2325
- };
2326
-
2327
- // Verify task doesn't exist before
2328
- expect(taskManager.getTask(newMergedTaskId)).toBeUndefined();
2329
-
2330
- webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
2331
-
2332
- // Verify task was created
2333
- const newTask = taskManager.getTask(newMergedTaskId);
2334
- expect(newTask).toBeDefined();
2335
- expect(newTask.data.interactionId).toBe(newMergedTaskId);
2336
- expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, newTask);
2337
- });
2338
-
2339
- it('should remove child task when childInteractionId is present in CONTACT_MERGED', () => {
2340
- const childTaskId = 'child-task-id';
2341
- const parentTaskId = 'parent-task-id';
2342
-
2343
- // Create child task
2344
- const childPayload = {
2345
- data: {
2346
- type: CC_EVENTS.AGENT_CONTACT_RESERVED,
2347
- interactionId: childTaskId,
2348
- agentId: taskDataMock.agentId,
2349
- interaction: {mediaType: 'telephony'},
2350
- },
2351
- };
2352
- webSocketManagerMock.emit('message', JSON.stringify(childPayload));
2353
-
2354
- // Verify child task exists
2355
- expect(taskManager.getTask(childTaskId)).toBeDefined();
2356
-
2357
- // Create parent task
2358
- const parentPayload = {
2359
- data: {
2360
- type: CC_EVENTS.AGENT_CONTACT_RESERVED,
2361
- interactionId: parentTaskId,
2362
- agentId: taskDataMock.agentId,
2363
- interaction: {mediaType: 'telephony'},
2364
- },
2365
- };
2366
- webSocketManagerMock.emit('message', JSON.stringify(parentPayload));
2367
-
2368
- // Send CONTACT_MERGED with childInteractionId
2369
- const mergedPayload = {
2370
- data: {
2371
- type: CC_EVENTS.CONTACT_MERGED,
2372
- interactionId: parentTaskId,
2373
- childInteractionId: childTaskId,
2374
- agentId: taskDataMock.agentId,
2375
- interaction: {
2376
- mediaType: 'telephony',
2377
- state: 'merged',
2378
- },
2379
- },
2380
- };
2381
-
2382
- webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
2383
-
2384
- // Verify child task was removed
2385
- expect(taskManager.getTask(childTaskId)).toBeUndefined();
2386
-
2387
- // Verify parent task still exists
2388
- expect(taskManager.getTask(parentTaskId)).toBeDefined();
2389
-
2390
- // Verify TASK_MERGED event was emitted
2391
- expect(managerEmitSpy).toHaveBeenCalledWith(
2392
- TASK_EVENTS.TASK_MERGED,
2393
- expect.objectContaining({
2394
- data: expect.objectContaining({
2395
- interactionId: parentTaskId,
2396
- }),
2397
- })
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}
2398
2458
  );
2399
- });
2400
-
2401
- it('should handle CONTACT_MERGED with EP-DN participant correctly', () => {
2402
- const epdnTaskId = 'epdn-merged-task';
2403
- const mergedPayload = {
2404
- data: {
2405
- type: CC_EVENTS.CONTACT_MERGED,
2406
- interactionId: epdnTaskId,
2407
- agentId: taskDataMock.agentId,
2408
- interaction: {
2409
- mediaType: 'telephony',
2410
- state: 'merged',
2411
- participants: {
2412
- [taskDataMock.agentId]: {
2413
- type: 'Agent',
2414
- isWrapUp: false,
2415
- hasJoined: true,
2416
- },
2417
- 'epdn-participant': {
2418
- type: 'EpDn',
2419
- epId: 'entry-point-123',
2420
- isWrapUp: false,
2421
- },
2422
- },
2423
- },
2424
- },
2425
- };
2426
-
2427
- webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
2428
2459
 
2429
- const mergedTask = taskManager.getTask(epdnTaskId);
2430
- expect(mergedTask).toBeDefined();
2431
- expect(mergedTask.data.interaction.participants['epdn-participant']).toBeDefined();
2432
- expect(mergedTask.data.interaction.participants['epdn-participant'].type).toBe('EpDn');
2433
- expect(managerEmitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_MERGED, mergedTask);
2460
+ expect(mapped).toEqual({
2461
+ type: TaskEvent.ASSIGN,
2462
+ taskData: {...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED},
2463
+ });
2434
2464
  });
2435
2465
 
2436
- it('should not affect other tasks when CONTACT_MERGED is received', () => {
2437
- const otherTaskId = 'other-unrelated-task';
2438
- const otherPayload = {
2439
- data: {
2440
- type: CC_EVENTS.AGENT_CONTACT_RESERVED,
2441
- interactionId: otherTaskId,
2442
- agentId: taskDataMock.agentId,
2443
- interaction: {mediaType: 'chat'},
2444
- },
2445
- };
2446
- webSocketManagerMock.emit('message', JSON.stringify(otherPayload));
2447
-
2448
- const otherTask = taskManager.getTask(otherTaskId);
2449
- const otherTaskEmitSpy = jest.spyOn(otherTask, 'emit');
2466
+ it('sends mapped events to the task state machine service', () => {
2467
+ const payload = {...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED};
2468
+ const task = taskManager.getTask(taskId);
2469
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
2450
2470
 
2451
- // Send CONTACT_MERGED for the original task
2452
- const mergedPayload = {
2453
- data: {
2454
- type: CC_EVENTS.CONTACT_MERGED,
2455
- interactionId: taskId,
2456
- agentId: taskDataMock.agentId,
2457
- interaction: {
2458
- mediaType: 'telephony',
2459
- state: 'merged',
2460
- },
2461
- },
2462
- };
2471
+ webSocketManagerMock.emit('message', JSON.stringify({data: payload}));
2463
2472
 
2464
- webSocketManagerMock.emit('message', JSON.stringify(mergedPayload));
2465
-
2466
- // Verify other task was not affected
2467
- expect(otherTaskEmitSpy).not.toHaveBeenCalled();
2468
- expect(otherTask.data.interaction.mediaType).toBe('chat');
2469
-
2470
- // Verify original task was updated
2471
- expect(managerEmitSpy).toHaveBeenCalledWith(
2472
- TASK_EVENTS.TASK_MERGED,
2473
- expect.objectContaining({
2474
- data: expect.objectContaining({
2475
- interactionId: taskId,
2476
- }),
2477
- })
2478
- );
2473
+ expect(sendStateMachineEventSpy).toHaveBeenCalledWith({
2474
+ type: TaskEvent.ASSIGN,
2475
+ taskData: payload,
2476
+ });
2477
+
2478
+ sendStateMachineEventSpy.mockRestore();
2479
2479
  });
2480
2480
  });
2481
-
2482
2481
  });
2483
-