@webex/contact-center 3.12.0-next.13 → 3.12.0-next.130

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 (312) hide show
  1. package/.sdd/manifest.json +884 -0
  2. package/AGENTS.md +94 -0
  3. package/ai-docs/ARCHITECTURE.md +168 -0
  4. package/ai-docs/CONTRACTS.md +53 -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 +52 -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 +382 -0
  17. package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
  18. package/ai-docs/features/contact-owner-change-refactor.md +78 -0
  19. package/ai-docs/features/generated-spec-conformance-fidelity-remediation/spec/feature-spec.md +117 -0
  20. package/ai-docs/features/residual-warning-coverage-completion/spec/feature-spec.md +203 -0
  21. package/ai-docs/features/validator-code-fidelity-drift-fix/spec/feature-spec.md +315 -0
  22. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  23. package/ai-docs/patterns/testing-patterns.md +480 -0
  24. package/ai-docs/patterns/typescript-patterns.md +365 -0
  25. package/ai-docs/templates/README.md +102 -0
  26. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  27. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  28. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  29. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  30. package/ai-docs/templates/new-method/00-master.md +80 -0
  31. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  32. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  33. package/ai-docs/templates/new-method/03-tests.md +201 -0
  34. package/ai-docs/templates/new-method/04-validation.md +141 -0
  35. package/ai-docs/templates/new-service/00-master.md +109 -0
  36. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  37. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  38. package/ai-docs/templates/new-service/03-integration.md +178 -0
  39. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  40. package/ai-docs/templates/new-service/05-validation.md +145 -0
  41. package/dist/cc.js +971 -42
  42. package/dist/cc.js.map +1 -1
  43. package/dist/config.js +13 -0
  44. package/dist/config.js.map +1 -1
  45. package/dist/constants.js +31 -3
  46. package/dist/constants.js.map +1 -1
  47. package/dist/index.js +45 -15
  48. package/dist/index.js.map +1 -1
  49. package/dist/metrics/MetricsManager.js +16 -0
  50. package/dist/metrics/MetricsManager.js.map +1 -1
  51. package/dist/metrics/behavioral-events.js +205 -0
  52. package/dist/metrics/behavioral-events.js.map +1 -1
  53. package/dist/metrics/constants.js +48 -4
  54. package/dist/metrics/constants.js.map +1 -1
  55. package/dist/services/AddressBook.js +18 -15
  56. package/dist/services/AddressBook.js.map +1 -1
  57. package/dist/services/AnswerCallOnWebexService.js +202 -0
  58. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  59. package/dist/services/ApiAiAssistant.js +269 -9
  60. package/dist/services/ApiAiAssistant.js.map +1 -1
  61. package/dist/services/EntryPoint.js +46 -68
  62. package/dist/services/EntryPoint.js.map +1 -1
  63. package/dist/services/Queue.js +27 -22
  64. package/dist/services/Queue.js.map +1 -1
  65. package/dist/services/UserPreference.js +427 -0
  66. package/dist/services/UserPreference.js.map +1 -0
  67. package/dist/services/WebexCrossClientService.js +222 -0
  68. package/dist/services/WebexCrossClientService.js.map +1 -0
  69. package/dist/services/WxAppTelephonyMercurySync.js +108 -0
  70. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  71. package/dist/services/config/Util.js +11 -4
  72. package/dist/services/config/Util.js.map +1 -1
  73. package/dist/services/config/constants.js +41 -7
  74. package/dist/services/config/constants.js.map +1 -1
  75. package/dist/services/config/index.js +51 -0
  76. package/dist/services/config/index.js.map +1 -1
  77. package/dist/services/config/types.js +57 -12
  78. package/dist/services/config/types.js.map +1 -1
  79. package/dist/services/core/Err.js.map +1 -1
  80. package/dist/services/core/Utils.js +111 -32
  81. package/dist/services/core/Utils.js.map +1 -1
  82. package/dist/services/core/WebexRequest.js +6 -2
  83. package/dist/services/core/WebexRequest.js.map +1 -1
  84. package/dist/services/core/aqm-reqs.js +28 -14
  85. package/dist/services/core/aqm-reqs.js.map +1 -1
  86. package/dist/services/core/types.js.map +1 -1
  87. package/dist/services/core/websocket/WebSocketManager.js +2 -1
  88. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  89. package/dist/services/core/websocket/types.js.map +1 -1
  90. package/dist/services/index.js +1 -1
  91. package/dist/services/index.js.map +1 -1
  92. package/dist/services/task/Task.js +754 -0
  93. package/dist/services/task/Task.js.map +1 -0
  94. package/dist/services/task/TaskFactory.js +49 -0
  95. package/dist/services/task/TaskFactory.js.map +1 -0
  96. package/dist/services/task/TaskManager.js +1172 -521
  97. package/dist/services/task/TaskManager.js.map +1 -1
  98. package/dist/services/task/TaskUtils.js +211 -27
  99. package/dist/services/task/TaskUtils.js.map +1 -1
  100. package/dist/services/task/WebexCallingUtils.js +70 -0
  101. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  102. package/dist/services/task/constants.js +12 -2
  103. package/dist/services/task/constants.js.map +1 -1
  104. package/dist/services/task/contact.js +29 -0
  105. package/dist/services/task/contact.js.map +1 -1
  106. package/dist/services/task/dialer.js +78 -0
  107. package/dist/services/task/dialer.js.map +1 -1
  108. package/dist/services/task/digital/Digital.js +78 -0
  109. package/dist/services/task/digital/Digital.js.map +1 -0
  110. package/dist/services/task/state-machine/TaskStateMachine.js +982 -0
  111. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  112. package/dist/services/task/state-machine/actions.js +572 -0
  113. package/dist/services/task/state-machine/actions.js.map +1 -0
  114. package/dist/services/task/state-machine/constants.js +161 -0
  115. package/dist/services/task/state-machine/constants.js.map +1 -0
  116. package/dist/services/task/state-machine/guards.js +379 -0
  117. package/dist/services/task/state-machine/guards.js.map +1 -0
  118. package/dist/services/task/state-machine/index.js +53 -0
  119. package/dist/services/task/state-machine/index.js.map +1 -0
  120. package/dist/services/task/state-machine/types.js +54 -0
  121. package/dist/services/task/state-machine/types.js.map +1 -0
  122. package/dist/services/task/state-machine/uiControlsComputer.js +703 -0
  123. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  124. package/dist/services/task/taskDataNormalizer.js +99 -0
  125. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  126. package/dist/services/task/types.js +228 -5
  127. package/dist/services/task/types.js.map +1 -1
  128. package/dist/services/task/voice/Voice.js +1485 -0
  129. package/dist/services/task/voice/Voice.js.map +1 -0
  130. package/dist/services/task/voice/WebRTC.js +152 -0
  131. package/dist/services/task/voice/WebRTC.js.map +1 -0
  132. package/dist/services/task/voice/wxAppVoiceMethods.js +354 -0
  133. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  134. package/dist/services/wxAppDiagnosticLogging.js +122 -0
  135. package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
  136. package/dist/services/wxAppTelephonyUtils.js +19 -0
  137. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  138. package/dist/types/cc.d.ts +193 -6
  139. package/dist/types/config.d.ts +13 -0
  140. package/dist/types/constants.d.ts +29 -1
  141. package/dist/types/index.d.ts +35 -8
  142. package/dist/types/metrics/MetricsManager.d.ts +6 -0
  143. package/dist/types/metrics/constants.d.ts +43 -1
  144. package/dist/types/services/AddressBook.d.ts +2 -1
  145. package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -0
  146. package/dist/types/services/ApiAiAssistant.d.ts +56 -5
  147. package/dist/types/services/EntryPoint.d.ts +7 -5
  148. package/dist/types/services/Queue.d.ts +5 -3
  149. package/dist/types/services/UserPreference.d.ts +118 -0
  150. package/dist/types/services/WebexCrossClientService.d.ts +32 -0
  151. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  152. package/dist/types/services/config/constants.d.ts +34 -5
  153. package/dist/types/services/config/types.d.ts +197 -12
  154. package/dist/types/services/core/Err.d.ts +6 -0
  155. package/dist/types/services/core/Utils.d.ts +35 -13
  156. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  157. package/dist/types/services/core/types.d.ts +2 -0
  158. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
  159. package/dist/types/services/core/websocket/types.d.ts +1 -1
  160. package/dist/types/services/index.d.ts +1 -1
  161. package/dist/types/services/task/Task.d.ts +175 -0
  162. package/dist/types/services/task/TaskFactory.d.ts +13 -0
  163. package/dist/types/services/task/TaskUtils.d.ts +48 -2
  164. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  165. package/dist/types/services/task/constants.d.ts +10 -0
  166. package/dist/types/services/task/contact.d.ts +4 -0
  167. package/dist/types/services/task/dialer.d.ts +30 -0
  168. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  169. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1442 -0
  170. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  171. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  172. package/dist/types/services/task/state-machine/guards.d.ts +103 -0
  173. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  174. package/dist/types/services/task/state-machine/types.d.ts +277 -0
  175. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  176. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  177. package/dist/types/services/task/types.d.ts +680 -66
  178. package/dist/types/services/task/voice/Voice.d.ts +227 -0
  179. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  180. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +55 -0
  181. package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
  182. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  183. package/dist/types/types.d.ts +205 -9
  184. package/dist/types/utils/PageCache.d.ts +20 -3
  185. package/dist/types/webex.d.ts +1 -0
  186. package/dist/types.js +162 -4
  187. package/dist/types.js.map +1 -1
  188. package/dist/utils/PageCache.js +19 -5
  189. package/dist/utils/PageCache.js.map +1 -1
  190. package/dist/webex.js +22 -2
  191. package/dist/webex.js.map +1 -1
  192. package/package.json +16 -12
  193. package/src/cc.ts +1289 -112
  194. package/src/config.ts +13 -0
  195. package/src/constants.ts +29 -1
  196. package/src/index.ts +36 -5
  197. package/src/metrics/MetricsManager.ts +16 -0
  198. package/src/metrics/ai-docs/AGENTS.md +350 -0
  199. package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
  200. package/src/metrics/ai-docs/metrics-spec.md +921 -0
  201. package/src/metrics/behavioral-events.ts +212 -0
  202. package/src/metrics/constants.ts +52 -4
  203. package/src/services/AddressBook.ts +17 -6
  204. package/src/services/AnswerCallOnWebexService.ts +239 -0
  205. package/src/services/ApiAiAssistant.ts +356 -9
  206. package/src/services/EntryPoint.ts +59 -60
  207. package/src/services/Queue.ts +29 -12
  208. package/src/services/UserPreference.ts +509 -0
  209. package/src/services/WebexCrossClientService.ts +293 -0
  210. package/src/services/WxAppTelephonyMercurySync.ts +142 -0
  211. package/src/services/agent/ai-docs/AGENTS.md +240 -0
  212. package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
  213. package/src/services/agent/ai-docs/agent-spec.md +505 -0
  214. package/src/services/ai-docs/AGENTS.md +386 -0
  215. package/src/services/ai-docs/services-spec.md +505 -0
  216. package/src/services/config/Util.ts +12 -3
  217. package/src/services/config/ai-docs/AGENTS.md +255 -0
  218. package/src/services/config/ai-docs/ARCHITECTURE.md +426 -0
  219. package/src/services/config/ai-docs/config-spec.md +686 -0
  220. package/src/services/config/constants.ts +46 -6
  221. package/src/services/config/index.ts +65 -0
  222. package/src/services/config/types.ts +197 -13
  223. package/src/services/core/Err.ts +3 -0
  224. package/src/services/core/Utils.ts +127 -37
  225. package/src/services/core/WebexRequest.ts +3 -1
  226. package/src/services/core/ai-docs/AGENTS.md +381 -0
  227. package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
  228. package/src/services/core/ai-docs/core-spec.md +787 -0
  229. package/src/services/core/aqm-reqs.ts +30 -15
  230. package/src/services/core/types.ts +2 -0
  231. package/src/services/core/websocket/WebSocketManager.ts +2 -0
  232. package/src/services/core/websocket/types.ts +1 -1
  233. package/src/services/index.ts +1 -1
  234. package/src/services/task/Task.ts +908 -0
  235. package/src/services/task/TaskFactory.ts +60 -0
  236. package/src/services/task/TaskManager.ts +1419 -608
  237. package/src/services/task/TaskUtils.ts +303 -25
  238. package/src/services/task/WebexCallingUtils.ts +136 -0
  239. package/src/services/task/ai-docs/AGENTS.md +457 -0
  240. package/src/services/task/ai-docs/ARCHITECTURE.md +595 -0
  241. package/src/services/task/ai-docs/task-spec.md +1474 -0
  242. package/src/services/task/constants.ts +10 -0
  243. package/src/services/task/contact.ts +30 -0
  244. package/src/services/task/dialer.ts +80 -0
  245. package/src/services/task/digital/Digital.ts +97 -0
  246. package/src/services/task/state-machine/TaskStateMachine.ts +1320 -0
  247. package/src/services/task/state-machine/actions.ts +741 -0
  248. package/src/services/task/state-machine/ai-docs/AGENTS.md +460 -0
  249. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1156 -0
  250. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2247 -0
  251. package/src/services/task/state-machine/constants.ts +172 -0
  252. package/src/services/task/state-machine/guards.ts +442 -0
  253. package/src/services/task/state-machine/index.ts +28 -0
  254. package/src/services/task/state-machine/types.ts +258 -0
  255. package/src/services/task/state-machine/uiControlsComputer.ts +1135 -0
  256. package/src/services/task/taskDataNormalizer.ts +137 -0
  257. package/src/services/task/types.ts +819 -70
  258. package/src/services/task/voice/Voice.ts +1851 -0
  259. package/src/services/task/voice/WebRTC.ts +191 -0
  260. package/src/services/task/voice/wxAppVoiceMethods.ts +516 -0
  261. package/src/services/wxAppDiagnosticLogging.ts +174 -0
  262. package/src/services/wxAppTelephonyUtils.ts +14 -0
  263. package/src/types.ts +262 -11
  264. package/src/utils/AGENTS.md +289 -0
  265. package/src/utils/PageCache.ts +38 -5
  266. package/src/utils/ai-docs/utils-spec.md +391 -0
  267. package/src/webex.js +10 -0
  268. package/test/unit/spec/cc.ts +2210 -119
  269. package/test/unit/spec/logger-proxy.ts +70 -0
  270. package/test/unit/spec/metrics/MetricsManager.ts +25 -0
  271. package/test/unit/spec/metrics/behavioral-events.ts +60 -0
  272. package/test/unit/spec/services/AddressBook.ts +37 -6
  273. package/test/unit/spec/services/AnswerCallOnWebexService.ts +264 -0
  274. package/test/unit/spec/services/ApiAiAssistant.ts +311 -21
  275. package/test/unit/spec/services/EntryPoint.ts +87 -40
  276. package/test/unit/spec/services/Queue.ts +123 -12
  277. package/test/unit/spec/services/UserPreference.ts +401 -0
  278. package/test/unit/spec/services/WebCallingService.ts +7 -1
  279. package/test/unit/spec/services/WebexCrossClientService.ts +467 -0
  280. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +136 -0
  281. package/test/unit/spec/services/config/Util.ts +129 -0
  282. package/test/unit/spec/services/config/index.ts +97 -29
  283. package/test/unit/spec/services/core/Utils.ts +425 -8
  284. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  285. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  286. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +66 -40
  287. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  288. package/test/unit/spec/services/task/Task.ts +745 -0
  289. package/test/unit/spec/services/task/TaskFactory.ts +99 -0
  290. package/test/unit/spec/services/task/TaskManager.ts +2518 -1494
  291. package/test/unit/spec/services/task/TaskUtils.ts +235 -0
  292. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  293. package/test/unit/spec/services/task/contact.ts +33 -0
  294. package/test/unit/spec/services/task/dialer.ts +190 -0
  295. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  296. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +4060 -0
  297. package/test/unit/spec/services/task/state-machine/guards.ts +839 -0
  298. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  299. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +3101 -0
  300. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  301. package/test/unit/spec/services/task/voice/Voice.ts +2061 -0
  302. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  303. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +603 -0
  304. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
  305. package/test/unit/spec/wellnessSampleUtils.ts +288 -0
  306. package/umd/contact-center.min.js +2 -2
  307. package/umd/contact-center.min.js.map +1 -1
  308. package/dist/services/task/index.js +0 -1525
  309. package/dist/services/task/index.js.map +0 -1
  310. package/dist/types/services/task/index.d.ts +0 -650
  311. package/src/services/task/index.ts +0 -1801
  312. package/test/unit/spec/services/task/index.ts +0 -2184
@@ -0,0 +1,4060 @@
1
+ import {createActor} from 'xstate';
2
+ import {
3
+ createTaskStateMachine,
4
+ TaskEvent,
5
+ TaskState,
6
+ } from '../../../../../../src/services/task/state-machine';
7
+ import {createTaskData} from '../taskTestUtils';
8
+ import {guards} from '../../../../../../src/services/task/state-machine/guards';
9
+
10
+ const createConfig = () => ({
11
+ channelType: 'voice' as const,
12
+ isEndTaskEnabled: true,
13
+ isEndConsultEnabled: true,
14
+ voiceVariant: 'pstn' as const,
15
+ isRecordingEnabled: true,
16
+ });
17
+
18
+ /** Mirrors Task.emitTaskWrapup: publishWrapup spy only runs when context would emit task:wrapup. */
19
+ function createPublishAwareEmitTaskWrapupMock() {
20
+ const publishWrapup = jest.fn();
21
+ const emitTaskWrapup = ({context}: {context: {taskData?: {wrapUpRequired?: boolean}}}) => {
22
+ if (context.taskData?.wrapUpRequired) {
23
+ publishWrapup();
24
+ }
25
+ };
26
+
27
+ return {emitTaskWrapup, publishWrapup};
28
+ }
29
+
30
+ describe('Task state machine', () => {
31
+ const startMachine = () => {
32
+ const actor = createActor(createTaskStateMachine(createConfig()));
33
+ actor.start();
34
+
35
+ return actor;
36
+ };
37
+
38
+ describe('recording state derivation', () => {
39
+ it('captures recording flags from offer payload', () => {
40
+ const service = startMachine();
41
+ const taskData = createTaskData({
42
+ interaction: {
43
+ callProcessingDetails: {
44
+ recordInProgress: true,
45
+ isPaused: false,
46
+ },
47
+ } as any,
48
+ });
49
+
50
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
51
+
52
+ const snapshot = service.getSnapshot();
53
+ expect(snapshot.context.recordingControlsAvailable).toBe(true);
54
+ expect(snapshot.context.recordingInProgress).toBe(true);
55
+ });
56
+
57
+ it('updates recordingPaused when ASSIGN payload reports pause', () => {
58
+ const service = startMachine();
59
+ const initialTaskData = createTaskData();
60
+ const pausedTaskData = createTaskData({
61
+ interaction: {
62
+ callProcessingDetails: {
63
+ isPaused: true,
64
+ },
65
+ } as any,
66
+ });
67
+
68
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: initialTaskData});
69
+ service.send({type: TaskEvent.ASSIGN, taskData: pausedTaskData});
70
+
71
+ const snapshot = service.getSnapshot();
72
+ expect(snapshot.context.recordingControlsAvailable).toBe(true);
73
+ expect(snapshot.context.recordingInProgress).toBe(false);
74
+ });
75
+
76
+ it('updates recording state when recording started event arrives', () => {
77
+ const service = startMachine();
78
+ const initialTaskData = createTaskData();
79
+ const recordingTaskData = createTaskData({
80
+ interaction: {
81
+ callProcessingDetails: {
82
+ recordingStarted: true,
83
+ recordInProgress: true,
84
+ },
85
+ } as any,
86
+ });
87
+
88
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: initialTaskData});
89
+ service.send({type: TaskEvent.ASSIGN, taskData: initialTaskData});
90
+ service.send({type: TaskEvent.RECORDING_STARTED, taskData: recordingTaskData});
91
+
92
+ const snapshot = service.getSnapshot();
93
+ expect(snapshot.value).toBe(TaskState.CONNECTED);
94
+ expect(snapshot.context.recordingControlsAvailable).toBe(true);
95
+ expect(snapshot.context.recordingInProgress).toBe(true);
96
+ });
97
+ });
98
+
99
+ describe('hold and resume flow', () => {
100
+ it('moves through HOLD -> HELD -> CONNECTED on success events', () => {
101
+ const service = startMachine();
102
+ const taskData = createTaskData();
103
+
104
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
105
+ service.send({type: TaskEvent.ASSIGN, taskData});
106
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
107
+
108
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
109
+ expect(service.getSnapshot().value).toBe(TaskState.HOLD_INITIATING);
110
+
111
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId});
112
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
113
+
114
+ service.send({type: TaskEvent.UNHOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
115
+ expect(service.getSnapshot().value).toBe(TaskState.RESUME_INITIATING);
116
+
117
+ service.send({type: TaskEvent.UNHOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId});
118
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
119
+ });
120
+ });
121
+
122
+ describe('partial lifecycle payloads', () => {
123
+ const createActiveMainCallTaskData = (isHold = false) =>
124
+ createTaskData({
125
+ agentId: 'agent-1',
126
+ interactionId: 'interaction-1',
127
+ mediaResourceId: 'main-media',
128
+ interaction: {
129
+ state: 'connected',
130
+ participants: {
131
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
132
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
133
+ },
134
+ media: {
135
+ 'main-media': {
136
+ mediaResourceId: 'main-media',
137
+ mType: 'mainCall',
138
+ participants: ['agent-1', 'customer-1'],
139
+ isHold,
140
+ },
141
+ },
142
+ } as any,
143
+ });
144
+
145
+ const createPartialMainCallTaskData = (isHold = false, omitSelf = false) =>
146
+ createTaskData({
147
+ agentId: 'agent-1',
148
+ interactionId: 'interaction-1',
149
+ mediaResourceId: 'main-media',
150
+ interaction: {
151
+ state: 'connected',
152
+ participants: omitSelf
153
+ ? {'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false}}
154
+ : {
155
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
156
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
157
+ },
158
+ media: {
159
+ 'main-media': {
160
+ mediaResourceId: 'main-media',
161
+ mType: 'mainCall',
162
+ participants: ['customer-1'],
163
+ isHold,
164
+ },
165
+ },
166
+ } as any,
167
+ });
168
+
169
+ const startMachineWithEndSpy = () => {
170
+ const emitTaskEnd = jest.fn();
171
+ const service = createActor(
172
+ createTaskStateMachine({...createConfig(), agentId: 'agent-1'}, {actions: {emitTaskEnd}})
173
+ );
174
+ service.start();
175
+
176
+ return {service, emitTaskEnd};
177
+ };
178
+
179
+ const enterTransientHoldState = (
180
+ service: ReturnType<typeof startMachine>,
181
+ state: TaskState.HOLD_INITIATING | TaskState.RESUME_INITIATING,
182
+ taskData: ReturnType<typeof createActiveMainCallTaskData>
183
+ ) => {
184
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
185
+ service.send({type: TaskEvent.ASSIGN, taskData});
186
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: 'main-media'});
187
+
188
+ if (state === TaskState.RESUME_INITIATING) {
189
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: 'main-media'});
190
+ service.send({type: TaskEvent.UNHOLD_INITIATED, mediaResourceId: 'main-media'});
191
+ }
192
+
193
+ expect(service.getSnapshot().value).toBe(state);
194
+ };
195
+
196
+ it('keeps CONNECTED when another participant leave event has a partial mainCall', () => {
197
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
198
+ const taskData = createActiveMainCallTaskData();
199
+
200
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
201
+ service.send({type: TaskEvent.ASSIGN, taskData});
202
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
203
+
204
+ service.send({
205
+ type: TaskEvent.PARTICIPANT_LEAVE,
206
+ participantId: 'agent-2',
207
+ taskData: createPartialMainCallTaskData(false, true),
208
+ });
209
+
210
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
211
+ expect(emitTaskEnd).not.toHaveBeenCalled();
212
+ });
213
+
214
+ it('ignores PARTICIPANT_LEAVE in CONNECTED even when it identifies the current agent', () => {
215
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
216
+ const taskData = createActiveMainCallTaskData();
217
+
218
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
219
+ service.send({type: TaskEvent.ASSIGN, taskData});
220
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
221
+
222
+ service.send({
223
+ type: TaskEvent.PARTICIPANT_LEAVE,
224
+ participantId: 'agent-1',
225
+ taskData: createPartialMainCallTaskData(false, true),
226
+ });
227
+
228
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
229
+ expect(emitTaskEnd).not.toHaveBeenCalled();
230
+ });
231
+
232
+ it('enters WRAPPING_UP on TASK_WRAPUP while HOLD_INITIATING and emits wrap-up once', () => {
233
+ const emitTaskWrapup = jest.fn();
234
+ const service = createActor(
235
+ createTaskStateMachine({...createConfig(), agentId: 'agent-1'}, {actions: {emitTaskWrapup}})
236
+ );
237
+ service.start();
238
+ const taskData = createActiveMainCallTaskData();
239
+
240
+ enterTransientHoldState(service, TaskState.HOLD_INITIATING, taskData);
241
+ service.send({
242
+ type: TaskEvent.TASK_WRAPUP,
243
+ taskData: {...taskData, wrapUpRequired: true},
244
+ });
245
+
246
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
247
+ expect(emitTaskWrapup).toHaveBeenCalledTimes(1);
248
+ });
249
+
250
+ it('ignores PARTICIPANT_LEAVE in HOLD_INITIATING when a remote Drop identifies the current agent', () => {
251
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
252
+ const taskData = createActiveMainCallTaskData();
253
+
254
+ enterTransientHoldState(service, TaskState.HOLD_INITIATING, taskData);
255
+ service.send({
256
+ type: TaskEvent.PARTICIPANT_LEAVE,
257
+ participantId: 'agent-1',
258
+ taskData: createPartialMainCallTaskData(false, true),
259
+ });
260
+
261
+ expect(service.getSnapshot().value).toBe(TaskState.HOLD_INITIATING);
262
+ expect(emitTaskEnd).not.toHaveBeenCalled();
263
+ });
264
+
265
+ it('terminates RESUME_INITIATING when a remote Drop identifies the current agent', () => {
266
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
267
+ const taskData = createActiveMainCallTaskData(true);
268
+
269
+ enterTransientHoldState(service, TaskState.RESUME_INITIATING, taskData);
270
+ service.send({
271
+ type: TaskEvent.PARTICIPANT_LEAVE,
272
+ participantId: 'agent-1',
273
+ taskData: createPartialMainCallTaskData(true, true),
274
+ });
275
+
276
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
277
+ expect(emitTaskEnd).toHaveBeenCalledTimes(1);
278
+ });
279
+
280
+ it('keeps HOLD_INITIATING when ParticipantLeftConference identifies another participant', () => {
281
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
282
+ const taskData = createActiveMainCallTaskData();
283
+
284
+ enterTransientHoldState(service, TaskState.HOLD_INITIATING, taskData);
285
+ service.send({
286
+ type: TaskEvent.PARTICIPANT_LEAVE,
287
+ participantId: 'agent-2',
288
+ taskData: createPartialMainCallTaskData(false, true),
289
+ });
290
+
291
+ expect(service.getSnapshot().value).toBe(TaskState.HOLD_INITIATING);
292
+ expect(emitTaskEnd).not.toHaveBeenCalled();
293
+ });
294
+
295
+ it('terminates RESUME_INITIATING when another participant leave omits self from the roster', () => {
296
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
297
+ const taskData = createActiveMainCallTaskData(true);
298
+
299
+ enterTransientHoldState(service, TaskState.RESUME_INITIATING, taskData);
300
+ service.send({
301
+ type: TaskEvent.PARTICIPANT_LEAVE,
302
+ participantId: 'agent-2',
303
+ taskData: createPartialMainCallTaskData(true, true),
304
+ });
305
+
306
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
307
+ expect(emitTaskEnd).toHaveBeenCalledTimes(1);
308
+ });
309
+
310
+ it('terminates HELD when another participant leave event omits self from its roster', () => {
311
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
312
+ const taskData = createActiveMainCallTaskData(true);
313
+
314
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
315
+ service.send({type: TaskEvent.ASSIGN, taskData});
316
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: 'main-media'});
317
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: 'main-media'});
318
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
319
+
320
+ service.send({
321
+ type: TaskEvent.PARTICIPANT_LEAVE,
322
+ participantId: 'agent-2',
323
+ taskData: createPartialMainCallTaskData(true, true),
324
+ });
325
+
326
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
327
+ expect(emitTaskEnd).toHaveBeenCalledTimes(1);
328
+ });
329
+
330
+ it('keeps HELD when CONSULT_END has a partial mainCall', () => {
331
+ const {service, emitTaskEnd} = startMachineWithEndSpy();
332
+ const taskData = createActiveMainCallTaskData(true);
333
+
334
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
335
+ service.send({type: TaskEvent.ASSIGN, taskData});
336
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: 'main-media'});
337
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: 'main-media'});
338
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
339
+
340
+ service.send({
341
+ type: TaskEvent.CONSULT_END,
342
+ taskData: createPartialMainCallTaskData(true),
343
+ });
344
+
345
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
346
+ expect(emitTaskEnd).not.toHaveBeenCalled();
347
+ });
348
+ });
349
+
350
+ describe('recording pause/resume events', () => {
351
+ it('toggles recordingPaused flag based on events', () => {
352
+ const service = startMachine();
353
+ const taskData = createTaskData({
354
+ interaction: {
355
+ callProcessingDetails: {recordInProgress: true},
356
+ } as any,
357
+ });
358
+
359
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
360
+ service.send({type: TaskEvent.ASSIGN, taskData});
361
+ expect(service.getSnapshot().context.recordingInProgress).toBe(true);
362
+
363
+ service.send({type: TaskEvent.PAUSE_RECORDING});
364
+ expect(service.getSnapshot().context.recordingInProgress).toBe(false);
365
+
366
+ service.send({type: TaskEvent.RESUME_RECORDING});
367
+ expect(service.getSnapshot().context.recordingInProgress).toBe(true);
368
+ });
369
+
370
+ it('toggles recording state while task is held', () => {
371
+ const service = startMachine();
372
+ const taskData = createTaskData({
373
+ interaction: {
374
+ callProcessingDetails: {recordInProgress: true},
375
+ } as any,
376
+ });
377
+
378
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
379
+ service.send({type: TaskEvent.ASSIGN, taskData});
380
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
381
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId});
382
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
383
+ expect(service.getSnapshot().context.recordingInProgress).toBe(true);
384
+
385
+ service.send({type: TaskEvent.PAUSE_RECORDING});
386
+ expect(service.getSnapshot().context.recordingInProgress).toBe(false);
387
+
388
+ service.send({type: TaskEvent.RESUME_RECORDING});
389
+ expect(service.getSnapshot().context.recordingInProgress).toBe(true);
390
+ });
391
+ });
392
+
393
+ describe('wrap-up and completion flow', () => {
394
+ it('moves from CONNECTED -> WRAPPING_UP -> COMPLETED on END/WRAPUP_COMPLETE', () => {
395
+ const service = startMachine();
396
+ const taskData = createTaskData();
397
+
398
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
399
+ service.send({type: TaskEvent.ASSIGN, taskData});
400
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
401
+
402
+ service.send({type: TaskEvent.TASK_WRAPUP});
403
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
404
+
405
+ service.send({type: TaskEvent.WRAPUP_COMPLETE});
406
+ expect(service.getSnapshot().value).toBe(TaskState.COMPLETED);
407
+ });
408
+
409
+ it('handles CONTACT_ENDED by entering wrapping up before completion', () => {
410
+ const service = startMachine();
411
+ // Primary agent (isConsulted: false) should go to WRAPPING_UP
412
+ const taskData = createTaskData({isConsulted: false} as any);
413
+
414
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
415
+ service.send({type: TaskEvent.ASSIGN, taskData});
416
+
417
+ // CONTACT_ENDED event must include taskData for shouldWrapUpForThisAgent check
418
+ service.send({type: TaskEvent.CONTACT_ENDED, taskData});
419
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
420
+
421
+ service.send({type: TaskEvent.WRAPUP_COMPLETE});
422
+ expect(service.getSnapshot().value).toBe(TaskState.COMPLETED);
423
+ });
424
+
425
+ it('emits task wrappedup before cleanup on COMPLETED entry', () => {
426
+ const actionOrder: string[] = [];
427
+ const actor = createActor(
428
+ createTaskStateMachine(createConfig(), {
429
+ actions: {
430
+ emitTaskWrappedup: () => {
431
+ actionOrder.push('emitTaskWrappedup');
432
+ },
433
+ cleanupResources: () => {
434
+ actionOrder.push('cleanupResources');
435
+ },
436
+ },
437
+ })
438
+ );
439
+ actor.start();
440
+ const taskData = createTaskData();
441
+
442
+ actor.send({type: TaskEvent.TASK_INCOMING, taskData});
443
+ actor.send({type: TaskEvent.ASSIGN, taskData});
444
+ actor.send({type: TaskEvent.TASK_WRAPUP});
445
+ actor.send({type: TaskEvent.WRAPUP_COMPLETE});
446
+
447
+ expect(actor.getSnapshot().value).toBe(TaskState.COMPLETED);
448
+ expect(actionOrder).toEqual(['emitTaskWrappedup', 'cleanupResources']);
449
+ });
450
+ });
451
+
452
+ describe('consult and conference flows', () => {
453
+ const createSingleAgentConferenceTaskData = (interactionState: string, isHold = false) =>
454
+ createTaskData({
455
+ interactionId: 'interaction-1',
456
+ mediaResourceId: 'interaction-1',
457
+ interaction: {
458
+ state: interactionState,
459
+ mainInteractionId: 'interaction-1',
460
+ interactionId: 'interaction-1',
461
+ participants: {
462
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
463
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
464
+ },
465
+ media: {
466
+ 'interaction-1': {
467
+ mediaResourceId: 'interaction-1',
468
+ mType: 'mainCall',
469
+ participants: ['agent-1', 'customer-1'],
470
+ isHold,
471
+ },
472
+ },
473
+ } as any,
474
+ });
475
+
476
+ const createConferenceConsultTaskData = ({
477
+ interactionState,
478
+ includeSecondAgent,
479
+ conferenceHoldParticipant,
480
+ isMainHeld = false,
481
+ }: {
482
+ interactionState: string;
483
+ includeSecondAgent: boolean;
484
+ conferenceHoldParticipant: boolean | string;
485
+ isMainHeld?: boolean;
486
+ }) =>
487
+ createTaskData({
488
+ interactionId: 'interaction-1',
489
+ mediaResourceId: 'interaction-1',
490
+ consultMediaResourceId: 'consult-media-1',
491
+ interaction: {
492
+ state: interactionState,
493
+ mainInteractionId: 'interaction-1',
494
+ interactionId: 'interaction-1',
495
+ callProcessingDetails: {
496
+ conferenceHoldParticipant,
497
+ },
498
+ participants: {
499
+ 'agent-1': {
500
+ id: 'agent-1',
501
+ pType: 'Agent',
502
+ hasLeft: false,
503
+ consultState: 'consulting',
504
+ },
505
+ ...(includeSecondAgent
506
+ ? {
507
+ 'agent-2': {
508
+ id: 'agent-2',
509
+ pType: 'Agent',
510
+ hasLeft: false,
511
+ },
512
+ }
513
+ : {}),
514
+ 'agent-3': {
515
+ id: 'agent-3',
516
+ pType: 'Agent',
517
+ hasLeft: false,
518
+ isConsulted: true,
519
+ consultState: 'consulting',
520
+ },
521
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
522
+ },
523
+ media: {
524
+ 'interaction-1': {
525
+ mediaResourceId: 'interaction-1',
526
+ mType: 'mainCall',
527
+ participants: includeSecondAgent
528
+ ? ['agent-1', 'agent-2', 'customer-1']
529
+ : ['agent-1', 'customer-1'],
530
+ isHold: isMainHeld,
531
+ },
532
+ 'consult-media-1': {
533
+ mediaResourceId: 'consult-media-1',
534
+ mType: 'consult',
535
+ participants: ['agent-1', 'agent-3'],
536
+ isHold: false,
537
+ },
538
+ },
539
+ } as any,
540
+ });
541
+
542
+ it('hydrates a recovered active conference from IDLE to CONFERENCING', () => {
543
+ const service = startMachine();
544
+ const taskData = createTaskData({
545
+ agentId: 'agent-1',
546
+ interactionId: 'interaction-1',
547
+ mediaResourceId: 'interaction-1',
548
+ interaction: {
549
+ state: 'conference',
550
+ interactionId: 'interaction-1',
551
+ mainInteractionId: 'interaction-1',
552
+ owner: 'agent-1',
553
+ participants: {
554
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
555
+ 'agent-2': {id: 'agent-2', pType: 'Agent', hasLeft: false},
556
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
557
+ },
558
+ media: {
559
+ 'interaction-1': {
560
+ mediaResourceId: 'interaction-1',
561
+ mType: 'mainCall',
562
+ participants: ['agent-1', 'agent-2', 'customer-1'],
563
+ isHold: false,
564
+ },
565
+ },
566
+ } as any,
567
+ });
568
+
569
+ service.send({type: TaskEvent.HYDRATE, taskData});
570
+
571
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
572
+ expect(service.getSnapshot().context.taskData).toEqual(taskData);
573
+ });
574
+
575
+ it('boots from IDLE to CONSULTING on CONSULTING_ACTIVE for split-leg ordering', () => {
576
+ const service = startMachine();
577
+ const taskData = createTaskData({
578
+ consultingAgentId: 'agent-1',
579
+ isConsulted: false,
580
+ interaction: {
581
+ state: 'consulting',
582
+ } as any,
583
+ });
584
+
585
+ expect(service.getSnapshot().value).toBe(TaskState.IDLE);
586
+ service.send({
587
+ type: TaskEvent.CONSULTING_ACTIVE,
588
+ consultDestinationAgentJoined: true,
589
+ taskData,
590
+ });
591
+
592
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
593
+ expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(true);
594
+ });
595
+
596
+ it('hydrates to CONSULTING when top-level state is conference but self consultState is consulting', () => {
597
+ const service = startMachine();
598
+ const taskData = createTaskData({
599
+ isConsulted: false,
600
+ interaction: {
601
+ state: 'conference',
602
+ mainInteractionId: 'interaction-1',
603
+ interactionId: 'interaction-1',
604
+ participants: {
605
+ 'agent-1': {
606
+ id: 'agent-1',
607
+ pType: 'Agent',
608
+ hasLeft: false,
609
+ consultState: 'consulting',
610
+ isConsulted: false,
611
+ },
612
+ 'agent-2': {
613
+ id: 'agent-2',
614
+ pType: 'Agent',
615
+ hasLeft: false,
616
+ consultState: 'consulting',
617
+ isConsulted: true,
618
+ },
619
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
620
+ },
621
+ media: {
622
+ 'interaction-1': {
623
+ mediaResourceId: 'interaction-1',
624
+ mType: 'mainCall',
625
+ participants: ['agent-1', 'customer-1'],
626
+ isHold: true,
627
+ },
628
+ 'consult-media-1': {
629
+ mediaResourceId: 'consult-media-1',
630
+ mType: 'consult',
631
+ participants: ['agent-1', 'agent-2'],
632
+ isHold: false,
633
+ },
634
+ },
635
+ } as any,
636
+ });
637
+
638
+ service.send({type: TaskEvent.HYDRATE, taskData});
639
+
640
+ const snapshot = service.getSnapshot();
641
+ expect(snapshot.value).toBe(TaskState.CONSULTING);
642
+ expect(snapshot.context.consultInitiator).toBe(true);
643
+ expect(snapshot.context.consultFromConference).toBe(true);
644
+ });
645
+
646
+ it('hydrates consulted agent to CONSULTING when self consultState is consulting and main leg is held', () => {
647
+ const service = startMachine();
648
+ const taskData = createTaskData({
649
+ agentId: 'agent-2',
650
+ isConsulted: true,
651
+ interaction: {
652
+ state: 'conference',
653
+ mainInteractionId: 'interaction-1',
654
+ interactionId: 'interaction-1',
655
+ participants: {
656
+ 'agent-1': {
657
+ id: 'agent-1',
658
+ pType: 'Agent',
659
+ hasLeft: false,
660
+ consultState: 'consulting',
661
+ isConsulted: false,
662
+ },
663
+ 'agent-2': {
664
+ id: 'agent-2',
665
+ pType: 'Agent',
666
+ hasLeft: false,
667
+ consultState: 'consulting',
668
+ isConsulted: true,
669
+ },
670
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
671
+ },
672
+ media: {
673
+ 'interaction-1': {
674
+ mediaResourceId: 'interaction-1',
675
+ mType: 'mainCall',
676
+ participants: ['agent-1', 'customer-1'],
677
+ isHold: true,
678
+ },
679
+ 'consult-media-1': {
680
+ mediaResourceId: 'consult-media-1',
681
+ mType: 'consult',
682
+ participants: ['agent-1', 'agent-2'],
683
+ isHold: false,
684
+ },
685
+ },
686
+ } as any,
687
+ });
688
+
689
+ service.send({type: TaskEvent.HYDRATE, taskData});
690
+
691
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
692
+ });
693
+
694
+ it('hydrates to CONSULTING when consult is pending (self consultState is consultInitiated)', () => {
695
+ const service = startMachine();
696
+ const taskData = createTaskData({
697
+ isConsulted: false,
698
+ interaction: {
699
+ state: 'conference',
700
+ mainInteractionId: 'interaction-1',
701
+ interactionId: 'interaction-1',
702
+ participants: {
703
+ 'agent-1': {
704
+ id: 'agent-1',
705
+ pType: 'Agent',
706
+ hasLeft: false,
707
+ consultState: 'consultInitiated',
708
+ isConsulted: false,
709
+ },
710
+ 'agent-2': {
711
+ id: 'agent-2',
712
+ pType: 'Agent',
713
+ hasLeft: false,
714
+ consultState: 'consultReserved',
715
+ isConsulted: true,
716
+ },
717
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
718
+ },
719
+ media: {
720
+ 'interaction-1': {
721
+ mediaResourceId: 'interaction-1',
722
+ mType: 'mainCall',
723
+ participants: ['agent-1', 'customer-1'],
724
+ isHold: true,
725
+ },
726
+ 'consult-media-1': {
727
+ mediaResourceId: 'consult-media-1',
728
+ mType: 'consult',
729
+ participants: ['agent-1', 'agent-2'],
730
+ isHold: false,
731
+ },
732
+ },
733
+ } as any,
734
+ });
735
+
736
+ service.send({type: TaskEvent.HYDRATE, taskData});
737
+
738
+ const snapshot = service.getSnapshot();
739
+ expect(snapshot.value).toBe(TaskState.CONSULTING);
740
+ expect(snapshot.context.consultInitiator).toBe(true);
741
+ expect(snapshot.context.consultFromConference).toBe(true);
742
+ });
743
+
744
+ it('tracks consult destination, agent join, and clears on consult end', () => {
745
+ const service = startMachine();
746
+ const taskData = createTaskData();
747
+
748
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
749
+ service.send({type: TaskEvent.ASSIGN, taskData});
750
+
751
+ service.send({
752
+ type: TaskEvent.CONSULT,
753
+ destination: 'agent-42',
754
+ destinationType: 'agent',
755
+ });
756
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
757
+ expect(service.getSnapshot().context.consultInitiator).toBe(true);
758
+
759
+ service.send({type: TaskEvent.CONSULT_SUCCESS});
760
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
761
+
762
+ service.send({
763
+ type: TaskEvent.CONSULTING_ACTIVE,
764
+ consultDestinationAgentJoined: true,
765
+ });
766
+ expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(true);
767
+
768
+ service.send({type: TaskEvent.CONSULT_END});
769
+ const snapshotAfterEnd = service.getSnapshot();
770
+ expect(snapshotAfterEnd.value).toBe(TaskState.HELD);
771
+ expect(snapshotAfterEnd.context.consultDestinationAgentJoined).toBe(false);
772
+ });
773
+
774
+ it('returns to HELD with main-leg controls after AgentConsultEnded from Stable Prod while CONSULTING', () => {
775
+ const service = startMachine();
776
+ const baseTaskData = createTaskData({
777
+ agentId: 'agent-1',
778
+ mediaResourceId: 'interaction-1',
779
+ });
780
+
781
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: baseTaskData});
782
+ service.send({type: TaskEvent.ASSIGN, taskData: baseTaskData});
783
+ service.send({
784
+ type: TaskEvent.CONSULT,
785
+ destination: 'agent-2',
786
+ destinationType: 'agent',
787
+ });
788
+ service.send({type: TaskEvent.CONSULT_SUCCESS});
789
+ service.send({type: TaskEvent.CONSULTING_ACTIVE, consultDestinationAgentJoined: true});
790
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
791
+
792
+ const consultEndedTaskData = createTaskData({
793
+ agentId: 'agent-1',
794
+ mediaResourceId: 'interaction-1',
795
+ type: 'AgentConsultEnded' as any,
796
+ isConsulted: false,
797
+ interaction: {
798
+ state: 'connected',
799
+ interactionId: 'interaction-1',
800
+ mainInteractionId: 'interaction-1',
801
+ owner: 'agent-1',
802
+ participants: {
803
+ 'agent-1': {
804
+ id: 'agent-1',
805
+ pType: 'Agent',
806
+ hasLeft: false,
807
+ consultState: 'consultCompleted',
808
+ isConsulted: false,
809
+ },
810
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
811
+ },
812
+ media: {
813
+ 'interaction-1': {
814
+ mediaResourceId: 'interaction-1',
815
+ mType: 'mainCall',
816
+ isHold: true,
817
+ participants: ['customer-1', 'agent-1'],
818
+ },
819
+ },
820
+ } as any,
821
+ });
822
+
823
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
824
+
825
+ const snapshot = service.getSnapshot();
826
+ expect(snapshot.value).toBe(TaskState.HELD);
827
+ expect(snapshot.context.consultInitiator).toBe(false);
828
+ expect(snapshot.context.consultCallHeld).toBe(false);
829
+ expect(snapshot.context.consultDestinationAgentJoined).toBe(false);
830
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
831
+ expect(snapshot.context.uiControls.consult.endConsult).toEqual({
832
+ isVisible: false,
833
+ isEnabled: false,
834
+ });
835
+ expect(snapshot.context.uiControls.main.hold).toEqual({
836
+ isVisible: true,
837
+ isEnabled: true,
838
+ });
839
+ expect(snapshot.context.uiControls.main.consult).toEqual({
840
+ isVisible: true,
841
+ isEnabled: true,
842
+ });
843
+ expect(snapshot.context.uiControls.main.transfer).toEqual({
844
+ isVisible: true,
845
+ isEnabled: true,
846
+ });
847
+ expect(snapshot.context.uiControls.main.recording).toEqual({
848
+ isVisible: true,
849
+ isEnabled: true,
850
+ });
851
+ });
852
+
853
+ it('enables main consult after ending consult before consultee answers (CONSULT_INITIATING -> CONSULT_END)', () => {
854
+ const service = startMachine();
855
+ const baseTaskData = createTaskData({
856
+ agentId: 'agent-1',
857
+ mediaResourceId: 'interaction-1',
858
+ });
859
+
860
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: baseTaskData});
861
+ service.send({type: TaskEvent.ASSIGN, taskData: baseTaskData});
862
+ service.send({
863
+ type: TaskEvent.CONSULT,
864
+ destination: 'agent-2',
865
+ destinationType: 'agent',
866
+ });
867
+ // Consult requested but consultee (agent-2) has not answered yet.
868
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
869
+
870
+ // Agent 1 ends the consult before agent-2 answers. Backend AgentConsultEnded:
871
+ // main on hold, self consultCompleted, no consult media, consultee not in participants.
872
+ const consultEndedTaskData = createTaskData({
873
+ agentId: 'agent-1',
874
+ mediaResourceId: 'interaction-1',
875
+ consultMediaResourceId: 'consult-media',
876
+ destAgentId: 'agent-2',
877
+ destinationType: 'Agent',
878
+ type: 'AgentConsultEnded' as any,
879
+ isConsulted: false,
880
+ interaction: {
881
+ state: 'connected',
882
+ interactionId: 'interaction-1',
883
+ mainInteractionId: 'interaction-1',
884
+ owner: 'agent-1',
885
+ participants: {
886
+ 'agent-1': {
887
+ id: 'agent-1',
888
+ pType: 'Agent',
889
+ hasLeft: false,
890
+ hasJoined: true,
891
+ consultState: 'consultCompleted',
892
+ isConsulted: false,
893
+ },
894
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
895
+ },
896
+ media: {
897
+ 'interaction-1': {
898
+ mediaResourceId: 'interaction-1',
899
+ mType: 'mainCall',
900
+ isHold: true,
901
+ participants: ['customer-1', 'agent-1'],
902
+ },
903
+ },
904
+ } as any,
905
+ });
906
+
907
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
908
+
909
+ const snapshot = service.getSnapshot();
910
+ expect(snapshot.value).toBe(TaskState.HELD);
911
+ expect(snapshot.context.consultInitiator).toBe(false);
912
+ expect(snapshot.context.consultDestinationAgentJoined).toBe(false);
913
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
914
+ expect(snapshot.context.uiControls.main.consult).toEqual({
915
+ isVisible: true,
916
+ isEnabled: true,
917
+ });
918
+ expect(snapshot.context.uiControls.consult.endConsult).toEqual({
919
+ isVisible: false,
920
+ isEnabled: false,
921
+ });
922
+ });
923
+
924
+ it('stays HELD and clears consult UI when AgentConsultEnded arrives on HELD state', () => {
925
+ const service = startMachine();
926
+ const heldTaskData = createTaskData({
927
+ agentId: 'agent-1',
928
+ mediaResourceId: 'interaction-1',
929
+ interaction: {
930
+ state: 'hold',
931
+ interactionId: 'interaction-1',
932
+ mainInteractionId: 'interaction-1',
933
+ owner: 'agent-1',
934
+ participants: {
935
+ 'agent-1': {
936
+ id: 'agent-1',
937
+ pType: 'Agent',
938
+ hasLeft: false,
939
+ consultState: 'consulting',
940
+ isConsulted: false,
941
+ },
942
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
943
+ },
944
+ media: {
945
+ 'interaction-1': {
946
+ mediaResourceId: 'interaction-1',
947
+ mType: 'mainCall',
948
+ isHold: true,
949
+ participants: ['customer-1', 'agent-1'],
950
+ },
951
+ },
952
+ } as any,
953
+ });
954
+
955
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: heldTaskData});
956
+ service.send({type: TaskEvent.ASSIGN, taskData: heldTaskData});
957
+ service.send({
958
+ type: TaskEvent.HOLD_INITIATED,
959
+ mediaResourceId: heldTaskData.mediaResourceId,
960
+ });
961
+ service.send({
962
+ type: TaskEvent.HOLD_SUCCESS,
963
+ mediaResourceId: heldTaskData.mediaResourceId,
964
+ taskData: heldTaskData,
965
+ });
966
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
967
+
968
+ const consultEndedTaskData = createTaskData({
969
+ agentId: 'agent-1',
970
+ mediaResourceId: 'interaction-1',
971
+ type: 'AgentConsultEnded' as any,
972
+ isConsulted: false,
973
+ interaction: {
974
+ state: 'hold',
975
+ interactionId: 'interaction-1',
976
+ mainInteractionId: 'interaction-1',
977
+ owner: 'agent-1',
978
+ participants: {
979
+ 'agent-1': {
980
+ id: 'agent-1',
981
+ pType: 'Agent',
982
+ hasLeft: false,
983
+ consultState: 'consultCompleted',
984
+ isConsulted: false,
985
+ },
986
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
987
+ },
988
+ media: {
989
+ 'interaction-1': {
990
+ mediaResourceId: 'interaction-1',
991
+ mType: 'mainCall',
992
+ isHold: true,
993
+ participants: ['customer-1', 'agent-1'],
994
+ },
995
+ },
996
+ } as any,
997
+ });
998
+
999
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
1000
+
1001
+ const snapshot = service.getSnapshot();
1002
+ expect(snapshot.value).toBe(TaskState.HELD);
1003
+ expect(snapshot.context.consultInitiator).toBe(false);
1004
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
1005
+ expect(snapshot.context.uiControls.main.hold).toEqual({
1006
+ isVisible: true,
1007
+ isEnabled: true,
1008
+ });
1009
+ expect(snapshot.context.uiControls.consult.endConsult).toEqual({
1010
+ isVisible: false,
1011
+ isEnabled: false,
1012
+ });
1013
+ });
1014
+
1015
+ it('keeps main.consult enabled on HELD after AgentConsultFailed then AgentConsultEnded (RONA)', () => {
1016
+ const service = startMachine();
1017
+ const heldTaskData = createTaskData({
1018
+ agentId: 'agent-1',
1019
+ mediaResourceId: 'interaction-1',
1020
+ type: 'AgentContactHeld' as any,
1021
+ interaction: {
1022
+ state: 'hold',
1023
+ interactionId: 'interaction-1',
1024
+ mainInteractionId: 'interaction-1',
1025
+ owner: 'agent-1',
1026
+ participants: {
1027
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
1028
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1029
+ } as any,
1030
+ media: {
1031
+ 'interaction-1': {
1032
+ mediaResourceId: 'interaction-1',
1033
+ mType: 'mainCall',
1034
+ isHold: true,
1035
+ participants: ['customer-1', 'agent-1'],
1036
+ },
1037
+ } as any,
1038
+ } as any,
1039
+ });
1040
+
1041
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: heldTaskData});
1042
+ service.send({type: TaskEvent.ASSIGN, taskData: heldTaskData});
1043
+ service.send({
1044
+ type: TaskEvent.HOLD_INITIATED,
1045
+ mediaResourceId: heldTaskData.mediaResourceId,
1046
+ });
1047
+ service.send({
1048
+ type: TaskEvent.HOLD_SUCCESS,
1049
+ mediaResourceId: heldTaskData.mediaResourceId,
1050
+ taskData: heldTaskData,
1051
+ });
1052
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
1053
+
1054
+ const consultCreatedTaskData = createTaskData({
1055
+ agentId: 'agent-1',
1056
+ mediaResourceId: 'interaction-1',
1057
+ consultMediaResourceId: 'consult-media',
1058
+ consultingAgentId: 'agent-1',
1059
+ destAgentId: 'agent-2',
1060
+ isConsulted: false,
1061
+ type: 'AgentConsultCreated' as any,
1062
+ interaction: {
1063
+ state: 'consult',
1064
+ interactionId: 'interaction-1',
1065
+ mainInteractionId: 'interaction-1',
1066
+ owner: 'agent-1',
1067
+ participants: {
1068
+ 'agent-1': {
1069
+ id: 'agent-1',
1070
+ pType: 'Agent',
1071
+ hasLeft: false,
1072
+ consultState: 'consultInitiated',
1073
+ isConsulted: false,
1074
+ },
1075
+ 'agent-2': {
1076
+ id: 'agent-2',
1077
+ pType: 'Agent',
1078
+ hasLeft: false,
1079
+ consultState: 'consultReserved',
1080
+ isConsulted: true,
1081
+ },
1082
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1083
+ } as any,
1084
+ media: {
1085
+ 'interaction-1': {
1086
+ mediaResourceId: 'interaction-1',
1087
+ mType: 'mainCall',
1088
+ isHold: true,
1089
+ participants: ['customer-1', 'agent-1'],
1090
+ },
1091
+ 'consult-media': {
1092
+ mediaResourceId: 'consult-media',
1093
+ mType: 'consult',
1094
+ participants: ['agent-2', 'agent-1'],
1095
+ },
1096
+ } as any,
1097
+ } as any,
1098
+ });
1099
+ service.send({type: TaskEvent.CONSULT_CREATED, taskData: consultCreatedTaskData});
1100
+
1101
+ const consultFailedTaskData = createTaskData({
1102
+ agentId: 'agent-1',
1103
+ mediaResourceId: 'interaction-1',
1104
+ consultMediaResourceId: 'consult-media',
1105
+ consultingAgentId: 'agent-1',
1106
+ destAgentId: 'agent-2',
1107
+ isConsulted: false,
1108
+ type: 'AgentConsultFailed' as any,
1109
+ interaction: {
1110
+ state: 'connected',
1111
+ interactionId: 'interaction-1',
1112
+ mainInteractionId: 'interaction-1',
1113
+ owner: 'agent-1',
1114
+ participants: {
1115
+ 'agent-1': {
1116
+ id: 'agent-1',
1117
+ pType: 'Agent',
1118
+ hasLeft: false,
1119
+ consultState: 'consultCompleted',
1120
+ isConsulted: false,
1121
+ },
1122
+ 'agent-2': {
1123
+ id: 'agent-2',
1124
+ pType: 'Agent',
1125
+ hasLeft: false,
1126
+ consultState: 'consultReserved',
1127
+ isConsulted: true,
1128
+ },
1129
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1130
+ } as any,
1131
+ media: {
1132
+ 'interaction-1': {
1133
+ mediaResourceId: 'interaction-1',
1134
+ mType: 'mainCall',
1135
+ isHold: true,
1136
+ participants: ['customer-1', 'agent-1'],
1137
+ },
1138
+ } as any,
1139
+ } as any,
1140
+ });
1141
+ service.send({type: TaskEvent.CONSULT_FAILED, taskData: consultFailedTaskData});
1142
+
1143
+ const consultEndedTaskData = createTaskData({
1144
+ agentId: 'agent-1',
1145
+ mediaResourceId: 'interaction-1',
1146
+ consultMediaResourceId: 'consult-media',
1147
+ isConsulted: false,
1148
+ type: 'AgentConsultEnded' as any,
1149
+ interaction: consultFailedTaskData.interaction,
1150
+ });
1151
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
1152
+
1153
+ const snapshot = service.getSnapshot();
1154
+ expect(snapshot.value).toBe(TaskState.HELD);
1155
+ expect(snapshot.context.consultInitiator).toBe(false);
1156
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
1157
+ expect(snapshot.context.uiControls.main.consult).toEqual({
1158
+ isVisible: true,
1159
+ isEnabled: true,
1160
+ });
1161
+ expect(snapshot.context.uiControls.main.hold).toEqual({
1162
+ isVisible: true,
1163
+ isEnabled: true,
1164
+ });
1165
+ expect(snapshot.context.uiControls.main.transfer).toEqual({
1166
+ isVisible: true,
1167
+ isEnabled: true,
1168
+ });
1169
+ });
1170
+
1171
+ it('returns to main leg (HELD) and does not clear the task after AgentConsultFailed then AgentConsultEnded while CONSULTING (RONA)', () => {
1172
+ const service = startMachine();
1173
+ const heldTaskData = createTaskData({
1174
+ agentId: 'agent-1',
1175
+ mediaResourceId: 'interaction-1',
1176
+ type: 'AgentContactHeld' as any,
1177
+ interaction: {
1178
+ state: 'hold',
1179
+ interactionId: 'interaction-1',
1180
+ mainInteractionId: 'interaction-1',
1181
+ owner: 'agent-1',
1182
+ participants: {
1183
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
1184
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1185
+ } as any,
1186
+ media: {
1187
+ 'interaction-1': {
1188
+ mediaResourceId: 'interaction-1',
1189
+ mType: 'mainCall',
1190
+ isHold: true,
1191
+ participants: ['customer-1', 'agent-1'],
1192
+ },
1193
+ } as any,
1194
+ } as any,
1195
+ });
1196
+
1197
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: heldTaskData});
1198
+ service.send({type: TaskEvent.ASSIGN, taskData: heldTaskData});
1199
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: heldTaskData.mediaResourceId});
1200
+ service.send({
1201
+ type: TaskEvent.HOLD_SUCCESS,
1202
+ mediaResourceId: heldTaskData.mediaResourceId,
1203
+ taskData: heldTaskData,
1204
+ });
1205
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
1206
+
1207
+ // Agent 1 initiates a consult and AgentConsulting arrives during ringing, moving the
1208
+ // initiator into CONSULTING before the consultee answers.
1209
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-2', destinationType: 'agent'});
1210
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
1211
+ service.send({type: TaskEvent.CONSULT_SUCCESS});
1212
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
1213
+ expect(service.getSnapshot().context.consultInitiator).toBe(true);
1214
+
1215
+ // Consultee RONAs: AgentConsultFailed arrives while still in CONSULTING. Main stays held.
1216
+ const consultFailedTaskData = createTaskData({
1217
+ agentId: 'agent-1',
1218
+ mediaResourceId: 'interaction-1',
1219
+ consultMediaResourceId: 'consult-media',
1220
+ consultingAgentId: 'agent-1',
1221
+ destAgentId: 'agent-2',
1222
+ isConsulted: false,
1223
+ type: 'AgentConsultFailed' as any,
1224
+ interaction: {
1225
+ state: 'consult',
1226
+ interactionId: 'interaction-1',
1227
+ mainInteractionId: 'interaction-1',
1228
+ owner: 'agent-1',
1229
+ participants: {
1230
+ 'agent-1': {
1231
+ id: 'agent-1',
1232
+ pType: 'Agent',
1233
+ hasLeft: false,
1234
+ consultState: 'consultCompleted',
1235
+ isConsulted: false,
1236
+ },
1237
+ 'agent-2': {
1238
+ id: 'agent-2',
1239
+ pType: 'Agent',
1240
+ hasLeft: false,
1241
+ hasJoined: false,
1242
+ consultState: 'consultReserved',
1243
+ isConsulted: true,
1244
+ },
1245
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1246
+ } as any,
1247
+ media: {
1248
+ 'interaction-1': {
1249
+ mediaResourceId: 'interaction-1',
1250
+ mType: 'mainCall',
1251
+ isHold: true,
1252
+ participants: ['customer-1', 'agent-1'],
1253
+ },
1254
+ 'consult-media': {
1255
+ mediaResourceId: 'consult-media',
1256
+ mType: 'consult',
1257
+ participants: ['agent-2', 'agent-1'],
1258
+ },
1259
+ } as any,
1260
+ } as any,
1261
+ });
1262
+ service.send({type: TaskEvent.CONSULT_FAILED, taskData: consultFailedTaskData});
1263
+ // The initiator must leave CONSULTING and fall back to the held main leg (not stay in
1264
+ // CONSULTING, which would let the trailing AgentConsultEnded terminate the task).
1265
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
1266
+
1267
+ // AgentConsultEnded closes out the consult leg.
1268
+ const consultEndedTaskData = createTaskData({
1269
+ agentId: 'agent-1',
1270
+ mediaResourceId: 'interaction-1',
1271
+ consultMediaResourceId: 'consult-media',
1272
+ isConsulted: false,
1273
+ type: 'AgentConsultEnded' as any,
1274
+ interaction: {
1275
+ state: 'connected',
1276
+ interactionId: 'interaction-1',
1277
+ mainInteractionId: 'interaction-1',
1278
+ owner: 'agent-1',
1279
+ participants: {
1280
+ 'agent-1': {
1281
+ id: 'agent-1',
1282
+ pType: 'Agent',
1283
+ hasLeft: false,
1284
+ consultState: 'consultCompleted',
1285
+ isConsulted: false,
1286
+ },
1287
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1288
+ } as any,
1289
+ media: {
1290
+ 'interaction-1': {
1291
+ mediaResourceId: 'interaction-1',
1292
+ mType: 'mainCall',
1293
+ isHold: true,
1294
+ participants: ['customer-1', 'agent-1'],
1295
+ },
1296
+ } as any,
1297
+ } as any,
1298
+ });
1299
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
1300
+
1301
+ const snapshot = service.getSnapshot();
1302
+ // The task must remain on the main leg (HELD) and never reach TERMINATED.
1303
+ expect(snapshot.value).toBe(TaskState.HELD);
1304
+ expect(snapshot.value).not.toBe(TaskState.TERMINATED);
1305
+ expect(snapshot.context.consultInitiator).toBe(false);
1306
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
1307
+ expect(snapshot.context.uiControls.main.consult).toEqual({
1308
+ isVisible: true,
1309
+ isEnabled: true,
1310
+ });
1311
+ });
1312
+
1313
+ it('transitions CONSULT_INITIATING to CONSULTING on CONSULTING_ACTIVE and marks destination joined', () => {
1314
+ const service = startMachine();
1315
+ const baseTaskData = createTaskData();
1316
+ const consultingTaskData = createTaskData({
1317
+ agentId: 'agent-1',
1318
+ isConsulted: false,
1319
+ consultMediaResourceId: 'consult-media',
1320
+ interaction: {
1321
+ state: 'consulting',
1322
+ interactionId: 'interaction-1',
1323
+ mainInteractionId: 'interaction-1',
1324
+ participants: {
1325
+ 'agent-1': {
1326
+ id: 'agent-1',
1327
+ pType: 'Agent',
1328
+ hasLeft: false,
1329
+ consultState: 'consulting',
1330
+ isConsulted: false,
1331
+ },
1332
+ 'agent-2': {
1333
+ id: 'agent-2',
1334
+ pType: 'Agent',
1335
+ hasLeft: false,
1336
+ hasJoined: true,
1337
+ consultState: 'consulting',
1338
+ isConsulted: true,
1339
+ },
1340
+ } as any,
1341
+ media: {
1342
+ 'interaction-1': {mediaResourceId: 'interaction-1', mType: 'mainCall', isHold: true},
1343
+ 'consult-media': {
1344
+ mediaResourceId: 'consult-media',
1345
+ mType: 'consult',
1346
+ participants: ['agent-1', 'agent-2'],
1347
+ },
1348
+ } as any,
1349
+ } as any,
1350
+ });
1351
+
1352
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: baseTaskData});
1353
+ service.send({type: TaskEvent.ASSIGN, taskData: baseTaskData});
1354
+ service.send({
1355
+ type: TaskEvent.CONSULT,
1356
+ destination: 'agent-2',
1357
+ destinationType: 'agent',
1358
+ });
1359
+
1360
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
1361
+
1362
+ service.send({
1363
+ type: TaskEvent.CONSULTING_ACTIVE,
1364
+ consultDestinationAgentJoined: true,
1365
+ taskData: consultingTaskData,
1366
+ });
1367
+
1368
+ const snapshot = service.getSnapshot();
1369
+ expect(snapshot.value).toBe(TaskState.CONSULTING);
1370
+ expect(snapshot.context.consultInitiator).toBe(true);
1371
+ expect(snapshot.context.consultDestinationAgentJoined).toBe(true);
1372
+ });
1373
+
1374
+ it('keeps consultDestinationAgentJoined false while consultee is only reserved, then sets true on actual join', () => {
1375
+ const service = startMachine();
1376
+ const pendingTaskData = createTaskData({
1377
+ isConsulted: false,
1378
+ interaction: {
1379
+ state: 'conference',
1380
+ mainInteractionId: 'interaction-1',
1381
+ interactionId: 'interaction-1',
1382
+ participants: {
1383
+ 'agent-1': {
1384
+ id: 'agent-1',
1385
+ pType: 'Agent',
1386
+ hasLeft: false,
1387
+ consultState: 'consultInitiated',
1388
+ isConsulted: false,
1389
+ },
1390
+ 'agent-2': {
1391
+ id: 'agent-2',
1392
+ pType: 'Agent',
1393
+ hasLeft: false,
1394
+ hasJoined: false,
1395
+ consultState: 'consultReserved',
1396
+ isConsulted: true,
1397
+ },
1398
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
1399
+ },
1400
+ media: {
1401
+ 'interaction-1': {
1402
+ mediaResourceId: 'interaction-1',
1403
+ mType: 'mainCall',
1404
+ participants: ['agent-1', 'customer-1'],
1405
+ isHold: true,
1406
+ },
1407
+ 'consult-media-1': {
1408
+ mediaResourceId: 'consult-media-1',
1409
+ mType: 'consult',
1410
+ participants: ['agent-1', 'agent-2'],
1411
+ isHold: false,
1412
+ },
1413
+ },
1414
+ } as any,
1415
+ });
1416
+
1417
+ service.send({type: TaskEvent.HYDRATE, taskData: pendingTaskData});
1418
+
1419
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
1420
+ expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(false);
1421
+
1422
+ const joinedTaskData = {
1423
+ ...pendingTaskData,
1424
+ interaction: {
1425
+ ...pendingTaskData.interaction,
1426
+ participants: {
1427
+ ...pendingTaskData.interaction?.participants,
1428
+ 'agent-2': {
1429
+ ...pendingTaskData.interaction?.participants?.['agent-2'],
1430
+ hasJoined: true,
1431
+ consultState: 'consulting',
1432
+ },
1433
+ },
1434
+ },
1435
+ } as any;
1436
+
1437
+ service.send({type: TaskEvent.CONTACT_UPDATED, taskData: joinedTaskData});
1438
+
1439
+ expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(true);
1440
+ });
1441
+
1442
+ it('hydrates conference consult context flags for active consult leg controls', () => {
1443
+ const service = startMachine();
1444
+ const taskData = createTaskData({
1445
+ type: 'AgentContactUnheld' as any,
1446
+ isConsulted: false,
1447
+ consultingAgentId: 'agent-1',
1448
+ consultMediaResourceId: 'consult-media-1',
1449
+ interaction: {
1450
+ state: 'conference',
1451
+ mainInteractionId: 'interaction-1',
1452
+ interactionId: 'interaction-1',
1453
+ participants: {
1454
+ 'agent-1': {
1455
+ id: 'agent-1',
1456
+ pType: 'Agent',
1457
+ hasLeft: false,
1458
+ consultState: 'consulting',
1459
+ isConsulted: false,
1460
+ },
1461
+ 'agent-2': {
1462
+ id: 'agent-2',
1463
+ pType: 'Agent',
1464
+ hasLeft: false,
1465
+ hasJoined: true,
1466
+ consultState: 'consulting',
1467
+ isConsulted: true,
1468
+ },
1469
+ 'agent-3': {
1470
+ id: 'agent-3',
1471
+ pType: 'Agent',
1472
+ hasLeft: false,
1473
+ consultState: 'conferencing',
1474
+ isConsulted: false,
1475
+ },
1476
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
1477
+ },
1478
+ media: {
1479
+ 'interaction-1': {
1480
+ mediaResourceId: 'interaction-1',
1481
+ mType: 'mainCall',
1482
+ participants: ['agent-1', 'agent-3', 'customer-1'],
1483
+ isHold: false,
1484
+ },
1485
+ 'consult-media-1': {
1486
+ mediaResourceId: 'consult-media-1',
1487
+ mType: 'consult',
1488
+ participants: ['agent-1', 'agent-2'],
1489
+ isHold: false,
1490
+ },
1491
+ },
1492
+ } as any,
1493
+ });
1494
+
1495
+ service.send({type: TaskEvent.HYDRATE, taskData});
1496
+
1497
+ const snapshot = service.getSnapshot();
1498
+ expect(snapshot.value).toBe(TaskState.CONSULTING);
1499
+ expect(snapshot.context.consultInitiator).toBe(true);
1500
+ expect(snapshot.context.consultFromConference).toBe(true);
1501
+ expect(snapshot.context.consultDestinationAgentJoined).toBe(true);
1502
+ expect(snapshot.context.consultCallHeld).toBe(false);
1503
+ });
1504
+
1505
+ it('returns to connected when consult ends after switching back to the main leg', () => {
1506
+ const service = startMachine();
1507
+ const taskData = createTaskData();
1508
+
1509
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1510
+ service.send({type: TaskEvent.ASSIGN, taskData});
1511
+ service.send({
1512
+ type: TaskEvent.CONSULT,
1513
+ destination: 'agent-42',
1514
+ destinationType: 'agent',
1515
+ });
1516
+ service.send({type: TaskEvent.CONSULT_SUCCESS});
1517
+ service.send({type: TaskEvent.SWITCH_TO_MAIN_CALL});
1518
+
1519
+ expect(service.getSnapshot().context.consultCallHeld).toBe(true);
1520
+
1521
+ service.send({type: TaskEvent.CONSULT_END});
1522
+
1523
+ const snapshotAfterEnd = service.getSnapshot();
1524
+ expect(snapshotAfterEnd.value).toBe(TaskState.CONNECTED);
1525
+ expect(snapshotAfterEnd.context.consultCallHeld).toBe(false);
1526
+ });
1527
+
1528
+ it('downgrades to HELD on CONSULT_END when conference has downgraded and conferenceHoldParticipant is true', () => {
1529
+ const service = startMachine();
1530
+ const conferenceTaskData = createConferenceConsultTaskData({
1531
+ interactionState: 'conference',
1532
+ includeSecondAgent: true,
1533
+ conferenceHoldParticipant: false,
1534
+ });
1535
+ const downgradedHeldTaskData = createConferenceConsultTaskData({
1536
+ interactionState: 'hold',
1537
+ includeSecondAgent: false,
1538
+ conferenceHoldParticipant: true,
1539
+ isMainHeld: true,
1540
+ });
1541
+
1542
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1543
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
1544
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
1545
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1546
+
1547
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
1548
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
1549
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: conferenceTaskData});
1550
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
1551
+
1552
+ service.send({type: TaskEvent.CONSULT_END, taskData: downgradedHeldTaskData});
1553
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
1554
+ });
1555
+
1556
+ it('downgrades to CONNECTED on CONSULT_END when conference has downgraded and conferenceHoldParticipant is false', () => {
1557
+ const service = startMachine();
1558
+ const conferenceTaskData = createConferenceConsultTaskData({
1559
+ interactionState: 'conference',
1560
+ includeSecondAgent: true,
1561
+ conferenceHoldParticipant: false,
1562
+ });
1563
+ const downgradedConnectedTaskData = createConferenceConsultTaskData({
1564
+ interactionState: 'connected',
1565
+ includeSecondAgent: false,
1566
+ conferenceHoldParticipant: false,
1567
+ isMainHeld: false,
1568
+ });
1569
+
1570
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1571
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
1572
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
1573
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1574
+
1575
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
1576
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
1577
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: conferenceTaskData});
1578
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
1579
+
1580
+ service.send({type: TaskEvent.CONSULT_END, taskData: downgradedConnectedTaskData});
1581
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
1582
+ });
1583
+
1584
+ it('returns to CONFERENCING on CONSULT_END when conference is still active', () => {
1585
+ const service = startMachine();
1586
+ const conferenceTaskData = createConferenceConsultTaskData({
1587
+ interactionState: 'conference',
1588
+ includeSecondAgent: true,
1589
+ conferenceHoldParticipant: false,
1590
+ });
1591
+ const stillConferenceTaskData = createConferenceConsultTaskData({
1592
+ interactionState: 'conference',
1593
+ includeSecondAgent: true,
1594
+ conferenceHoldParticipant: false,
1595
+ });
1596
+
1597
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1598
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
1599
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
1600
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1601
+
1602
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
1603
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
1604
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: conferenceTaskData});
1605
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
1606
+
1607
+ service.send({type: TaskEvent.CONSULT_END, taskData: stillConferenceTaskData});
1608
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1609
+ });
1610
+
1611
+ it('transitions CONFERENCING to CONSULTING on CONSULTING_ACTIVE and marks DN consult joined', () => {
1612
+ const service = startMachine();
1613
+ const conferenceTaskData = createConferenceConsultTaskData({
1614
+ interactionState: 'conference',
1615
+ includeSecondAgent: true,
1616
+ conferenceHoldParticipant: false,
1617
+ });
1618
+
1619
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
1620
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
1621
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
1622
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1623
+
1624
+ const dnConsultTaskData = createTaskData({
1625
+ type: 'AgentConsulting' as any,
1626
+ agentId: 'agent-1',
1627
+ consultingAgentId: 'agent-1',
1628
+ isConsulted: false,
1629
+ destinationType: 'DN',
1630
+ consultMediaResourceId: 'consult-media-1',
1631
+ interaction: {
1632
+ state: 'conference',
1633
+ mainInteractionId: 'interaction-1',
1634
+ interactionId: 'interaction-1',
1635
+ participants: {
1636
+ 'agent-1': {
1637
+ id: 'agent-1',
1638
+ pType: 'Agent',
1639
+ hasLeft: false,
1640
+ consultState: 'consulting',
1641
+ isConsulted: false,
1642
+ },
1643
+ 'agent-2': {
1644
+ id: 'agent-2',
1645
+ pType: 'Agent',
1646
+ hasLeft: false,
1647
+ consultState: 'conferencing',
1648
+ },
1649
+ 'dn-dest': {
1650
+ id: 'dn-dest',
1651
+ pType: 'DN',
1652
+ hasLeft: false,
1653
+ hasJoined: true,
1654
+ },
1655
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
1656
+ },
1657
+ media: {
1658
+ 'interaction-1': {
1659
+ mediaResourceId: 'interaction-1',
1660
+ mType: 'mainCall',
1661
+ participants: ['agent-1', 'agent-2', 'customer-1'],
1662
+ isHold: false,
1663
+ },
1664
+ 'consult-media-1': {
1665
+ mediaResourceId: 'consult-media-1',
1666
+ mType: 'consult',
1667
+ participants: ['agent-1', 'dn-dest'],
1668
+ isHold: false,
1669
+ },
1670
+ },
1671
+ } as any,
1672
+ });
1673
+
1674
+ service.send({
1675
+ type: TaskEvent.CONSULTING_ACTIVE,
1676
+ consultDestinationAgentJoined: true,
1677
+ taskData: dnConsultTaskData,
1678
+ });
1679
+
1680
+ const snapshot = service.getSnapshot();
1681
+ expect(snapshot.value).toBe(TaskState.CONSULTING);
1682
+ expect(snapshot.context.consultDestinationAgentJoined).toBe(true);
1683
+ expect(snapshot.context.consultFromConference).toBe(true);
1684
+ expect(snapshot.context.consultDestinationType).toBe('entryPoint');
1685
+ });
1686
+
1687
+ it('transitions to conferencing when merge event is received', () => {
1688
+ const service = startMachine();
1689
+ const taskData = createTaskData({consultingAgentId: 'agent-1'});
1690
+
1691
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1692
+ service.send({type: TaskEvent.ASSIGN, taskData});
1693
+ service.send({
1694
+ type: TaskEvent.CONSULT,
1695
+ destination: 'agent-42',
1696
+ destinationType: 'agent',
1697
+ });
1698
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
1699
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
1700
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
1701
+
1702
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1703
+ expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
1704
+
1705
+ service.send({type: TaskEvent.CONFERENCE_START});
1706
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1707
+ });
1708
+
1709
+ it('sets hideBlindTransfer flag and clears consult context on EP-DN AgentConsultConferencing (CAI-8329)', () => {
1710
+ const service = startMachine();
1711
+ const agentId = 'agent-1';
1712
+ const consultMediaId = 'consult-media-1';
1713
+ const consultTaskData = createTaskData({
1714
+ agentId,
1715
+ consultingAgentId: agentId,
1716
+ consultMediaResourceId: consultMediaId,
1717
+ isConsulted: false,
1718
+ interaction: {
1719
+ state: 'consulting',
1720
+ interactionId: 'interaction-1',
1721
+ mainInteractionId: 'interaction-1',
1722
+ owner: agentId,
1723
+ participants: {
1724
+ 'agent-1': {
1725
+ id: 'agent-1',
1726
+ pType: 'Agent',
1727
+ hasJoined: true,
1728
+ hasLeft: false,
1729
+ consultState: 'consulting',
1730
+ isConsulted: false,
1731
+ },
1732
+ '+13159998087': {
1733
+ id: '+13159998087',
1734
+ pType: 'EP-DN',
1735
+ hasLeft: false,
1736
+ },
1737
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: false},
1738
+ },
1739
+ media: {
1740
+ 'interaction-1': {
1741
+ mediaResourceId: 'interaction-1',
1742
+ mType: 'mainCall',
1743
+ participants: ['customer-1', 'agent-1'],
1744
+ isHold: false,
1745
+ },
1746
+ [consultMediaId]: {
1747
+ mediaResourceId: consultMediaId,
1748
+ mType: 'consult',
1749
+ participants: ['+13159998087', 'agent-1'],
1750
+ isHold: false,
1751
+ },
1752
+ },
1753
+ } as any,
1754
+ });
1755
+
1756
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: consultTaskData});
1757
+ service.send({type: TaskEvent.ASSIGN, taskData: consultTaskData});
1758
+ service.send({
1759
+ type: TaskEvent.CONSULT,
1760
+ destination: '+13159998087',
1761
+ destinationType: 'entryPoint',
1762
+ });
1763
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: consultTaskData});
1764
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1765
+ expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
1766
+ expect(service.getSnapshot().context.consultInitiator).toBe(true);
1767
+
1768
+ const conferencingTaskData = createTaskData({
1769
+ agentId,
1770
+ consultMediaResourceId: consultMediaId,
1771
+ isConsulted: false,
1772
+ type: 'AgentConsultConferencing' as any,
1773
+ interaction: {
1774
+ state: 'conference',
1775
+ interactionId: 'interaction-1',
1776
+ mainInteractionId: 'interaction-1',
1777
+ owner: agentId,
1778
+ participants: {
1779
+ 'agent-1': {
1780
+ id: 'agent-1',
1781
+ pType: 'Agent',
1782
+ hasJoined: true,
1783
+ hasLeft: false,
1784
+ consultState: 'conferencing',
1785
+ isConsulted: false,
1786
+ },
1787
+ '+13159998087': {
1788
+ id: '+13159998087',
1789
+ pType: 'EP-DN',
1790
+ hasLeft: false,
1791
+ },
1792
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: false},
1793
+ },
1794
+ media: {
1795
+ 'interaction-1': {
1796
+ mediaResourceId: 'interaction-1',
1797
+ mType: 'mainCall',
1798
+ participants: ['customer-1', 'agent-1'],
1799
+ isHold: false,
1800
+ },
1801
+ [consultMediaId]: {
1802
+ mediaResourceId: consultMediaId,
1803
+ mType: 'consult',
1804
+ participants: ['+13159998087', 'agent-1'],
1805
+ isHold: false,
1806
+ },
1807
+ },
1808
+ } as any,
1809
+ });
1810
+
1811
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferencingTaskData});
1812
+
1813
+ const snapshot = service.getSnapshot();
1814
+ expect(snapshot.value).toBe(TaskState.CONFERENCING);
1815
+ expect(snapshot.context.consultInitiator).toBe(false);
1816
+ expect(snapshot.context.consultDestinationType).toBe(null);
1817
+ expect(snapshot.context.hideBlindTransferForEpDnPendingMerge).toBe(true);
1818
+ expect(snapshot.context.uiControls.activeLeg).toBe('main');
1819
+ expect(snapshot.context.uiControls.main.transfer).toEqual({
1820
+ isVisible: false,
1821
+ isEnabled: false,
1822
+ });
1823
+ expect(snapshot.context.uiControls.main.exitConference).toEqual({
1824
+ isVisible: true,
1825
+ isEnabled: true,
1826
+ });
1827
+ expect(snapshot.context.uiControls.consult.endConsult).toEqual({
1828
+ isVisible: false,
1829
+ isEnabled: false,
1830
+ });
1831
+ });
1832
+
1833
+ it('terminates via wrapup when EXIT_CONFERENCE_SUCCESS is received in conferencing', () => {
1834
+ const service = startMachine();
1835
+ const taskData = createTaskData({
1836
+ // shouldWrapUpForThisAgent will return true when owner matches self agent
1837
+ interaction: {
1838
+ owner: 'agent-1',
1839
+ state: 'conference',
1840
+ mainInteractionId: 'interaction-1',
1841
+ interactionId: 'interaction-1',
1842
+ participants: {
1843
+ 'agent-1': {
1844
+ id: 'agent-1',
1845
+ pType: 'Agent',
1846
+ type: 'Agent',
1847
+ hasJoined: true,
1848
+ hasLeft: false,
1849
+ isInPredial: false,
1850
+ },
1851
+ c1: {
1852
+ id: 'c1',
1853
+ pType: 'Customer',
1854
+ type: 'Customer',
1855
+ hasJoined: true,
1856
+ hasLeft: false,
1857
+ isInPredial: false,
1858
+ },
1859
+ },
1860
+ media: {
1861
+ 'interaction-1': {
1862
+ mediaResourceId: 'interaction-1',
1863
+ mediaType: 'telephony',
1864
+ mediaMgr: 'mm',
1865
+ participants: ['agent-1', 'c1'],
1866
+ mType: 'mainCall',
1867
+ isHold: false,
1868
+ holdTimestamp: null,
1869
+ },
1870
+ },
1871
+ } as any,
1872
+ });
1873
+
1874
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1875
+ service.send({type: TaskEvent.ASSIGN, taskData});
1876
+ service.send({
1877
+ type: TaskEvent.CONSULT,
1878
+ destination: 'agent-42',
1879
+ destinationType: 'agent',
1880
+ });
1881
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
1882
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1883
+ service.send({type: TaskEvent.CONFERENCE_START, taskData});
1884
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1885
+
1886
+ service.send({type: TaskEvent.EXIT_CONFERENCE_SUCCESS, taskData});
1887
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
1888
+ });
1889
+
1890
+ const primeConferencing = (service: ReturnType<typeof startMachine>, taskData: ReturnType<typeof createTaskData>) => {
1891
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1892
+ service.send({type: TaskEvent.ASSIGN, taskData});
1893
+ service.send({
1894
+ type: TaskEvent.CONSULT,
1895
+ destination: 'agent-42',
1896
+ destinationType: 'agent',
1897
+ });
1898
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
1899
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
1900
+ service.send({type: TaskEvent.CONFERENCE_START, taskData});
1901
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
1902
+ };
1903
+
1904
+ const conferenceInProgressTaskData = (overrides: Parameters<typeof createTaskData>[0] = {}) =>
1905
+ createTaskData({
1906
+ wrapUpRequired: true,
1907
+ interaction: {
1908
+ owner: 'agent-1',
1909
+ state: 'conference',
1910
+ mainInteractionId: 'interaction-1',
1911
+ interactionId: 'interaction-1',
1912
+ participants: {
1913
+ 'agent-1': {
1914
+ id: 'agent-1',
1915
+ pType: 'Agent',
1916
+ type: 'Agent',
1917
+ hasJoined: true,
1918
+ hasLeft: false,
1919
+ },
1920
+ 'agent-2': {
1921
+ id: 'agent-2',
1922
+ pType: 'Agent',
1923
+ type: 'Agent',
1924
+ hasJoined: true,
1925
+ hasLeft: false,
1926
+ },
1927
+ },
1928
+ media: {
1929
+ 'interaction-1': {
1930
+ mediaResourceId: 'interaction-1',
1931
+ mediaType: 'telephony',
1932
+ participants: ['agent-1', 'agent-2'],
1933
+ mType: 'mainCall',
1934
+ isHold: false,
1935
+ },
1936
+ },
1937
+ } as any,
1938
+ ...overrides,
1939
+ });
1940
+
1941
+ const primeConnectedWithConferenceTaskData = (
1942
+ service: ReturnType<typeof startMachine>,
1943
+ taskData: ReturnType<typeof createTaskData>
1944
+ ) => {
1945
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
1946
+ service.send({type: TaskEvent.ASSIGN, taskData});
1947
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
1948
+ };
1949
+
1950
+ it('enters WRAPPING_UP on EXIT_CONFERENCE_SUCCESS while CONNECTED with conference in task data', () => {
1951
+ const service = startMachine();
1952
+ const taskData = conferenceInProgressTaskData();
1953
+
1954
+ primeConnectedWithConferenceTaskData(service, taskData);
1955
+ service.send({type: TaskEvent.EXIT_CONFERENCE_SUCCESS, taskData});
1956
+
1957
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
1958
+ expect(service.getSnapshot().context.taskData?.wrapUpRequired).toBe(true);
1959
+ });
1960
+
1961
+ it('emits wrap-up once on EXIT_CONFERENCE_SUCCESS while CONNECTED', () => {
1962
+ const emitTaskWrapup = jest.fn();
1963
+ const service = createActor(
1964
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
1965
+ );
1966
+ service.start();
1967
+ const taskData = conferenceInProgressTaskData();
1968
+
1969
+ primeConnectedWithConferenceTaskData(service, taskData);
1970
+ service.send({type: TaskEvent.EXIT_CONFERENCE_SUCCESS, taskData});
1971
+
1972
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
1973
+ expect(emitTaskWrapup).toHaveBeenCalledTimes(1);
1974
+ });
1975
+
1976
+ it('terminates on EXIT_CONFERENCE_SUCCESS while CONNECTED when wrap-up is not required', () => {
1977
+ const service = startMachine();
1978
+ const taskData = conferenceInProgressTaskData({
1979
+ wrapUpRequired: false,
1980
+ interaction: {
1981
+ owner: 'other-agent',
1982
+ state: 'conference',
1983
+ mainInteractionId: 'interaction-1',
1984
+ interactionId: 'interaction-1',
1985
+ participants: {
1986
+ 'agent-1': {
1987
+ id: 'agent-1',
1988
+ pType: 'Agent',
1989
+ type: 'Agent',
1990
+ hasJoined: true,
1991
+ hasLeft: false,
1992
+ isWrapUp: false,
1993
+ },
1994
+ 'agent-2': {
1995
+ id: 'agent-2',
1996
+ pType: 'Agent',
1997
+ type: 'Agent',
1998
+ hasJoined: true,
1999
+ hasLeft: false,
2000
+ },
2001
+ },
2002
+ media: {
2003
+ 'interaction-1': {
2004
+ mediaResourceId: 'interaction-1',
2005
+ mediaType: 'telephony',
2006
+ participants: ['agent-1', 'agent-2'],
2007
+ mType: 'mainCall',
2008
+ isHold: false,
2009
+ },
2010
+ },
2011
+ } as any,
2012
+ });
2013
+
2014
+ primeConnectedWithConferenceTaskData(service, taskData);
2015
+ service.send({type: TaskEvent.EXIT_CONFERENCE_SUCCESS, taskData});
2016
+
2017
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2018
+ });
2019
+
2020
+ it('does not re-emit wrap-up on late TASK_WRAPUP when wrap-up was already published', () => {
2021
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
2022
+ const service = createActor(
2023
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
2024
+ );
2025
+ service.start();
2026
+ const taskData = conferenceInProgressTaskData();
2027
+
2028
+ primeConferencing(service, taskData);
2029
+ service.send({
2030
+ type: TaskEvent.EXIT_CONFERENCE_SUCCESS,
2031
+ taskData: {...taskData, wrapUpRequired: true},
2032
+ });
2033
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2034
+ expect(service.getSnapshot().context.taskData?.wrapUpRequired).toBe(true);
2035
+
2036
+ const emitCountAfterExit = publishWrapup.mock.calls.length;
2037
+ service.send({
2038
+ type: TaskEvent.TASK_WRAPUP,
2039
+ taskData: {...taskData, wrapUpRequired: true},
2040
+ });
2041
+
2042
+ expect(publishWrapup.mock.calls.length).toBe(emitCountAfterExit);
2043
+ });
2044
+
2045
+ it('emits wrap-up on late TASK_WRAPUP when wrap-up was not yet published', () => {
2046
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
2047
+ const service = createActor(
2048
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
2049
+ );
2050
+ service.start();
2051
+ const taskData = createTaskData({
2052
+ wrapUpRequired: false,
2053
+ interaction: {
2054
+ owner: 'agent-1',
2055
+ state: 'connected',
2056
+ mainInteractionId: 'interaction-1',
2057
+ interactionId: 'interaction-1',
2058
+ participants: {
2059
+ 'agent-1': {
2060
+ id: 'agent-1',
2061
+ pType: 'Agent',
2062
+ type: 'Agent',
2063
+ hasJoined: true,
2064
+ hasLeft: false,
2065
+ },
2066
+ },
2067
+ } as any,
2068
+ });
2069
+
2070
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2071
+ service.send({type: TaskEvent.ASSIGN, taskData});
2072
+ service.send({type: TaskEvent.TASK_WRAPUP, taskData});
2073
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2074
+ expect(service.getSnapshot().context.taskData?.wrapUpRequired).not.toBe(true);
2075
+ expect(publishWrapup).not.toHaveBeenCalled();
2076
+
2077
+ service.send({
2078
+ type: TaskEvent.TASK_WRAPUP,
2079
+ taskData: {...taskData, wrapUpRequired: true},
2080
+ });
2081
+
2082
+ expect(publishWrapup).toHaveBeenCalledTimes(1);
2083
+ });
2084
+
2085
+ it('terminates when EXIT_CONFERENCE_SUCCESS is received in conferencing and wrapup is not required', () => {
2086
+ const service = startMachine();
2087
+ const taskData = createTaskData({
2088
+ isConsulted: true,
2089
+ wrapUpRequired: false,
2090
+ interaction: {
2091
+ owner: 'other-agent',
2092
+ state: 'conference',
2093
+ mainInteractionId: 'interaction-1',
2094
+ interactionId: 'interaction-1',
2095
+ participants: {
2096
+ 'agent-1': {
2097
+ id: 'agent-1',
2098
+ pType: 'Agent',
2099
+ type: 'Agent',
2100
+ hasJoined: true,
2101
+ hasLeft: false,
2102
+ isInPredial: false,
2103
+ isWrapUp: false,
2104
+ },
2105
+ c1: {
2106
+ id: 'c1',
2107
+ pType: 'Customer',
2108
+ type: 'Customer',
2109
+ hasJoined: true,
2110
+ hasLeft: false,
2111
+ isInPredial: false,
2112
+ },
2113
+ },
2114
+ media: {
2115
+ 'interaction-1': {
2116
+ mediaResourceId: 'interaction-1',
2117
+ mediaType: 'telephony',
2118
+ mediaMgr: 'mm',
2119
+ participants: ['agent-1', 'c1'],
2120
+ mType: 'mainCall',
2121
+ isHold: false,
2122
+ holdTimestamp: null,
2123
+ },
2124
+ },
2125
+ } as any,
2126
+ });
2127
+
2128
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2129
+ service.send({type: TaskEvent.ASSIGN, taskData});
2130
+ service.send({
2131
+ type: TaskEvent.CONSULT,
2132
+ destination: 'agent-42',
2133
+ destinationType: 'agent',
2134
+ });
2135
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
2136
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
2137
+ service.send({type: TaskEvent.CONFERENCE_START, taskData});
2138
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2139
+
2140
+ service.send({type: TaskEvent.EXIT_CONFERENCE_SUCCESS, taskData});
2141
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2142
+ });
2143
+
2144
+ it('enters WRAPPING_UP on CONFERENCE_END when wrapUpRequired is stamped', () => {
2145
+ const service = startMachine();
2146
+ const taskData = createTaskData({
2147
+ wrapUpRequired: true,
2148
+ interaction: {
2149
+ owner: 'agent-1',
2150
+ state: 'conference',
2151
+ mainInteractionId: 'interaction-1',
2152
+ interactionId: 'interaction-1',
2153
+ participants: {
2154
+ 'agent-1': {
2155
+ id: 'agent-1',
2156
+ pType: 'Agent',
2157
+ type: 'Agent',
2158
+ isWrapUp: true,
2159
+ hasJoined: true,
2160
+ hasLeft: true,
2161
+ },
2162
+ },
2163
+ } as any,
2164
+ });
2165
+
2166
+ primeConferencing(service, taskData);
2167
+ service.send({type: TaskEvent.CONFERENCE_END, taskData});
2168
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2169
+ });
2170
+
2171
+ it('emits wrap-up once on CONFERENCE_END when wrap-up is required', () => {
2172
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
2173
+ const service = createActor(
2174
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
2175
+ );
2176
+ service.start();
2177
+ const taskData = createTaskData({
2178
+ wrapUpRequired: true,
2179
+ interaction: {
2180
+ owner: 'agent-1',
2181
+ state: 'conference',
2182
+ mainInteractionId: 'interaction-1',
2183
+ interactionId: 'interaction-1',
2184
+ participants: {
2185
+ 'agent-1': {
2186
+ id: 'agent-1',
2187
+ pType: 'Agent',
2188
+ type: 'Agent',
2189
+ isWrapUp: true,
2190
+ hasJoined: true,
2191
+ hasLeft: true,
2192
+ },
2193
+ },
2194
+ } as any,
2195
+ });
2196
+
2197
+ primeConferencing(service, taskData);
2198
+ service.send({type: TaskEvent.CONFERENCE_END, taskData});
2199
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2200
+ expect(publishWrapup).toHaveBeenCalledTimes(1);
2201
+ });
2202
+
2203
+ it('emits wrap-up once on TASK_WRAPUP from CONSULTING when wrap-up is required', () => {
2204
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
2205
+ const service = createActor(
2206
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
2207
+ );
2208
+ service.start();
2209
+ const taskData = createTaskData({
2210
+ interaction: {owner: 'agent-1', state: 'connected'} as any,
2211
+ });
2212
+
2213
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2214
+ service.send({type: TaskEvent.ASSIGN, taskData});
2215
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-2', destinationType: 'agent'});
2216
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
2217
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
2218
+
2219
+ service.send({
2220
+ type: TaskEvent.TASK_WRAPUP,
2221
+ taskData: {...taskData, wrapUpRequired: true},
2222
+ });
2223
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2224
+ expect(publishWrapup).toHaveBeenCalledTimes(1);
2225
+ });
2226
+
2227
+ it('emits wrap-up once on TASK_WRAPUP from CONFERENCING when wrap-up is required', () => {
2228
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
2229
+ const service = createActor(
2230
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
2231
+ );
2232
+ service.start();
2233
+ const taskData = createTaskData({
2234
+ interaction: {
2235
+ owner: 'agent-1',
2236
+ state: 'conference',
2237
+ mainInteractionId: 'interaction-1',
2238
+ interactionId: 'interaction-1',
2239
+ } as any,
2240
+ });
2241
+
2242
+ primeConferencing(service, taskData);
2243
+ service.send({
2244
+ type: TaskEvent.TASK_WRAPUP,
2245
+ taskData: {...taskData, wrapUpRequired: true},
2246
+ });
2247
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2248
+ expect(publishWrapup).toHaveBeenCalledTimes(1);
2249
+ });
2250
+
2251
+ it('enters WRAPPING_UP on PARTICIPANT_LEAVE when this agent left and wrap-up is required', () => {
2252
+ const service = startMachine();
2253
+ const conferenceData = createTaskData({
2254
+ interaction: {
2255
+ owner: 'agent-1',
2256
+ state: 'conference',
2257
+ mainInteractionId: 'interaction-1',
2258
+ interactionId: 'interaction-1',
2259
+ participants: {
2260
+ 'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2261
+ 'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2262
+ },
2263
+ } as any,
2264
+ });
2265
+ const leaveData = createTaskData({
2266
+ wrapUpRequired: true,
2267
+ participantId: 'agent-1',
2268
+ interaction: {
2269
+ owner: 'agent-1',
2270
+ state: 'conference',
2271
+ mainInteractionId: 'interaction-1',
2272
+ interactionId: 'interaction-1',
2273
+ participants: {
2274
+ 'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2275
+ },
2276
+ } as any,
2277
+ });
2278
+
2279
+ primeConferencing(service, conferenceData);
2280
+ service.send({
2281
+ type: TaskEvent.PARTICIPANT_LEAVE,
2282
+ participantId: 'agent-1',
2283
+ taskData: leaveData,
2284
+ });
2285
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2286
+ });
2287
+
2288
+ it('emits wrap-up once on PARTICIPANT_LEAVE when wrap-up is required', () => {
2289
+ const emitTaskWrapup = jest.fn();
2290
+ const service = createActor(
2291
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
2292
+ );
2293
+ service.start();
2294
+ const conferenceData = createTaskData({
2295
+ interaction: {
2296
+ owner: 'agent-1',
2297
+ state: 'conference',
2298
+ mainInteractionId: 'interaction-1',
2299
+ interactionId: 'interaction-1',
2300
+ participants: {
2301
+ 'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2302
+ 'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2303
+ },
2304
+ } as any,
2305
+ });
2306
+ const leaveData = createTaskData({
2307
+ wrapUpRequired: true,
2308
+ participantId: 'agent-1',
2309
+ interaction: {
2310
+ owner: 'agent-1',
2311
+ state: 'conference',
2312
+ mainInteractionId: 'interaction-1',
2313
+ interactionId: 'interaction-1',
2314
+ participants: {
2315
+ 'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2316
+ },
2317
+ } as any,
2318
+ });
2319
+
2320
+ primeConferencing(service, conferenceData);
2321
+ service.send({
2322
+ type: TaskEvent.PARTICIPANT_LEAVE,
2323
+ participantId: 'agent-1',
2324
+ taskData: leaveData,
2325
+ });
2326
+
2327
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2328
+ expect(emitTaskWrapup).toHaveBeenCalledTimes(1);
2329
+ });
2330
+
2331
+ it('does not wrap remaining agent on PARTICIPANT_LEAVE', () => {
2332
+ const service = startMachine();
2333
+ const conferenceData = createTaskData({
2334
+ interaction: {
2335
+ owner: 'agent-1',
2336
+ state: 'conference',
2337
+ mainInteractionId: 'interaction-1',
2338
+ interactionId: 'interaction-1',
2339
+ participants: {
2340
+ 'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2341
+ 'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2342
+ c1: {id: 'c1', pType: 'Customer', type: 'Customer', hasJoined: true, hasLeft: false},
2343
+ },
2344
+ media: {
2345
+ 'interaction-1': {
2346
+ mediaResourceId: 'interaction-1',
2347
+ mediaType: 'telephony',
2348
+ mediaMgr: 'mm',
2349
+ participants: ['agent-1', 'agent-2', 'c1'],
2350
+ mType: 'mainCall',
2351
+ isHold: false,
2352
+ holdTimestamp: null,
2353
+ },
2354
+ },
2355
+ } as any,
2356
+ });
2357
+ const otherLeft = createTaskData({
2358
+ wrapUpRequired: false,
2359
+ participantId: 'agent-2',
2360
+ interaction: {
2361
+ owner: 'agent-1',
2362
+ state: 'conference',
2363
+ mainInteractionId: 'interaction-1',
2364
+ interactionId: 'interaction-1',
2365
+ participants: {
2366
+ 'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
2367
+ c1: {id: 'c1', pType: 'Customer', type: 'Customer', hasJoined: true, hasLeft: false},
2368
+ },
2369
+ media: {
2370
+ 'interaction-1': {
2371
+ mediaResourceId: 'interaction-1',
2372
+ mediaType: 'telephony',
2373
+ mediaMgr: 'mm',
2374
+ participants: ['agent-1', 'c1'],
2375
+ mType: 'mainCall',
2376
+ isHold: false,
2377
+ holdTimestamp: null,
2378
+ },
2379
+ },
2380
+ } as any,
2381
+ });
2382
+
2383
+ primeConferencing(service, conferenceData);
2384
+ service.send({
2385
+ type: TaskEvent.PARTICIPANT_LEAVE,
2386
+ participantId: 'agent-2',
2387
+ taskData: otherLeft,
2388
+ });
2389
+ expect(service.getSnapshot().value).not.toBe(TaskState.WRAPPING_UP);
2390
+ });
2391
+
2392
+ it('downgrades to HELD on HOLD_SUCCESS when no other agents remain on task', () => {
2393
+ const service = startMachine();
2394
+ const conferenceTaskData = createSingleAgentConferenceTaskData('conference');
2395
+ const heldTaskData = createSingleAgentConferenceTaskData('hold', true);
2396
+
2397
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
2398
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
2399
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
2400
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2401
+
2402
+ service.send({
2403
+ type: TaskEvent.HOLD_SUCCESS,
2404
+ mediaResourceId: heldTaskData.mediaResourceId,
2405
+ taskData: heldTaskData,
2406
+ });
2407
+
2408
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
2409
+ });
2410
+
2411
+ it('downgrades to CONNECTED on UNHOLD_SUCCESS when no other agents remain on task', () => {
2412
+ const service = startMachine();
2413
+ const conferenceTaskData = createSingleAgentConferenceTaskData('conference');
2414
+ const connectedTaskData = createSingleAgentConferenceTaskData('connected', false);
2415
+
2416
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
2417
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
2418
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
2419
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2420
+
2421
+ service.send({
2422
+ type: TaskEvent.UNHOLD_SUCCESS,
2423
+ mediaResourceId: connectedTaskData.mediaResourceId,
2424
+ taskData: connectedTaskData,
2425
+ });
2426
+
2427
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
2428
+ });
2429
+
2430
+ it('downgrades to CONNECTED on PARTICIPANT_LEAVE when the current agent remains', () => {
2431
+ const service = startMachine();
2432
+ const conferenceTaskData = createSingleAgentConferenceTaskData('conference');
2433
+ const connectedTaskData = createSingleAgentConferenceTaskData('connected');
2434
+
2435
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
2436
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
2437
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
2438
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2439
+
2440
+ service.send({
2441
+ type: TaskEvent.PARTICIPANT_LEAVE,
2442
+ participantId: 'agent-2',
2443
+ taskData: connectedTaskData,
2444
+ });
2445
+
2446
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
2447
+ });
2448
+
2449
+ it('terminates instead of downgrading when PARTICIPANT_LEAVE identifies the current agent', () => {
2450
+ const service = startMachine();
2451
+ const conferenceTaskData = createSingleAgentConferenceTaskData('conference');
2452
+ const connectedTaskData = {
2453
+ ...createSingleAgentConferenceTaskData('connected'),
2454
+ wrapUpRequired: false,
2455
+ };
2456
+
2457
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
2458
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
2459
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
2460
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2461
+
2462
+ service.send({
2463
+ type: TaskEvent.PARTICIPANT_LEAVE,
2464
+ participantId: 'agent-1',
2465
+ taskData: connectedTaskData,
2466
+ });
2467
+
2468
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2469
+ });
2470
+
2471
+ it('returns to CONNECTED when CTQ cancel arrives before queue connects', () => {
2472
+ const service = startMachine();
2473
+ const taskData = createTaskData();
2474
+
2475
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2476
+ service.send({type: TaskEvent.ASSIGN, taskData});
2477
+
2478
+ service.send({
2479
+ type: TaskEvent.CONSULT,
2480
+ destination: 'queue-1',
2481
+ destinationType: 'queue',
2482
+ });
2483
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
2484
+
2485
+ service.send({type: TaskEvent.CTQ_CANCEL});
2486
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
2487
+ });
2488
+
2489
+ it('returns to CONNECTED when CTQ consult fails', () => {
2490
+ const service = startMachine();
2491
+ const taskData = createTaskData();
2492
+
2493
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2494
+ service.send({type: TaskEvent.ASSIGN, taskData});
2495
+
2496
+ service.send({
2497
+ type: TaskEvent.CONSULT,
2498
+ destination: 'queue-1',
2499
+ destinationType: 'queue',
2500
+ });
2501
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
2502
+
2503
+ service.send({type: TaskEvent.CONSULT_FAILED, taskData});
2504
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
2505
+ });
2506
+ });
2507
+
2508
+ describe('failure scenarios', () => {
2509
+ it('returns to CONNECTED when HOLD fails', () => {
2510
+ const service = startMachine();
2511
+ const taskData = createTaskData();
2512
+
2513
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2514
+ service.send({type: TaskEvent.ASSIGN, taskData});
2515
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
2516
+ expect(service.getSnapshot().value).toBe(TaskState.HOLD_INITIATING);
2517
+
2518
+ service.send({
2519
+ type: TaskEvent.HOLD_FAILED,
2520
+ mediaResourceId: taskData.mediaResourceId,
2521
+ });
2522
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
2523
+ });
2524
+
2525
+ it('falls back to HELD when UNHOLD fails', () => {
2526
+ const service = startMachine();
2527
+ const taskData = createTaskData();
2528
+
2529
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2530
+ service.send({type: TaskEvent.ASSIGN, taskData});
2531
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
2532
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId});
2533
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
2534
+
2535
+ service.send({type: TaskEvent.UNHOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
2536
+ expect(service.getSnapshot().value).toBe(TaskState.RESUME_INITIATING);
2537
+
2538
+ service.send({type: TaskEvent.UNHOLD_FAILED, mediaResourceId: taskData.mediaResourceId});
2539
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
2540
+ });
2541
+ });
2542
+
2543
+ describe('CONF_INITIATING state event handlers', () => {
2544
+ it('transitions to CONFERENCING on CONFERENCE_START', () => {
2545
+ const service = startMachine();
2546
+ const taskData = createTaskData({consultingAgentId: 'agent-1'});
2547
+
2548
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2549
+ service.send({type: TaskEvent.ASSIGN, taskData});
2550
+ service.send({
2551
+ type: TaskEvent.CONSULT,
2552
+ destination: 'agent-42',
2553
+ destinationType: 'agent',
2554
+ });
2555
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
2556
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
2557
+ expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
2558
+
2559
+ service.send({type: TaskEvent.CONFERENCE_START, taskData});
2560
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2561
+ });
2562
+
2563
+ it('transitions to WRAPPING_UP on CONSULT_END with isTerminated during CONF_INITIATING', () => {
2564
+ const service = startMachine();
2565
+ const taskData = createTaskData({consultingAgentId: 'agent-1'});
2566
+
2567
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2568
+ service.send({type: TaskEvent.ASSIGN, taskData});
2569
+ service.send({
2570
+ type: TaskEvent.CONSULT,
2571
+ destination: 'agent-42',
2572
+ destinationType: 'agent',
2573
+ });
2574
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
2575
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
2576
+ expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
2577
+
2578
+ const terminatedTaskData = createTaskData({
2579
+ consultingAgentId: 'agent-1',
2580
+ interaction: {
2581
+ isTerminated: true,
2582
+ owner: 'agent-1',
2583
+ } as any,
2584
+ });
2585
+ service.send({type: TaskEvent.CONSULT_END, taskData: terminatedTaskData});
2586
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2587
+ });
2588
+
2589
+ it('transitions to CONNECTED on CONSULT_END without isTerminated during CONF_INITIATING', () => {
2590
+ const service = startMachine();
2591
+ const taskData = createTaskData({
2592
+ consultingAgentId: 'agent-1',
2593
+ interaction: {
2594
+ participants: {
2595
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
2596
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
2597
+ },
2598
+ } as any,
2599
+ });
2600
+
2601
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2602
+ service.send({type: TaskEvent.ASSIGN, taskData});
2603
+ service.send({
2604
+ type: TaskEvent.CONSULT,
2605
+ destination: 'agent-42',
2606
+ destinationType: 'agent',
2607
+ });
2608
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
2609
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
2610
+ expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
2611
+
2612
+ service.send({type: TaskEvent.CONSULT_END, taskData});
2613
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
2614
+ });
2615
+ });
2616
+
2617
+ describe('CONFERENCING state CONSULT_FAILED ANOTHER_CONSULT_IN_PROGRESS (CAI-8329)', () => {
2618
+ it('keeps hideBlindTransfer flag and hides transfer after secondary consult rejected from Agent Desktop', () => {
2619
+ const service = startMachine();
2620
+ const agentId = 'agent-1';
2621
+ const consultMediaId = 'consult-media-1';
2622
+ const baseTaskData = createTaskData({
2623
+ agentId,
2624
+ consultingAgentId: agentId,
2625
+ consultMediaResourceId: consultMediaId,
2626
+ isConsulted: false,
2627
+ interaction: {
2628
+ state: 'conference',
2629
+ interactionId: 'interaction-1',
2630
+ mainInteractionId: 'interaction-1',
2631
+ owner: agentId,
2632
+ participants: {
2633
+ [agentId]: {
2634
+ id: agentId,
2635
+ pType: 'Agent',
2636
+ hasJoined: true,
2637
+ hasLeft: false,
2638
+ consultState: 'conferencing',
2639
+ },
2640
+ '+13159998059': {id: '+13159998059', pType: 'EP-DN', hasLeft: false},
2641
+ '+14696762938': {
2642
+ id: '+14696762938',
2643
+ pType: 'Customer',
2644
+ hasJoined: true,
2645
+ hasLeft: false,
2646
+ },
2647
+ },
2648
+ media: {
2649
+ 'interaction-1': {
2650
+ mediaResourceId: 'interaction-1',
2651
+ mType: 'mainCall',
2652
+ participants: ['+14696762938', agentId],
2653
+ isHold: false,
2654
+ },
2655
+ [consultMediaId]: {
2656
+ mediaResourceId: consultMediaId,
2657
+ mType: 'consult',
2658
+ participants: ['+13159998059', agentId],
2659
+ isHold: false,
2660
+ },
2661
+ },
2662
+ } as any,
2663
+ });
2664
+
2665
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: baseTaskData});
2666
+ service.send({type: TaskEvent.ASSIGN, taskData: baseTaskData});
2667
+ service.send({
2668
+ type: TaskEvent.CONSULT,
2669
+ destination: '+13159998059',
2670
+ destinationType: 'entryPoint',
2671
+ });
2672
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: baseTaskData});
2673
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
2674
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: baseTaskData});
2675
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2676
+ expect(service.getSnapshot().context.hideBlindTransferForEpDnPendingMerge).toBe(true);
2677
+
2678
+ const consultFailedTaskData = createTaskData({
2679
+ agentId,
2680
+ consultingAgentId: agentId,
2681
+ consultMediaResourceId: consultMediaId,
2682
+ isConsulted: false,
2683
+ destinationType: 'Agent',
2684
+ reason: 'ANOTHER_CONSULT_IN_PROGRESS',
2685
+ type: 'AgentConsultFailed' as any,
2686
+ interaction: {
2687
+ state: 'conference',
2688
+ interactionId: 'interaction-1',
2689
+ mainInteractionId: 'interaction-1',
2690
+ owner: agentId,
2691
+ participants: {
2692
+ [agentId]: {
2693
+ id: agentId,
2694
+ pType: 'Agent',
2695
+ hasJoined: true,
2696
+ hasLeft: false,
2697
+ consultState: 'conferencing',
2698
+ },
2699
+ '+13159998059': {id: '+13159998059', pType: 'EP-DN', hasLeft: false},
2700
+ '+14696762938': {
2701
+ id: '+14696762938',
2702
+ pType: 'Customer',
2703
+ hasJoined: true,
2704
+ hasLeft: false,
2705
+ },
2706
+ },
2707
+ media: {
2708
+ 'interaction-1': {
2709
+ mediaResourceId: 'interaction-1',
2710
+ mType: 'mainCall',
2711
+ participants: ['+14696762938', agentId],
2712
+ isHold: false,
2713
+ },
2714
+ [consultMediaId]: {
2715
+ mediaResourceId: consultMediaId,
2716
+ mType: 'consult',
2717
+ participants: ['+13159998059', agentId],
2718
+ isHold: false,
2719
+ },
2720
+ },
2721
+ } as any,
2722
+ });
2723
+ service.send({
2724
+ type: TaskEvent.CONSULT_FAILED,
2725
+ taskData: consultFailedTaskData,
2726
+ reason: 'ANOTHER_CONSULT_IN_PROGRESS',
2727
+ });
2728
+
2729
+ const snapshot = service.getSnapshot();
2730
+ expect(snapshot.value).toBe(TaskState.CONFERENCING);
2731
+ expect(snapshot.context.hideBlindTransferForEpDnPendingMerge).toBe(true);
2732
+ expect(snapshot.context.uiControls.main.transfer).toEqual({
2733
+ isVisible: false,
2734
+ isEnabled: false,
2735
+ });
2736
+ expect(snapshot.context.uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
2737
+ });
2738
+ });
2739
+
2740
+ describe('CONFERENCING state TASK_WRAPUP after EP-DN post-call (CAI-8329)', () => {
2741
+ it('transitions to WRAPPING_UP on AgentWrapup while in CONFERENCING post_call', () => {
2742
+ const service = startMachine();
2743
+ const agentId = 'agent-1';
2744
+ const consultMediaId = 'consult-media-1';
2745
+ const baseTaskData = createTaskData({
2746
+ agentId,
2747
+ consultingAgentId: agentId,
2748
+ consultMediaResourceId: consultMediaId,
2749
+ isConsulted: false,
2750
+ interaction: {
2751
+ state: 'conference',
2752
+ interactionId: 'interaction-1',
2753
+ mainInteractionId: 'interaction-1',
2754
+ owner: agentId,
2755
+ participants: {
2756
+ [agentId]: {
2757
+ id: agentId,
2758
+ pType: 'Agent',
2759
+ hasJoined: true,
2760
+ hasLeft: false,
2761
+ consultState: 'conferencing',
2762
+ },
2763
+ '+13159998059': {id: '+13159998059', pType: 'EP-DN', hasLeft: false},
2764
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: false},
2765
+ },
2766
+ media: {
2767
+ 'interaction-1': {
2768
+ mediaResourceId: 'interaction-1',
2769
+ mType: 'mainCall',
2770
+ participants: ['customer-1', agentId],
2771
+ isHold: false,
2772
+ },
2773
+ [consultMediaId]: {
2774
+ mediaResourceId: consultMediaId,
2775
+ mType: 'consult',
2776
+ participants: ['+13159998059', agentId],
2777
+ isHold: false,
2778
+ },
2779
+ },
2780
+ } as any,
2781
+ });
2782
+
2783
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: baseTaskData});
2784
+ service.send({type: TaskEvent.ASSIGN, taskData: baseTaskData});
2785
+ service.send({
2786
+ type: TaskEvent.CONSULT,
2787
+ destination: '+13159998059',
2788
+ destinationType: 'entryPoint',
2789
+ });
2790
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: baseTaskData});
2791
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
2792
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: baseTaskData});
2793
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2794
+
2795
+ const postCallTaskData = createTaskData({
2796
+ agentId,
2797
+ consultMediaResourceId: consultMediaId,
2798
+ isConsulted: false,
2799
+ type: 'ParticipantPostCallActivity' as any,
2800
+ interaction: {
2801
+ state: 'post_call',
2802
+ interactionId: 'interaction-1',
2803
+ mainInteractionId: 'interaction-1',
2804
+ owner: agentId,
2805
+ isTerminated: false,
2806
+ callProcessingDetails: {hasCustomerLeft: 'true'},
2807
+ participants: {
2808
+ [agentId]: {
2809
+ id: agentId,
2810
+ pType: 'Agent',
2811
+ hasJoined: true,
2812
+ hasLeft: false,
2813
+ consultState: 'conferencing',
2814
+ currentState: 'post_call',
2815
+ },
2816
+ '+13159998059': {id: '+13159998059', pType: 'EP-DN', hasLeft: false},
2817
+ '+14696762938': {id: '+14696762938', pType: 'Customer', hasJoined: true, hasLeft: true},
2818
+ },
2819
+ media: {
2820
+ 'interaction-1': {
2821
+ mediaResourceId: 'interaction-1',
2822
+ mType: 'mainCall',
2823
+ participants: [agentId],
2824
+ isHold: false,
2825
+ },
2826
+ [consultMediaId]: {
2827
+ mediaResourceId: consultMediaId,
2828
+ mType: 'consult',
2829
+ participants: ['+13159998059', agentId],
2830
+ isHold: false,
2831
+ },
2832
+ },
2833
+ } as any,
2834
+ });
2835
+ service.send({type: TaskEvent.PARTICIPANT_LEAVE, taskData: postCallTaskData});
2836
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2837
+ expect(service.getSnapshot().context.hideBlindTransferForEpDnPendingMerge).toBe(true);
2838
+
2839
+ const wrapupTaskData = createTaskData({
2840
+ agentId,
2841
+ wrapUpRequired: true,
2842
+ type: 'AgentWrapup' as any,
2843
+ interaction: {
2844
+ state: 'post_call',
2845
+ interactionId: 'interaction-1',
2846
+ mainInteractionId: 'interaction-1',
2847
+ owner: agentId,
2848
+ isTerminated: true,
2849
+ participants: {
2850
+ [agentId]: {
2851
+ id: agentId,
2852
+ pType: 'Agent',
2853
+ hasJoined: true,
2854
+ hasLeft: false,
2855
+ consultState: 'consultCompleted',
2856
+ isWrapUp: true,
2857
+ },
2858
+ '+14696762938': {id: '+14696762938', pType: 'Customer', hasJoined: true, hasLeft: true},
2859
+ },
2860
+ media: {
2861
+ 'interaction-1': {
2862
+ mediaResourceId: 'interaction-1',
2863
+ mType: 'mainCall',
2864
+ participants: [],
2865
+ isHold: false,
2866
+ },
2867
+ },
2868
+ } as any,
2869
+ });
2870
+ service.send({type: TaskEvent.TASK_WRAPUP, taskData: wrapupTaskData});
2871
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2872
+ });
2873
+ });
2874
+
2875
+ describe('CONFERENCING state CONSULT_END with terminated interaction', () => {
2876
+ it('transitions to WRAPPING_UP when CONSULT_END arrives with isTerminated in CONFERENCING', () => {
2877
+ const service = startMachine();
2878
+ const taskData = createTaskData({
2879
+ consultingAgentId: 'agent-1',
2880
+ interaction: {
2881
+ owner: 'agent-1',
2882
+ state: 'conference',
2883
+ } as any,
2884
+ });
2885
+
2886
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2887
+ service.send({type: TaskEvent.ASSIGN, taskData});
2888
+ service.send({
2889
+ type: TaskEvent.CONSULT,
2890
+ destination: 'agent-42',
2891
+ destinationType: 'agent',
2892
+ });
2893
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
2894
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
2895
+ service.send({type: TaskEvent.CONFERENCE_START, taskData});
2896
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
2897
+
2898
+ const terminatedTaskData = createTaskData({
2899
+ consultingAgentId: 'agent-1',
2900
+ interaction: {
2901
+ isTerminated: true,
2902
+ owner: 'agent-1',
2903
+ state: 'conference',
2904
+ } as any,
2905
+ });
2906
+ service.send({type: TaskEvent.CONSULT_END, taskData: terminatedTaskData});
2907
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2908
+ });
2909
+ });
2910
+
2911
+ describe('OFFERED state event handlers', () => {
2912
+ it('transitions to TERMINATED when customer disconnects before agent answers', () => {
2913
+ const service = startMachine();
2914
+ const taskData = createTaskData({isConsulted: false});
2915
+
2916
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2917
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
2918
+
2919
+ service.send({type: TaskEvent.CONTACT_ENDED, taskData});
2920
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2921
+ });
2922
+
2923
+ it('transitions to TERMINATED when consulted agent does not answer', () => {
2924
+ const service = startMachine();
2925
+ const taskData = createTaskData({
2926
+ isConsulted: true,
2927
+ consultingAgentId: 'agent-1',
2928
+ });
2929
+
2930
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2931
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
2932
+
2933
+ service.send({type: TaskEvent.CONSULT_FAILED, taskData});
2934
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2935
+ });
2936
+
2937
+ it('emits only consult-end when an unaccepted consulted offer is ended', () => {
2938
+ const emitTaskConsultEnd = jest.fn();
2939
+ const emitTaskEnd = jest.fn();
2940
+ const service = createActor(
2941
+ createTaskStateMachine(createConfig(), {
2942
+ actions: {emitTaskConsultEnd, emitTaskEnd},
2943
+ })
2944
+ );
2945
+ service.start();
2946
+ const taskData = createTaskData({isConsulted: true, consultingAgentId: 'agent-1'});
2947
+
2948
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
2949
+ service.send({type: TaskEvent.CONSULT_END, taskData});
2950
+
2951
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
2952
+ expect(emitTaskConsultEnd).toHaveBeenCalledTimes(1);
2953
+ expect(emitTaskEnd).not.toHaveBeenCalled();
2954
+ });
2955
+
2956
+ it('transitions to WRAPPING_UP on TASK_WRAPUP when wrapup is required', () => {
2957
+ const service = startMachine();
2958
+ const offerTaskData = createTaskData({
2959
+ interaction: {
2960
+ outboundType: 'OUTDIAL',
2961
+ } as any,
2962
+ });
2963
+
2964
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
2965
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
2966
+
2967
+ const wrapupTaskData = createTaskData({
2968
+ agentId: 'agent-1',
2969
+ agentsPendingWrapUp: ['agent-1'],
2970
+ wrapUpRequired: true,
2971
+ interaction: {
2972
+ outboundType: 'OUTDIAL',
2973
+ state: 'wrapUp',
2974
+ isTerminated: true,
2975
+ } as any,
2976
+ });
2977
+ service.send({type: TaskEvent.TASK_WRAPUP, taskData: wrapupTaskData});
2978
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
2979
+ });
2980
+
2981
+ it('emits wrap-up once on TASK_WRAPUP from OFFERED when wrapup is required', () => {
2982
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
2983
+ const service = createActor(
2984
+ createTaskStateMachine(createConfig(), {actions: {emitTaskWrapup}})
2985
+ );
2986
+ service.start();
2987
+ const offerTaskData = createTaskData({
2988
+ interaction: {outboundType: 'OUTDIAL'} as any,
2989
+ });
2990
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
2991
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
2992
+
2993
+ const wrapupTaskData = createTaskData({
2994
+ agentId: 'agent-1',
2995
+ wrapUpRequired: true,
2996
+ interaction: {outboundType: 'OUTDIAL', state: 'wrapUp', isTerminated: true} as any,
2997
+ });
2998
+ service.send({type: TaskEvent.TASK_WRAPUP, taskData: wrapupTaskData});
2999
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3000
+ expect(publishWrapup).toHaveBeenCalledTimes(1);
3001
+ });
3002
+ });
3003
+
3004
+ describe('OUTBOUND_FAILED handling', () => {
3005
+ it('transitions from IDLE to TERMINATED on OUTBOUND_FAILED (race condition)', () => {
3006
+ const service = startMachine();
3007
+ expect(service.getSnapshot().value).toBe(TaskState.IDLE);
3008
+
3009
+ const taskData = createTaskData({
3010
+ interaction: {
3011
+ outboundType: 'OUTDIAL',
3012
+ isTerminated: true,
3013
+ } as any,
3014
+ });
3015
+
3016
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData, reason: 'CUSTOMER_BUSY'});
3017
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3018
+ });
3019
+
3020
+ it('transitions from OFFERED to TERMINATED on OUTBOUND_FAILED without wrapup', () => {
3021
+ const service = startMachine();
3022
+ const offerTaskData = createTaskData({
3023
+ interaction: {
3024
+ outboundType: 'OUTDIAL',
3025
+ } as any,
3026
+ });
3027
+
3028
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
3029
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
3030
+
3031
+ const failedTaskData = createTaskData({
3032
+ interaction: {
3033
+ outboundType: 'OUTDIAL',
3034
+ isTerminated: true,
3035
+ } as any,
3036
+ });
3037
+ service.send({
3038
+ type: TaskEvent.OUTBOUND_FAILED,
3039
+ taskData: failedTaskData,
3040
+ reason: 'CUSTOMER_BUSY',
3041
+ });
3042
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3043
+ });
3044
+
3045
+ it('transitions from OFFERED to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3046
+ const service = startMachine();
3047
+ const offerTaskData = createTaskData({
3048
+ interaction: {
3049
+ outboundType: 'OUTDIAL',
3050
+ } as any,
3051
+ });
3052
+
3053
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
3054
+ expect(service.getSnapshot().value).toBe(TaskState.OFFERED);
3055
+
3056
+ const failedTaskData = createTaskData({
3057
+ agentId: 'agent-1',
3058
+ agentsPendingWrapUp: ['agent-1'],
3059
+ interaction: {
3060
+ outboundType: 'OUTDIAL',
3061
+ isTerminated: true,
3062
+ } as any,
3063
+ });
3064
+ service.send({
3065
+ type: TaskEvent.OUTBOUND_FAILED,
3066
+ taskData: failedTaskData,
3067
+ reason: 'CUSTOMER_BUSY',
3068
+ });
3069
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3070
+ });
3071
+
3072
+ it('transitions from CONNECTED to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3073
+ const service = startMachine();
3074
+ const offerTaskData = createTaskData({
3075
+ interaction: {
3076
+ outboundType: 'OUTDIAL',
3077
+ } as any,
3078
+ });
3079
+
3080
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: offerTaskData});
3081
+ service.send({type: TaskEvent.ASSIGN, taskData: offerTaskData});
3082
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
3083
+
3084
+ const failedTaskData = createTaskData({
3085
+ agentId: 'agent-1',
3086
+ agentsPendingWrapUp: ['agent-1'],
3087
+ interaction: {
3088
+ outboundType: 'OUTDIAL',
3089
+ isTerminated: true,
3090
+ } as any,
3091
+ });
3092
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3093
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3094
+ });
3095
+
3096
+ it('transitions from HELD to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3097
+ const service = startMachine();
3098
+ const taskData = createTaskData({
3099
+ interaction: {
3100
+ outboundType: 'OUTDIAL',
3101
+ } as any,
3102
+ });
3103
+
3104
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3105
+ service.send({type: TaskEvent.ASSIGN, taskData});
3106
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
3107
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId});
3108
+ expect(service.getSnapshot().value).toBe(TaskState.HELD);
3109
+
3110
+ const failedTaskData = createTaskData({
3111
+ agentId: 'agent-1',
3112
+ agentsPendingWrapUp: ['agent-1'],
3113
+ interaction: {
3114
+ outboundType: 'OUTDIAL',
3115
+ isTerminated: true,
3116
+ } as any,
3117
+ });
3118
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3119
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3120
+ });
3121
+
3122
+ it('transitions from CONSULTING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3123
+ const service = startMachine();
3124
+ const taskData = createTaskData({
3125
+ interaction: {
3126
+ outboundType: 'OUTDIAL',
3127
+ } as any,
3128
+ });
3129
+
3130
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3131
+ service.send({type: TaskEvent.ASSIGN, taskData});
3132
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-42', destinationType: 'agent'});
3133
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
3134
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
3135
+
3136
+ const failedTaskData = createTaskData({
3137
+ agentId: 'agent-1',
3138
+ agentsPendingWrapUp: ['agent-1'],
3139
+ interaction: {
3140
+ outboundType: 'OUTDIAL',
3141
+ isTerminated: true,
3142
+ } as any,
3143
+ });
3144
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3145
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3146
+ });
3147
+
3148
+ it('transitions from CONFERENCING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3149
+ const service = startMachine();
3150
+ const taskData = createTaskData({
3151
+ consultingAgentId: 'agent-1',
3152
+ interaction: {
3153
+ outboundType: 'OUTDIAL',
3154
+ } as any,
3155
+ });
3156
+
3157
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3158
+ service.send({type: TaskEvent.ASSIGN, taskData});
3159
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-42', destinationType: 'agent'});
3160
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
3161
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
3162
+ service.send({type: TaskEvent.CONFERENCE_START});
3163
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
3164
+
3165
+ const failedTaskData = createTaskData({
3166
+ agentId: 'agent-1',
3167
+ agentsPendingWrapUp: ['agent-1'],
3168
+ interaction: {
3169
+ outboundType: 'OUTDIAL',
3170
+ isTerminated: true,
3171
+ } as any,
3172
+ });
3173
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3174
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3175
+ });
3176
+
3177
+ it('transitions from HOLD_INITIATING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3178
+ const service = startMachine();
3179
+ const taskData = createTaskData({
3180
+ interaction: {
3181
+ outboundType: 'OUTDIAL',
3182
+ } as any,
3183
+ });
3184
+
3185
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3186
+ service.send({type: TaskEvent.ASSIGN, taskData});
3187
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
3188
+ expect(service.getSnapshot().value).toBe(TaskState.HOLD_INITIATING);
3189
+
3190
+ const failedTaskData = createTaskData({
3191
+ agentId: 'agent-1',
3192
+ agentsPendingWrapUp: ['agent-1'],
3193
+ interaction: {
3194
+ outboundType: 'OUTDIAL',
3195
+ isTerminated: true,
3196
+ } as any,
3197
+ });
3198
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3199
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3200
+ });
3201
+
3202
+ it('transitions from RESUME_INITIATING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3203
+ const service = startMachine();
3204
+ const taskData = createTaskData({
3205
+ interaction: {
3206
+ outboundType: 'OUTDIAL',
3207
+ } as any,
3208
+ });
3209
+
3210
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3211
+ service.send({type: TaskEvent.ASSIGN, taskData});
3212
+ service.send({type: TaskEvent.HOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
3213
+ service.send({type: TaskEvent.HOLD_SUCCESS, mediaResourceId: taskData.mediaResourceId});
3214
+ service.send({type: TaskEvent.UNHOLD_INITIATED, mediaResourceId: taskData.mediaResourceId});
3215
+ expect(service.getSnapshot().value).toBe(TaskState.RESUME_INITIATING);
3216
+
3217
+ const failedTaskData = createTaskData({
3218
+ agentId: 'agent-1',
3219
+ agentsPendingWrapUp: ['agent-1'],
3220
+ interaction: {
3221
+ outboundType: 'OUTDIAL',
3222
+ isTerminated: true,
3223
+ } as any,
3224
+ });
3225
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3226
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3227
+ });
3228
+
3229
+ it('transitions from CONSULT_INITIATING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3230
+ const service = startMachine();
3231
+ const taskData = createTaskData({
3232
+ interaction: {
3233
+ outboundType: 'OUTDIAL',
3234
+ } as any,
3235
+ });
3236
+
3237
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3238
+ service.send({type: TaskEvent.ASSIGN, taskData});
3239
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-42', destinationType: 'agent'});
3240
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
3241
+
3242
+ const failedTaskData = createTaskData({
3243
+ agentId: 'agent-1',
3244
+ agentsPendingWrapUp: ['agent-1'],
3245
+ interaction: {
3246
+ outboundType: 'OUTDIAL',
3247
+ isTerminated: true,
3248
+ } as any,
3249
+ });
3250
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3251
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3252
+ });
3253
+
3254
+ it('transitions from CONF_INITIATING to WRAPPING_UP on OUTBOUND_FAILED when wrapup is required', () => {
3255
+ const service = startMachine();
3256
+ const taskData = createTaskData({
3257
+ consultingAgentId: 'agent-1',
3258
+ interaction: {
3259
+ outboundType: 'OUTDIAL',
3260
+ } as any,
3261
+ });
3262
+
3263
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3264
+ service.send({type: TaskEvent.ASSIGN, taskData});
3265
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-42', destinationType: 'agent'});
3266
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
3267
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
3268
+ expect(service.getSnapshot().value).toBe(TaskState.CONF_INITIATING);
3269
+
3270
+ const failedTaskData = createTaskData({
3271
+ agentId: 'agent-1',
3272
+ agentsPendingWrapUp: ['agent-1'],
3273
+ interaction: {
3274
+ outboundType: 'OUTDIAL',
3275
+ isTerminated: true,
3276
+ } as any,
3277
+ });
3278
+ service.send({type: TaskEvent.OUTBOUND_FAILED, taskData: failedTaskData, reason: 'AGENT_ENDS'});
3279
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3280
+ });
3281
+ });
3282
+
3283
+ describe('CONSULTING state TRANSFER_CONFERENCE_SUCCESS desktop-initiated (CAI-8329)', () => {
3284
+ const startAgent2Machine = () => {
3285
+ const actor = createActor(createTaskStateMachine({...createConfig(), agentId: 'agent-2'}));
3286
+ actor.start();
3287
+
3288
+ return actor;
3289
+ };
3290
+
3291
+ const createSecondaryAgentConferenceBaseTaskData = () =>
3292
+ createTaskData({
3293
+ agentId: 'agent-2',
3294
+ isConsulted: true,
3295
+ interactionId: 'interaction-1',
3296
+ mediaResourceId: 'interaction-1',
3297
+ consultMediaResourceId: 'consult-media-1',
3298
+ interaction: {
3299
+ state: 'conference',
3300
+ owner: 'agent-1',
3301
+ mainInteractionId: 'interaction-1',
3302
+ interactionId: 'interaction-1',
3303
+ participants: {
3304
+ 'agent-1': {
3305
+ id: 'agent-1',
3306
+ pType: 'Agent',
3307
+ hasLeft: false,
3308
+ consultState: 'conferencing',
3309
+ },
3310
+ 'agent-2': {
3311
+ id: 'agent-2',
3312
+ pType: 'Agent',
3313
+ hasLeft: false,
3314
+ isConsulted: true,
3315
+ consultState: 'conferencing',
3316
+ },
3317
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
3318
+ },
3319
+ media: {
3320
+ 'interaction-1': {
3321
+ mediaResourceId: 'interaction-1',
3322
+ mType: 'mainCall',
3323
+ participants: ['agent-1', 'agent-2', 'customer-1'],
3324
+ isHold: false,
3325
+ },
3326
+ },
3327
+ } as any,
3328
+ });
3329
+
3330
+ const createSecondaryAgentConsultingTaskData = () =>
3331
+ createTaskData({
3332
+ agentId: 'agent-2',
3333
+ isConsulted: true,
3334
+ consultingAgentId: 'agent-2',
3335
+ type: 'AgentConferenceTransferred' as any,
3336
+ interactionId: 'interaction-1',
3337
+ mediaResourceId: 'interaction-1',
3338
+ consultMediaResourceId: 'consult-media-1',
3339
+ interaction: {
3340
+ state: 'conference',
3341
+ owner: 'agent-1',
3342
+ mainInteractionId: 'interaction-1',
3343
+ interactionId: 'interaction-1',
3344
+ participants: {
3345
+ 'agent-1': {
3346
+ id: 'agent-1',
3347
+ pType: 'Agent',
3348
+ hasLeft: false,
3349
+ consultState: 'conferencing',
3350
+ },
3351
+ 'agent-2': {
3352
+ id: 'agent-2',
3353
+ pType: 'Agent',
3354
+ hasLeft: false,
3355
+ isConsulted: true,
3356
+ consultState: 'consulting',
3357
+ },
3358
+ 'agent-3': {
3359
+ id: 'agent-3',
3360
+ pType: 'Agent',
3361
+ hasLeft: false,
3362
+ isConsulted: true,
3363
+ consultState: 'consulting',
3364
+ },
3365
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
3366
+ },
3367
+ media: {
3368
+ 'interaction-1': {
3369
+ mediaResourceId: 'interaction-1',
3370
+ mType: 'mainCall',
3371
+ participants: ['agent-1', 'agent-2', 'customer-1'],
3372
+ isHold: false,
3373
+ },
3374
+ 'consult-media-1': {
3375
+ mediaResourceId: 'consult-media-1',
3376
+ mType: 'consult',
3377
+ participants: ['agent-2', 'agent-3'],
3378
+ isHold: false,
3379
+ },
3380
+ },
3381
+ } as any,
3382
+ });
3383
+
3384
+ const createSecondaryAgentRemovedTaskData = () =>
3385
+ createTaskData({
3386
+ agentId: 'agent-2',
3387
+ isConsulted: true,
3388
+ interactionId: 'interaction-1',
3389
+ mediaResourceId: 'interaction-1',
3390
+ consultMediaResourceId: 'consult-media-1',
3391
+ interaction: {
3392
+ state: 'conference',
3393
+ owner: 'agent-1',
3394
+ mainInteractionId: 'interaction-1',
3395
+ interactionId: 'interaction-1',
3396
+ participants: {
3397
+ 'agent-1': {
3398
+ id: 'agent-1',
3399
+ pType: 'Agent',
3400
+ hasLeft: false,
3401
+ consultState: 'conferencing',
3402
+ },
3403
+ 'agent-3': {
3404
+ id: 'agent-3',
3405
+ pType: 'Agent',
3406
+ hasLeft: false,
3407
+ isConsulted: true,
3408
+ consultState: 'consulting',
3409
+ },
3410
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
3411
+ },
3412
+ media: {
3413
+ 'arbitrary-main-media-key': {
3414
+ mediaResourceId: 'interaction-1',
3415
+ mType: 'mainCall',
3416
+ participants: ['agent-1', 'customer-1'],
3417
+ isHold: false,
3418
+ },
3419
+ 'consult-media-1': {
3420
+ mediaResourceId: 'consult-media-1',
3421
+ mType: 'consult',
3422
+ participants: ['agent-2', 'agent-3'],
3423
+ isHold: false,
3424
+ },
3425
+ },
3426
+ } as any,
3427
+ });
3428
+
3429
+ const setupSecondaryAgentConsultingFromConference = (
3430
+ service: ReturnType<typeof startAgent2Machine>
3431
+ ) => {
3432
+ const conferenceTaskData = createSecondaryAgentConferenceBaseTaskData();
3433
+ const consultingTaskData = createSecondaryAgentConsultingTaskData();
3434
+
3435
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
3436
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
3437
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
3438
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
3439
+
3440
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
3441
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
3442
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData: consultingTaskData});
3443
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
3444
+ expect(service.getSnapshot().context.consultInitiator).toBe(true);
3445
+ expect(service.getSnapshot().context.consultFromConference).toBe(true);
3446
+ };
3447
+
3448
+ const setupSecondaryAgentDesktopConsultingFromConference = (
3449
+ service: ReturnType<typeof startAgent2Machine>
3450
+ ) => {
3451
+ const conferenceTaskData = createSecondaryAgentConferenceBaseTaskData();
3452
+ const consultingTaskData = createSecondaryAgentConsultingTaskData();
3453
+ const consultingActivePayload = {
3454
+ ...consultingTaskData,
3455
+ type: 'AgentConsulting' as any,
3456
+ };
3457
+ delete (consultingActivePayload as {consultingAgentId?: string}).consultingAgentId;
3458
+
3459
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
3460
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
3461
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
3462
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
3463
+
3464
+ service.send({
3465
+ type: TaskEvent.CONSULTING_ACTIVE,
3466
+ consultDestinationAgentJoined: true,
3467
+ taskData: consultingActivePayload,
3468
+ });
3469
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
3470
+ expect(service.getSnapshot().context.consultFromConference).toBe(true);
3471
+ };
3472
+
3473
+ it('terminates secondary agent on true desktop path without widgets CONSULT event', () => {
3474
+ const service = startAgent2Machine();
3475
+ setupSecondaryAgentDesktopConsultingFromConference(service);
3476
+
3477
+ const transferSuccessTaskData = createSecondaryAgentConsultingTaskData();
3478
+ service.send({
3479
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3480
+ taskData: transferSuccessTaskData,
3481
+ });
3482
+
3483
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3484
+ });
3485
+
3486
+ it('terminates secondary agent on widgets-initiated consult conference transfer from CONSULTING', () => {
3487
+ const service = startAgent2Machine();
3488
+ setupSecondaryAgentConsultingFromConference(service);
3489
+
3490
+ const transferSuccessTaskData = createSecondaryAgentConsultingTaskData();
3491
+ service.send({
3492
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3493
+ taskData: transferSuccessTaskData,
3494
+ });
3495
+
3496
+ const snapshot = service.getSnapshot();
3497
+ expect(snapshot.value).toBe(TaskState.TERMINATED);
3498
+ expect(snapshot.context.consultInitiator).toBe(false);
3499
+ expect(snapshot.context.consultFromConference).toBe(false);
3500
+ });
3501
+
3502
+ it('moves consultee to CONFERENCING when primary agent conference-transfers', () => {
3503
+ const startAgent3Machine = () => {
3504
+ const actor = createActor(createTaskStateMachine({...createConfig(), agentId: 'agent-3'}));
3505
+ actor.start();
3506
+
3507
+ return actor;
3508
+ };
3509
+
3510
+ const conferenceTaskData = createTaskData({
3511
+ agentId: 'agent-3',
3512
+ isConsulted: true,
3513
+ interactionId: 'interaction-1',
3514
+ mediaResourceId: 'interaction-1',
3515
+ interaction: {
3516
+ state: 'conference',
3517
+ owner: 'agent-1',
3518
+ mainInteractionId: 'interaction-1',
3519
+ interactionId: 'interaction-1',
3520
+ participants: {
3521
+ 'agent-1': {
3522
+ id: 'agent-1',
3523
+ pType: 'Agent',
3524
+ hasLeft: false,
3525
+ consultState: 'conferencing',
3526
+ },
3527
+ 'agent-2': {
3528
+ id: 'agent-2',
3529
+ pType: 'Agent',
3530
+ hasLeft: false,
3531
+ isConsulted: true,
3532
+ consultState: 'conferencing',
3533
+ },
3534
+ 'agent-3': {
3535
+ id: 'agent-3',
3536
+ pType: 'Agent',
3537
+ hasLeft: false,
3538
+ isConsulted: true,
3539
+ consultState: 'conferencing',
3540
+ },
3541
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
3542
+ },
3543
+ media: {
3544
+ 'interaction-1': {
3545
+ mediaResourceId: 'interaction-1',
3546
+ mType: 'mainCall',
3547
+ participants: ['agent-1', 'agent-2', 'agent-3', 'customer-1'],
3548
+ isHold: false,
3549
+ },
3550
+ },
3551
+ } as any,
3552
+ });
3553
+
3554
+ const consultingTaskData = createTaskData({
3555
+ agentId: 'agent-3',
3556
+ isConsulted: true,
3557
+ consultingAgentId: 'agent-1',
3558
+ interactionId: 'interaction-1',
3559
+ mediaResourceId: 'interaction-1',
3560
+ consultMediaResourceId: 'consult-media-1',
3561
+ interaction: {
3562
+ state: 'conference',
3563
+ owner: 'agent-1',
3564
+ mainInteractionId: 'interaction-1',
3565
+ interactionId: 'interaction-1',
3566
+ participants: {
3567
+ 'agent-1': {
3568
+ id: 'agent-1',
3569
+ pType: 'Agent',
3570
+ hasLeft: false,
3571
+ consultState: 'consulting',
3572
+ },
3573
+ 'agent-2': {
3574
+ id: 'agent-2',
3575
+ pType: 'Agent',
3576
+ hasLeft: false,
3577
+ isConsulted: true,
3578
+ consultState: 'conferencing',
3579
+ },
3580
+ 'agent-3': {
3581
+ id: 'agent-3',
3582
+ pType: 'Agent',
3583
+ hasLeft: false,
3584
+ isConsulted: true,
3585
+ consultState: 'consulting',
3586
+ },
3587
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
3588
+ },
3589
+ media: {
3590
+ 'interaction-1': {
3591
+ mediaResourceId: 'interaction-1',
3592
+ mType: 'mainCall',
3593
+ participants: ['agent-1', 'agent-2', 'customer-1'],
3594
+ isHold: false,
3595
+ },
3596
+ 'consult-media-1': {
3597
+ mediaResourceId: 'consult-media-1',
3598
+ mType: 'consult',
3599
+ participants: ['agent-1', 'agent-3'],
3600
+ isHold: false,
3601
+ },
3602
+ },
3603
+ } as any,
3604
+ });
3605
+
3606
+ const transferSuccessTaskData = createTaskData({
3607
+ agentId: 'agent-3',
3608
+ isConsulted: true,
3609
+ consultingAgentId: 'agent-1',
3610
+ wrapUpRequired: false,
3611
+ type: 'AgentConferenceTransferred' as any,
3612
+ interactionId: 'interaction-1',
3613
+ mediaResourceId: 'interaction-1',
3614
+ interaction: {
3615
+ state: 'conference',
3616
+ owner: 'agent-1',
3617
+ mainInteractionId: 'interaction-1',
3618
+ interactionId: 'interaction-1',
3619
+ participants: {
3620
+ 'agent-1': {
3621
+ id: 'agent-1',
3622
+ pType: 'Agent',
3623
+ hasLeft: true,
3624
+ consultState: 'conferencing',
3625
+ },
3626
+ 'agent-2': {
3627
+ id: 'agent-2',
3628
+ pType: 'Agent',
3629
+ hasLeft: false,
3630
+ isConsulted: true,
3631
+ consultState: 'conferencing',
3632
+ },
3633
+ 'agent-3': {
3634
+ id: 'agent-3',
3635
+ pType: 'Agent',
3636
+ hasLeft: false,
3637
+ isConsulted: true,
3638
+ consultState: 'consulting',
3639
+ },
3640
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
3641
+ },
3642
+ media: {
3643
+ 'interaction-1': {
3644
+ mediaResourceId: 'interaction-1',
3645
+ mType: 'mainCall',
3646
+ participants: ['agent-2', 'agent-3', 'customer-1'],
3647
+ isHold: false,
3648
+ },
3649
+ },
3650
+ } as any,
3651
+ });
3652
+
3653
+ const service = startAgent3Machine();
3654
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
3655
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
3656
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
3657
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
3658
+
3659
+ service.send({
3660
+ type: TaskEvent.CONSULTING_ACTIVE,
3661
+ consultDestinationAgentJoined: true,
3662
+ taskData: consultingTaskData,
3663
+ });
3664
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
3665
+ expect(service.getSnapshot().context.consultInitiator).toBe(false);
3666
+
3667
+ service.send({
3668
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3669
+ taskData: transferSuccessTaskData,
3670
+ });
3671
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
3672
+ });
3673
+
3674
+ it('passive observer stays in CONFERENCING when another agent conference-transfers', () => {
3675
+ const service = startAgent2Machine();
3676
+ const conferenceTaskData = createSecondaryAgentConferenceBaseTaskData();
3677
+ const transferSuccessTaskData = createTaskData({
3678
+ agentId: 'agent-2',
3679
+ isConsulted: true,
3680
+ type: 'AgentConferenceTransferred' as any,
3681
+ interactionId: 'interaction-1',
3682
+ mediaResourceId: 'interaction-1',
3683
+ interaction: {
3684
+ state: 'conference',
3685
+ owner: 'agent-1',
3686
+ mainInteractionId: 'interaction-1',
3687
+ interactionId: 'interaction-1',
3688
+ participants: {
3689
+ 'agent-1': {
3690
+ id: 'agent-1',
3691
+ pType: 'Agent',
3692
+ hasLeft: false,
3693
+ consultState: 'conferencing',
3694
+ },
3695
+ 'agent-2': {
3696
+ id: 'agent-2',
3697
+ pType: 'Agent',
3698
+ hasLeft: false,
3699
+ isConsulted: true,
3700
+ consultState: 'conferencing',
3701
+ },
3702
+ 'agent-3': {
3703
+ id: 'agent-3',
3704
+ pType: 'Agent',
3705
+ hasLeft: false,
3706
+ isConsulted: true,
3707
+ consultState: 'conferencing',
3708
+ },
3709
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
3710
+ },
3711
+ media: {
3712
+ 'interaction-1': {
3713
+ mediaResourceId: 'interaction-1',
3714
+ mType: 'mainCall',
3715
+ participants: ['agent-1', 'agent-2', 'agent-3', 'customer-1'],
3716
+ isHold: false,
3717
+ },
3718
+ },
3719
+ } as any,
3720
+ });
3721
+
3722
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
3723
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
3724
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
3725
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
3726
+ expect(service.getSnapshot().context.consultInitiator).toBe(false);
3727
+
3728
+ service.send({
3729
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3730
+ taskData: transferSuccessTaskData,
3731
+ });
3732
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
3733
+ });
3734
+
3735
+ it('terminates secondary agent on widgets-initiated conference transfer regression', () => {
3736
+ const service = startAgent2Machine();
3737
+ setupSecondaryAgentConsultingFromConference(service);
3738
+
3739
+ const transferSuccessTaskData = createSecondaryAgentConsultingTaskData();
3740
+ service.send({type: TaskEvent.TRANSFER_CONFERENCE});
3741
+ service.send({
3742
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3743
+ taskData: transferSuccessTaskData,
3744
+ });
3745
+
3746
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3747
+ });
3748
+
3749
+ it('emits wrap-up once on TRANSFER_CONFERENCE_SUCCESS from CONSULTING when wrap-up is required', () => {
3750
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
3751
+ const service = createActor(
3752
+ createTaskStateMachine({...createConfig(), agentId: 'agent-1'}, {actions: {emitTaskWrapup}})
3753
+ );
3754
+ service.start();
3755
+ const taskData = createTaskData({
3756
+ agentId: 'agent-1',
3757
+ consultingAgentId: 'agent-1',
3758
+ interaction: {
3759
+ owner: 'agent-1',
3760
+ state: 'conference',
3761
+ mainInteractionId: 'interaction-1',
3762
+ interactionId: 'interaction-1',
3763
+ participants: {
3764
+ 'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
3765
+ 'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
3766
+ },
3767
+ } as any,
3768
+ });
3769
+
3770
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3771
+ service.send({type: TaskEvent.ASSIGN, taskData});
3772
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-2', destinationType: 'agent'});
3773
+ service.send({type: TaskEvent.CONSULT_SUCCESS, taskData});
3774
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
3775
+
3776
+ service.send({type: TaskEvent.TRANSFER_CONFERENCE});
3777
+ service.send({
3778
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3779
+ taskData: {...taskData, wrapUpRequired: true},
3780
+ });
3781
+
3782
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3783
+ expect(publishWrapup).toHaveBeenCalledTimes(1);
3784
+ });
3785
+
3786
+ it('emits wrap-up once on TRANSFER_CONFERENCE_SUCCESS from CONFERENCING when wrap-up is required', () => {
3787
+ const {emitTaskWrapup, publishWrapup} = createPublishAwareEmitTaskWrapupMock();
3788
+ const service = createActor(
3789
+ createTaskStateMachine({...createConfig(), agentId: 'agent-1'}, {actions: {emitTaskWrapup}})
3790
+ );
3791
+ service.start();
3792
+ const taskData = createTaskData({
3793
+ agentId: 'agent-1',
3794
+ interaction: {
3795
+ owner: 'agent-1',
3796
+ state: 'conference',
3797
+ mainInteractionId: 'interaction-1',
3798
+ interactionId: 'interaction-1',
3799
+ participants: {
3800
+ 'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
3801
+ 'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
3802
+ },
3803
+ } as any,
3804
+ });
3805
+
3806
+ service.send({type: TaskEvent.TASK_INCOMING, taskData});
3807
+ service.send({type: TaskEvent.ASSIGN, taskData});
3808
+ service.send({type: TaskEvent.MERGE_TO_CONFERENCE});
3809
+ service.send({type: TaskEvent.CONFERENCE_START, taskData});
3810
+ expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
3811
+
3812
+ service.send({type: TaskEvent.TRANSFER_CONFERENCE});
3813
+ service.send({
3814
+ type: TaskEvent.TRANSFER_CONFERENCE_SUCCESS,
3815
+ taskData: {...taskData, wrapUpRequired: true},
3816
+ });
3817
+
3818
+ expect(service.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
3819
+ expect(publishWrapup).toHaveBeenCalledTimes(1);
3820
+ });
3821
+
3822
+ it('terminates secondary agent on PARTICIPANT_LEAVE while in CONSULTING during conference', () => {
3823
+ const service = startAgent2Machine();
3824
+ setupSecondaryAgentDesktopConsultingFromConference(service);
3825
+
3826
+ const selfLeftTaskData = createTaskData({
3827
+ agentId: 'agent-2',
3828
+ isConsulted: true,
3829
+ participantId: 'agent-2',
3830
+ interactionId: 'interaction-1',
3831
+ mediaResourceId: 'interaction-1',
3832
+ interaction: {
3833
+ state: 'conference',
3834
+ owner: 'agent-1',
3835
+ mainInteractionId: 'interaction-1',
3836
+ interactionId: 'interaction-1',
3837
+ participants: {
3838
+ 'agent-1': {
3839
+ id: 'agent-1',
3840
+ pType: 'Agent',
3841
+ hasLeft: false,
3842
+ consultState: 'conferencing',
3843
+ },
3844
+ 'agent-3': {
3845
+ id: 'agent-3',
3846
+ pType: 'Agent',
3847
+ hasLeft: false,
3848
+ isConsulted: true,
3849
+ consultState: 'consulting',
3850
+ },
3851
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
3852
+ },
3853
+ media: {
3854
+ 'interaction-1': {
3855
+ mediaResourceId: 'interaction-1',
3856
+ mType: 'mainCall',
3857
+ participants: ['agent-1', 'agent-3', 'customer-1'],
3858
+ isHold: false,
3859
+ },
3860
+ },
3861
+ } as any,
3862
+ });
3863
+
3864
+ service.send({
3865
+ type: TaskEvent.PARTICIPANT_LEAVE,
3866
+ taskData: selfLeftTaskData,
3867
+ participantId: 'agent-2',
3868
+ });
3869
+
3870
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3871
+ });
3872
+
3873
+ it('terminates secondary agent on PARTICIPANT_LEAVE while consult is initiating', () => {
3874
+ const service = startAgent2Machine();
3875
+ const conferenceTaskData = createSecondaryAgentConferenceBaseTaskData();
3876
+
3877
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
3878
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
3879
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
3880
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
3881
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
3882
+ expect(service.getSnapshot().context.taskData.interaction).toBe(
3883
+ conferenceTaskData.interaction
3884
+ );
3885
+ expect(service.getSnapshot().context.taskData.destAgentId).toBe('agent-3');
3886
+
3887
+ const participantLeaveEvent = {
3888
+ type: TaskEvent.PARTICIPANT_LEAVE,
3889
+ taskData: createSecondaryAgentRemovedTaskData(),
3890
+ } as const;
3891
+ expect(
3892
+ guards.didCurrentAgentLeaveMainInteraction({
3893
+ context: service.getSnapshot().context,
3894
+ event: participantLeaveEvent,
3895
+ })
3896
+ ).toBe(true);
3897
+
3898
+ service.send(participantLeaveEvent);
3899
+
3900
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3901
+ });
3902
+
3903
+ it('terminates secondary agent on CONSULT_END while consult is initiating', () => {
3904
+ const service = startAgent2Machine();
3905
+ const conferenceTaskData = createSecondaryAgentConferenceBaseTaskData();
3906
+
3907
+ service.send({type: TaskEvent.TASK_INCOMING, taskData: conferenceTaskData});
3908
+ service.send({type: TaskEvent.ASSIGN, taskData: conferenceTaskData});
3909
+ service.send({type: TaskEvent.CONFERENCE_START, taskData: conferenceTaskData});
3910
+ service.send({type: TaskEvent.CONSULT, destination: 'agent-3', destinationType: 'agent'});
3911
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULT_INITIATING);
3912
+
3913
+ const consultEndEvent = {
3914
+ type: TaskEvent.CONSULT_END,
3915
+ taskData: createSecondaryAgentRemovedTaskData(),
3916
+ } as const;
3917
+ expect(
3918
+ guards.didCurrentAgentLeaveMainInteraction({
3919
+ context: service.getSnapshot().context,
3920
+ event: consultEndEvent,
3921
+ })
3922
+ ).toBe(true);
3923
+
3924
+ service.send(consultEndEvent);
3925
+
3926
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3927
+ });
3928
+
3929
+ it('terminates a consulting secondary initiator when CONSULT_END removes it from mainCall', () => {
3930
+ const service = startAgent2Machine();
3931
+ setupSecondaryAgentConsultingFromConference(service);
3932
+
3933
+ service.send({
3934
+ type: TaskEvent.CONSULT_END,
3935
+ taskData: createSecondaryAgentRemovedTaskData(),
3936
+ });
3937
+
3938
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
3939
+ expect(service.getSnapshot().context.consultInitiator).toBe(false);
3940
+ expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(false);
3941
+ });
3942
+
3943
+ it('does not infer departure from a partial CONSULT_END payload', () => {
3944
+ const service = startAgent2Machine();
3945
+ setupSecondaryAgentConsultingFromConference(service);
3946
+ const partialTaskData = {
3947
+ ...createTaskData({agentId: 'agent-2', interactionId: 'interaction-1'}),
3948
+ interaction: {
3949
+ state: 'conference',
3950
+ interactionId: 'interaction-1',
3951
+ mainInteractionId: 'interaction-1',
3952
+ },
3953
+ } as any;
3954
+
3955
+ const consultEndEvent = {type: TaskEvent.CONSULT_END, taskData: partialTaskData} as const;
3956
+ expect(service.getSnapshot().context.consultInitiator).toBe(true);
3957
+ expect(
3958
+ guards.didCurrentAgentLeaveMainInteraction({
3959
+ context: service.getSnapshot().context,
3960
+ event: consultEndEvent,
3961
+ })
3962
+ ).toBe(false);
3963
+
3964
+ service.send(consultEndEvent);
3965
+
3966
+ expect(service.getSnapshot().value).toBe(TaskState.CONNECTED);
3967
+ });
3968
+
3969
+ it('emits consult-end and task-end when AgentConsultEnded terminates the consulted agent', () => {
3970
+ const emitTaskConsultEnd = jest.fn();
3971
+ const emitTaskEnd = jest.fn();
3972
+ const cleanupResources = jest.fn();
3973
+ const service = createActor(
3974
+ createTaskStateMachine(
3975
+ {...createConfig(), agentId: 'agent-3'},
3976
+ {
3977
+ actions: {emitTaskConsultEnd, emitTaskEnd, cleanupResources},
3978
+ }
3979
+ )
3980
+ );
3981
+ service.start();
3982
+
3983
+ const consultingTaskData = createTaskData({
3984
+ agentId: 'agent-3',
3985
+ isConsulted: true,
3986
+ interactionId: 'consult-child',
3987
+ interaction: {
3988
+ state: 'consulting',
3989
+ owner: 'agent-1',
3990
+ interactionId: 'consult-child',
3991
+ mainInteractionId: 'interaction-1',
3992
+ participants: {
3993
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
3994
+ 'agent-2': {
3995
+ id: 'agent-2',
3996
+ pType: 'Agent',
3997
+ hasLeft: false,
3998
+ consultState: 'consulting',
3999
+ },
4000
+ 'agent-3': {
4001
+ id: 'agent-3',
4002
+ pType: 'Agent',
4003
+ hasLeft: false,
4004
+ isConsulted: true,
4005
+ consultState: 'consulting',
4006
+ },
4007
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
4008
+ },
4009
+ media: {
4010
+ 'interaction-1': {
4011
+ mediaResourceId: 'interaction-1',
4012
+ mType: 'mainCall',
4013
+ participants: ['agent-1', 'agent-2', 'customer-1'],
4014
+ isHold: true,
4015
+ },
4016
+ 'consult-child': {
4017
+ mediaResourceId: 'consult-child',
4018
+ mType: 'consult',
4019
+ participants: ['agent-2', 'agent-3'],
4020
+ isHold: false,
4021
+ },
4022
+ },
4023
+ } as any,
4024
+ });
4025
+ service.send({type: TaskEvent.HYDRATE, taskData: consultingTaskData});
4026
+ expect(service.getSnapshot().value).toBe(TaskState.CONSULTING);
4027
+
4028
+ const consultEndedTaskData = createTaskData({
4029
+ ...consultingTaskData,
4030
+ type: 'AgentConsultEnded' as any,
4031
+ interaction: {
4032
+ ...consultingTaskData.interaction,
4033
+ state: 'conference',
4034
+ participants: {
4035
+ 'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
4036
+ 'agent-2': {id: 'agent-2', pType: 'Agent', hasLeft: false},
4037
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
4038
+ },
4039
+ media: {
4040
+ 'interaction-1': {
4041
+ mediaResourceId: 'interaction-1',
4042
+ mType: 'mainCall',
4043
+ participants: ['agent-1', 'agent-2', 'customer-1'],
4044
+ isHold: false,
4045
+ },
4046
+ },
4047
+ } as any,
4048
+ });
4049
+
4050
+ service.send({type: TaskEvent.CONSULT_END, taskData: consultEndedTaskData});
4051
+
4052
+ expect(service.getSnapshot().value).toBe(TaskState.TERMINATED);
4053
+ expect(emitTaskConsultEnd).toHaveBeenCalledTimes(1);
4054
+ expect(emitTaskEnd).toHaveBeenCalledTimes(1);
4055
+ expect(cleanupResources).toHaveBeenCalledTimes(1);
4056
+ expect(service.getSnapshot().context.consultDestinationAgentJoined).toBe(false);
4057
+ expect(service.getSnapshot().context.consultInitiator).toBe(false);
4058
+ });
4059
+ });
4060
+ });