@webex/contact-center 3.11.0 → 3.12.0-auth-prejoin-fetch.1

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