@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,3101 @@
1
+ import {
2
+ CONSULT_TRANSFER_DESTINATION_TYPE,
3
+ TASK_CHANNEL_TYPE,
4
+ VOICE_VARIANT,
5
+ } from '../../../../../../src/services/task/types';
6
+ import {TaskState} from '../../../../../../src/services/task/state-machine/constants';
7
+ import {
8
+ computeUIControls,
9
+ getDefaultUIControls,
10
+ haveUIControlsChanged,
11
+ } from '../../../../../../src/services/task/state-machine/uiControlsComputer';
12
+ import {getTaskStateForUiControls} from '../../../../../../src/services/task/state-machine/actions';
13
+ import {TaskContext} from '../../../../../../src/services/task/state-machine/types';
14
+ import {createTaskData} from '../taskTestUtils';
15
+
16
+ function createConsultTaskData() {
17
+ return createTaskData({
18
+ agentId: 'agent-1',
19
+ mediaResourceId: 'interaction-1',
20
+ consultMediaResourceId: 'consult-media',
21
+ consultingAgentId: 'agent-1',
22
+ destAgentId: 'agent-2',
23
+ interaction: {
24
+ interactionId: 'interaction-1',
25
+ mainInteractionId: 'interaction-1',
26
+ participants: {
27
+ 'agent-1': {id: 'agent-1', pType: 'AGENT', hasLeft: false},
28
+ 'agent-2': {
29
+ id: 'agent-2',
30
+ pType: 'AGENT',
31
+ hasLeft: false,
32
+ consultState: 'consulting',
33
+ currentState: 'consulting',
34
+ isConsulted: true,
35
+ },
36
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
37
+ } as any,
38
+ media: {
39
+ 'interaction-1': {
40
+ mediaResourceId: 'interaction-1',
41
+ isHold: false,
42
+ participants: ['agent-1', 'customer-1'],
43
+ },
44
+ 'consult-media': {
45
+ mediaResourceId: 'consult-media',
46
+ isHold: false,
47
+ mType: 'consult',
48
+ participants: ['agent-2'],
49
+ },
50
+ } as any,
51
+ } as any,
52
+ });
53
+ }
54
+
55
+ function createConsultedAgentInconsistentTaskData() {
56
+ return createTaskData({
57
+ agentId: 'agent-2',
58
+ mediaResourceId: 'interaction-1',
59
+ consultMediaResourceId: 'consult-media',
60
+ consultingAgentId: 'agent-1',
61
+ isConsulted: false,
62
+ interaction: {
63
+ interactionId: 'interaction-1',
64
+ mainInteractionId: 'interaction-1',
65
+ participants: {
66
+ 'agent-1': {id: 'agent-1', pType: 'AGENT', hasLeft: false},
67
+ 'agent-2': {
68
+ id: 'agent-2',
69
+ pType: 'AGENT',
70
+ hasLeft: false,
71
+ consultState: 'consulting',
72
+ currentState: 'consulting',
73
+ isConsulted: true,
74
+ },
75
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
76
+ } as any,
77
+ media: {
78
+ 'interaction-1': {
79
+ mediaResourceId: 'interaction-1',
80
+ mType: 'mainCall',
81
+ isHold: false,
82
+ participants: ['agent-1', 'customer-1'],
83
+ },
84
+ 'consult-media': {
85
+ mediaResourceId: 'consult-media',
86
+ mType: 'consult',
87
+ isHold: false,
88
+ participants: ['agent-1', 'agent-2'],
89
+ },
90
+ } as any,
91
+ } as any,
92
+ });
93
+ }
94
+
95
+ function createPendingConsultHydrateTaskData() {
96
+ return createTaskData({
97
+ agentId: 'agent-1',
98
+ mediaResourceId: 'interaction-1',
99
+ consultMediaResourceId: 'consult-media',
100
+ isConsulted: false,
101
+ interaction: {
102
+ state: 'conference',
103
+ interactionId: 'interaction-1',
104
+ mainInteractionId: 'interaction-1',
105
+ participants: {
106
+ 'agent-1': {
107
+ id: 'agent-1',
108
+ pType: 'AGENT',
109
+ hasLeft: false,
110
+ consultState: 'consultInitiated',
111
+ isConsulted: false,
112
+ },
113
+ 'agent-2': {
114
+ id: 'agent-2',
115
+ pType: 'AGENT',
116
+ hasLeft: false,
117
+ hasJoined: false,
118
+ consultState: 'consultReserved',
119
+ isConsulted: true,
120
+ },
121
+ 'agent-3': {id: 'agent-3', pType: 'AGENT', hasLeft: false, consultState: 'conferencing'},
122
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
123
+ } as any,
124
+ media: {
125
+ 'interaction-1': {
126
+ mediaResourceId: 'interaction-1',
127
+ isHold: true,
128
+ mType: 'mainCall',
129
+ participants: ['agent-1', 'agent-3', 'customer-1'],
130
+ },
131
+ 'consult-media': {
132
+ mediaResourceId: 'consult-media',
133
+ isHold: false,
134
+ mType: 'consult',
135
+ participants: ['agent-1', 'agent-2'],
136
+ },
137
+ } as any,
138
+ callProcessingDetails: {
139
+ conferenceHoldParticipant: 'true',
140
+ },
141
+ } as any,
142
+ });
143
+ }
144
+
145
+ function createHeldConferenceWithActiveConsultTaskData() {
146
+ return createTaskData({
147
+ agentId: 'agent-2',
148
+ mediaResourceId: 'interaction-1',
149
+ consultMediaResourceId: 'consult-media',
150
+ consultingAgentId: 'agent-1',
151
+ isConsulted: false,
152
+ interaction: {
153
+ state: 'conference',
154
+ interactionId: 'interaction-1',
155
+ mainInteractionId: 'interaction-1',
156
+ participants: {
157
+ 'agent-1': {
158
+ id: 'agent-1',
159
+ pType: 'AGENT',
160
+ hasLeft: false,
161
+ consultState: 'consulting',
162
+ isConsulted: false,
163
+ },
164
+ 'agent-2': {
165
+ id: 'agent-2',
166
+ pType: 'AGENT',
167
+ hasLeft: false,
168
+ consultState: 'conferencing',
169
+ isConsulted: false,
170
+ },
171
+ 'agent-3': {
172
+ id: 'agent-3',
173
+ pType: 'AGENT',
174
+ hasLeft: false,
175
+ consultState: 'consultReserved',
176
+ isConsulted: true,
177
+ },
178
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
179
+ } as any,
180
+ media: {
181
+ 'interaction-1': {
182
+ mediaResourceId: 'interaction-1',
183
+ isHold: true,
184
+ mType: 'mainCall',
185
+ participants: ['agent-1', 'agent-2', 'customer-1'],
186
+ },
187
+ 'consult-media': {
188
+ mediaResourceId: 'consult-media',
189
+ isHold: false,
190
+ mType: 'consult',
191
+ participants: ['agent-1', 'agent-3'],
192
+ },
193
+ } as any,
194
+ } as any,
195
+ });
196
+ }
197
+
198
+ function createUnheldConferenceWithActiveConsultTaskData() {
199
+ return createTaskData({
200
+ agentId: 'agent-2',
201
+ mediaResourceId: 'interaction-1',
202
+ isConsulted: false,
203
+ interaction: {
204
+ state: 'conference',
205
+ type: 'AgentContactUnheld',
206
+ interactionId: 'interaction-1',
207
+ mainInteractionId: 'interaction-1',
208
+ owner: 'agent-5',
209
+ participants: {
210
+ 'agent-2': {
211
+ id: 'agent-2',
212
+ pType: 'AGENT',
213
+ hasLeft: false,
214
+ consultState: 'conferencing',
215
+ isConsulted: false,
216
+ },
217
+ 'agent-5': {
218
+ id: 'agent-5',
219
+ pType: 'AGENT',
220
+ hasLeft: false,
221
+ consultState: 'consulting',
222
+ isConsulted: false,
223
+ },
224
+ 'agent-15': {
225
+ id: 'agent-15',
226
+ pType: 'AGENT',
227
+ hasLeft: false,
228
+ consultState: 'consulting',
229
+ isConsulted: true,
230
+ },
231
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
232
+ } as any,
233
+ media: {
234
+ 'interaction-1': {
235
+ mediaResourceId: 'interaction-1',
236
+ isHold: false,
237
+ mType: 'mainCall',
238
+ participants: ['customer-1', 'agent-5', 'agent-2'],
239
+ },
240
+ 'consult-media': {
241
+ mediaResourceId: 'consult-media',
242
+ isHold: true,
243
+ mType: 'consult',
244
+ participants: ['agent-5', 'agent-15'],
245
+ },
246
+ } as any,
247
+ } as any,
248
+ });
249
+ }
250
+
251
+ function createConferenceWithOtherAgentConsultPendingTaskData() {
252
+ return createTaskData({
253
+ agentId: 'agent-2',
254
+ mediaResourceId: 'interaction-1',
255
+ consultMediaResourceId: 'consult-media',
256
+ isConsulted: false,
257
+ interaction: {
258
+ state: 'conference',
259
+ interactionId: 'interaction-1',
260
+ mainInteractionId: 'interaction-1',
261
+ participants: {
262
+ 'agent-1': {
263
+ id: 'agent-1',
264
+ pType: 'AGENT',
265
+ hasLeft: false,
266
+ consultState: 'consultInitiated',
267
+ isConsulted: false,
268
+ },
269
+ 'agent-2': {
270
+ id: 'agent-2',
271
+ pType: 'AGENT',
272
+ hasLeft: false,
273
+ consultState: 'conferencing',
274
+ isConsulted: false,
275
+ },
276
+ 'agent-3': {
277
+ id: 'agent-3',
278
+ pType: 'AGENT',
279
+ hasLeft: false,
280
+ consultState: 'consultReserved',
281
+ isConsulted: true,
282
+ },
283
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
284
+ } as any,
285
+ media: {
286
+ 'interaction-1': {
287
+ mediaResourceId: 'interaction-1',
288
+ isHold: false,
289
+ mType: 'mainCall',
290
+ participants: ['agent-1', 'agent-2', 'customer-1'],
291
+ },
292
+ 'consult-media': {
293
+ mediaResourceId: 'consult-media',
294
+ isHold: false,
295
+ mType: 'consult',
296
+ participants: ['agent-1', 'agent-3'],
297
+ },
298
+ } as any,
299
+ } as any,
300
+ });
301
+ }
302
+
303
+ function createPostConsultCompletedMultiAgentTaskData(agentId: string) {
304
+ return createTaskData({
305
+ agentId,
306
+ mediaResourceId: 'interaction-1',
307
+ consultMediaResourceId: null as any,
308
+ isConsulted: false,
309
+ interaction: {
310
+ state: 'conference',
311
+ interactionId: 'interaction-1',
312
+ mainInteractionId: 'interaction-1',
313
+ owner: 'agent-1',
314
+ participants: {
315
+ 'agent-1': {
316
+ id: 'agent-1',
317
+ pType: 'AGENT',
318
+ hasLeft: false,
319
+ consultState: 'consultCompleted',
320
+ isConsulted: false,
321
+ },
322
+ 'agent-2': {
323
+ id: 'agent-2',
324
+ pType: 'AGENT',
325
+ hasLeft: false,
326
+ consultState: 'conferencing',
327
+ isConsulted: false,
328
+ },
329
+ 'agent-3': {
330
+ id: 'agent-3',
331
+ pType: 'AGENT',
332
+ hasLeft: false,
333
+ consultState: 'conferencing',
334
+ isConsulted: false,
335
+ },
336
+ 'agent-4': {
337
+ id: 'agent-4',
338
+ pType: 'AGENT',
339
+ hasLeft: false,
340
+ consultState: 'conferencing',
341
+ isConsulted: false,
342
+ },
343
+ 'agent-5': {
344
+ id: 'agent-5',
345
+ pType: 'AGENT',
346
+ hasLeft: false,
347
+ consultState: 'conferencing',
348
+ isConsulted: false,
349
+ },
350
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
351
+ } as any,
352
+ media: {
353
+ 'interaction-1': {
354
+ mediaResourceId: 'interaction-1',
355
+ isHold: true,
356
+ mType: 'mainCall',
357
+ participants: ['agent-1', 'agent-2', 'agent-3', 'agent-4', 'agent-5', 'customer-1'],
358
+ },
359
+ } as any,
360
+ } as any,
361
+ });
362
+ }
363
+
364
+ function createConferenceConsultingInitiatorTaskData() {
365
+ return createTaskData({
366
+ agentId: 'agent-1',
367
+ mediaResourceId: 'interaction-1',
368
+ consultMediaResourceId: 'consult-media',
369
+ consultingAgentId: 'agent-1',
370
+ isConsulted: false,
371
+ interaction: {
372
+ state: 'conference',
373
+ interactionId: 'interaction-1',
374
+ mainInteractionId: 'interaction-1',
375
+ owner: 'agent-1',
376
+ participants: {
377
+ 'agent-1': {
378
+ id: 'agent-1',
379
+ pType: 'AGENT',
380
+ hasLeft: false,
381
+ consultState: 'consulting',
382
+ isConsulted: false,
383
+ },
384
+ 'agent-2': {
385
+ id: 'agent-2',
386
+ pType: 'AGENT',
387
+ hasLeft: false,
388
+ consultState: 'conferencing',
389
+ isConsulted: false,
390
+ },
391
+ 'agent-4': {
392
+ id: 'agent-4',
393
+ pType: 'AGENT',
394
+ hasLeft: false,
395
+ consultState: 'consultReserved',
396
+ isConsulted: true,
397
+ },
398
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
399
+ } as any,
400
+ media: {
401
+ 'interaction-1': {
402
+ mediaResourceId: 'interaction-1',
403
+ isHold: true,
404
+ mType: 'mainCall',
405
+ participants: ['agent-1', 'agent-2', 'customer-1'],
406
+ },
407
+ 'consult-media': {
408
+ mediaResourceId: 'consult-media',
409
+ isHold: false,
410
+ mType: 'consult',
411
+ participants: ['agent-1', 'agent-4'],
412
+ },
413
+ } as any,
414
+ } as any,
415
+ });
416
+ }
417
+
418
+ function createConferenceConsultInitiatedInitiatorTaskData() {
419
+ return createTaskData({
420
+ agentId: 'agent-1',
421
+ mediaResourceId: 'interaction-1',
422
+ consultMediaResourceId: 'consult-media',
423
+ destAgentId: 'agent-4',
424
+ destinationType: 'Agent',
425
+ isConsulted: false,
426
+ type: 'AgentConsultCreated' as any,
427
+ interaction: {
428
+ state: 'conference',
429
+ interactionId: 'interaction-1',
430
+ mainInteractionId: 'interaction-1',
431
+ owner: 'agent-5',
432
+ participants: {
433
+ 'agent-1': {
434
+ id: 'agent-1',
435
+ pType: 'AGENT',
436
+ hasLeft: false,
437
+ consultState: 'consultInitiated',
438
+ isConsulted: false,
439
+ },
440
+ 'agent-2': {
441
+ id: 'agent-2',
442
+ pType: 'AGENT',
443
+ hasLeft: false,
444
+ consultState: 'conferencing',
445
+ isConsulted: false,
446
+ },
447
+ 'agent-4': {
448
+ id: 'agent-4',
449
+ pType: 'AGENT',
450
+ hasLeft: false,
451
+ hasJoined: false,
452
+ consultState: null,
453
+ isConsulted: true,
454
+ },
455
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
456
+ } as any,
457
+ media: {
458
+ 'interaction-1': {
459
+ mediaResourceId: 'interaction-1',
460
+ isHold: true,
461
+ mType: 'mainCall',
462
+ participants: ['agent-1', 'agent-2', 'customer-1'],
463
+ },
464
+ 'consult-media': {
465
+ mediaResourceId: 'consult-media',
466
+ isHold: false,
467
+ mType: 'consult',
468
+ participants: ['agent-1', 'agent-4'],
469
+ },
470
+ } as any,
471
+ } as any,
472
+ });
473
+ }
474
+
475
+ function createAgentContactUnheldInitiatorConsultTaskData() {
476
+ return createTaskData({
477
+ agentId: 'agent-1',
478
+ mediaResourceId: 'interaction-1',
479
+ consultMediaResourceId: 'consult-media',
480
+ destAgentId: null as any,
481
+ isConsulted: false,
482
+ interaction: {
483
+ state: 'conference',
484
+ interactionId: 'interaction-1',
485
+ mainInteractionId: 'interaction-1',
486
+ owner: 'agent-5',
487
+ participants: {
488
+ 'agent-1': {
489
+ id: 'agent-1',
490
+ pType: 'AGENT',
491
+ hasLeft: false,
492
+ consultState: 'consulting',
493
+ isConsulted: false,
494
+ },
495
+ 'agent-15': {
496
+ id: 'agent-15',
497
+ pType: 'AGENT',
498
+ hasLeft: false,
499
+ consultState: 'consulting',
500
+ isConsulted: true,
501
+ },
502
+ 'agent-5': {
503
+ id: 'agent-5',
504
+ pType: 'AGENT',
505
+ hasLeft: false,
506
+ consultState: 'conferencing',
507
+ isConsulted: false,
508
+ },
509
+ 'agent-3': {
510
+ id: 'agent-3',
511
+ pType: 'AGENT',
512
+ hasLeft: false,
513
+ consultState: 'conferencing',
514
+ isConsulted: false,
515
+ },
516
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
517
+ } as any,
518
+ media: {
519
+ 'interaction-1': {
520
+ mediaResourceId: 'interaction-1',
521
+ isHold: true,
522
+ mType: 'mainCall',
523
+ participants: ['customer-1', 'agent-5', 'agent-3', 'agent-1'],
524
+ },
525
+ 'consult-media': {
526
+ mediaResourceId: 'consult-media',
527
+ isHold: false,
528
+ mType: 'consult',
529
+ participants: ['agent-15', 'agent-1'],
530
+ },
531
+ } as any,
532
+ } as any,
533
+ });
534
+ }
535
+
536
+ function createVoiceContext(overrides: Partial<TaskContext> = {}): TaskContext {
537
+ return {
538
+ taskData: createConsultTaskData(),
539
+ consultInitiator: true,
540
+ exitingConference: false,
541
+ consultFromConference: false,
542
+ transferConferenceRequested: false,
543
+ consultDestinationType: null,
544
+ consultDestinationAgentId: null,
545
+ consultDestinationAgentJoined: true,
546
+ consultCallHeld: false,
547
+ hideBlindTransferForEpDnPendingMerge: false,
548
+ recordingControlsAvailable: true,
549
+ recordingInProgress: true,
550
+ uiControlConfig: {
551
+ isEndTaskEnabled: true,
552
+ isEndConsultEnabled: true,
553
+ channelType: TASK_CHANNEL_TYPE.VOICE,
554
+ isRecordingEnabled: true,
555
+ agentId: 'agent-1',
556
+ },
557
+ uiControls: getDefaultUIControls(),
558
+ ...overrides,
559
+ };
560
+ }
561
+
562
+ function createConsultTransferContext(
563
+ taskData = createTaskData(),
564
+ overrides: Partial<NonNullable<TaskContext['uiControlConfig']['consultTransferConfig']>> = {}
565
+ ): TaskContext {
566
+ const context = createVoiceContext({taskData});
567
+
568
+ return {
569
+ ...context,
570
+ uiControlConfig: {
571
+ ...context.uiControlConfig,
572
+ consultTransferConfig: {
573
+ allowConsultToQueue: true,
574
+ accessQueue: 'ALL',
575
+ accessEntryPoint: 'ALL',
576
+ accessBuddyTeam: 'ALL',
577
+ ...overrides,
578
+ },
579
+ },
580
+ };
581
+ }
582
+
583
+ function createTaskData1LikeConferenceConsultTaskData() {
584
+ return createTaskData({
585
+ agentId: '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
586
+ mediaResourceId: '72402b8c-802d-4537-84d4-f244c3e586b1',
587
+ consultMediaResourceId: '66cc5edd-8ac9-4f27-8f48-286edea460b2',
588
+ consultingAgentId: '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
589
+ destAgentId: '747a2138-0a24-48fc-8d69-3a336d9b7158',
590
+ interaction: {
591
+ state: 'conference',
592
+ type: 'AgentConsulting',
593
+ interactionId: '72402b8c-802d-4537-84d4-f244c3e586b1',
594
+ mainInteractionId: '72402b8c-802d-4537-84d4-f244c3e586b1',
595
+ participants: {
596
+ '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9': {
597
+ id: '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
598
+ pType: 'Agent',
599
+ hasLeft: false,
600
+ consultState: 'consulting',
601
+ isConsulted: false,
602
+ },
603
+ '747a2138-0a24-48fc-8d69-3a336d9b7158': {
604
+ id: '747a2138-0a24-48fc-8d69-3a336d9b7158',
605
+ pType: 'Agent',
606
+ hasLeft: false,
607
+ consultState: 'consulting',
608
+ isConsulted: true,
609
+ },
610
+ 'e271075a-077d-42d0-9ae4-2a43cb847664': {
611
+ id: 'e271075a-077d-42d0-9ae4-2a43cb847664',
612
+ pType: 'Agent',
613
+ hasLeft: false,
614
+ consultState: 'conferencing',
615
+ isConsulted: false,
616
+ },
617
+ 'ed612aec-bafe-404d-b9b2-ea7de23a04f0': {
618
+ id: 'ed612aec-bafe-404d-b9b2-ea7de23a04f0',
619
+ pType: 'Agent',
620
+ hasLeft: false,
621
+ consultState: 'conferencing',
622
+ isConsulted: false,
623
+ },
624
+ '+14696762938': {id: '+14696762938', pType: 'Customer', hasLeft: false},
625
+ } as any,
626
+ media: {
627
+ '72402b8c-802d-4537-84d4-f244c3e586b1': {
628
+ mediaResourceId: '72402b8c-802d-4537-84d4-f244c3e586b1',
629
+ mType: 'mainCall',
630
+ isHold: true,
631
+ participants: [
632
+ '+14696762938',
633
+ 'e271075a-077d-42d0-9ae4-2a43cb847664',
634
+ 'ed612aec-bafe-404d-b9b2-ea7de23a04f0',
635
+ '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
636
+ ],
637
+ },
638
+ '66cc5edd-8ac9-4f27-8f48-286edea460b2': {
639
+ mediaResourceId: '66cc5edd-8ac9-4f27-8f48-286edea460b2',
640
+ mType: 'consult',
641
+ isHold: false,
642
+ participants: [
643
+ '747a2138-0a24-48fc-8d69-3a336d9b7158',
644
+ '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
645
+ ],
646
+ },
647
+ } as any,
648
+ owner: 'e271075a-077d-42d0-9ae4-2a43cb847664',
649
+ callProcessingDetails: {
650
+ conferenceHoldParticipant: 'true',
651
+ },
652
+ } as any,
653
+ });
654
+ }
655
+
656
+ describe('uiControlsComputer consult initiator controls', () => {
657
+ it('returns separate main and consult controls when consult leg is active', () => {
658
+ const context = createVoiceContext();
659
+
660
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, context.taskData);
661
+
662
+ expect(uiControls.activeLeg).toBe('consult');
663
+ expect(uiControls.consult).toBeDefined();
664
+
665
+ expect(uiControls.main.hold).toEqual({isVisible: false, isEnabled: false});
666
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: false});
667
+ expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: false});
668
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
669
+
670
+ expect(uiControls.consult.hold).toEqual({isVisible: false, isEnabled: false});
671
+ expect(uiControls.consult.mute).toEqual({isVisible: false, isEnabled: false});
672
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: true});
673
+ expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: true});
674
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
675
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: true});
676
+ });
677
+
678
+ it('switches top-level controls to main leg while keeping consult leg visible', () => {
679
+ const context = createVoiceContext({
680
+ consultCallHeld: true,
681
+ });
682
+
683
+ const uiControls = computeUIControls(TaskState.CONNECTED, context, context.taskData);
684
+
685
+ expect(uiControls.activeLeg).toBe('main');
686
+ expect(uiControls.consult).toBeDefined();
687
+
688
+ expect(uiControls.main.hold).toEqual({isVisible: false, isEnabled: false});
689
+ expect(uiControls.main.switch).toEqual({isVisible: true, isEnabled: true});
690
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
691
+ expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: true});
692
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
693
+
694
+ expect(uiControls.consult.hold).toEqual({isVisible: false, isEnabled: false});
695
+ expect(uiControls.consult.mute).toEqual({isVisible: false, isEnabled: false});
696
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false});
697
+ expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: false});
698
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
699
+ expect(uiControls.consult.end).toEqual({isVisible: false, isEnabled: false});
700
+ expect(uiControls.consult.switch).toEqual({isVisible: false, isEnabled: false});
701
+ });
702
+
703
+ it('enables mute only on active leg when main leg is active', () => {
704
+ const baseTaskData = createConsultTaskData();
705
+ const taskData = createTaskData({
706
+ ...baseTaskData,
707
+ interaction: {
708
+ ...baseTaskData.interaction,
709
+ media: {
710
+ ...baseTaskData.interaction.media,
711
+ 'consult-media': {
712
+ ...baseTaskData.interaction.media['consult-media'],
713
+ participants: ['agent-1', 'agent-2'],
714
+ },
715
+ },
716
+ },
717
+ });
718
+ const baseContext = createVoiceContext();
719
+ const context = createVoiceContext({
720
+ taskData,
721
+ consultCallHeld: true,
722
+ uiControlConfig: {
723
+ ...baseContext.uiControlConfig,
724
+ voiceVariant: VOICE_VARIANT.WEBRTC,
725
+ },
726
+ });
727
+
728
+ const uiControls = computeUIControls(TaskState.CONNECTED, context, context.taskData);
729
+
730
+ expect(uiControls.activeLeg).toBe('main');
731
+ expect(uiControls.main.mute).toEqual({isVisible: true, isEnabled: true});
732
+ expect(uiControls.consult.mute).toEqual({isVisible: true, isEnabled: false});
733
+ });
734
+
735
+ it('enables mute only on active leg when consult leg is active', () => {
736
+ const baseTaskData = createConsultTaskData();
737
+ const taskData = createTaskData({
738
+ ...baseTaskData,
739
+ interaction: {
740
+ ...baseTaskData.interaction,
741
+ media: {
742
+ ...baseTaskData.interaction.media,
743
+ 'consult-media': {
744
+ ...baseTaskData.interaction.media['consult-media'],
745
+ participants: ['agent-1', 'agent-2'],
746
+ },
747
+ },
748
+ },
749
+ });
750
+ const baseContext = createVoiceContext();
751
+ const context = createVoiceContext({
752
+ taskData,
753
+ consultCallHeld: false,
754
+ uiControlConfig: {
755
+ ...baseContext.uiControlConfig,
756
+ voiceVariant: VOICE_VARIANT.WEBRTC,
757
+ },
758
+ });
759
+
760
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, context.taskData);
761
+
762
+ expect(uiControls.activeLeg).toBe('consult');
763
+ expect(uiControls.main.mute).toEqual({isVisible: true, isEnabled: false});
764
+ expect(uiControls.consult.mute).toEqual({isVisible: true, isEnabled: true});
765
+ });
766
+
767
+ it('hides transfer for the consulted agent during consult', () => {
768
+ const consultedTaskData = createConsultTaskData();
769
+ const consultedContext = createVoiceContext({
770
+ consultInitiator: false,
771
+ taskData: {
772
+ ...consultedTaskData,
773
+ isConsulted: true,
774
+ agentId: 'agent-2',
775
+ } as any,
776
+ uiControlConfig: {
777
+ ...createVoiceContext().uiControlConfig,
778
+ agentId: 'agent-2',
779
+ isEndConsultEnabled: true,
780
+ },
781
+ });
782
+
783
+ const uiControls = computeUIControls(
784
+ TaskState.CONSULTING,
785
+ consultedContext,
786
+ consultedContext.taskData
787
+ );
788
+
789
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
790
+ expect(uiControls.consult.conference).toEqual({isVisible: false, isEnabled: false});
791
+ expect(uiControls.consult.mergeToConference).toEqual({isVisible: false, isEnabled: false});
792
+ expect(uiControls.consult.switch).toEqual({isVisible: false, isEnabled: false});
793
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
794
+ });
795
+
796
+ it('hides main-leg controls for consulted agent when payload isConsulted is stale false', () => {
797
+ const taskData = createConsultedAgentInconsistentTaskData();
798
+ const baseContext = createVoiceContext();
799
+ const consultedContext = createVoiceContext({
800
+ consultInitiator: false,
801
+ taskData,
802
+ uiControlConfig: {
803
+ ...baseContext.uiControlConfig,
804
+ agentId: 'agent-2',
805
+ },
806
+ });
807
+
808
+ const uiControls = computeUIControls(TaskState.CONNECTED, consultedContext, taskData);
809
+
810
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
811
+ expect(uiControls.main.end).toEqual({isVisible: false, isEnabled: false});
812
+ });
813
+
814
+ it('keeps consult-ring controls disabled except endConsult during hydrate pending state', () => {
815
+ const pendingTaskData = createPendingConsultHydrateTaskData();
816
+ const context = createVoiceContext({
817
+ taskData: pendingTaskData as any,
818
+ consultInitiator: false,
819
+ consultFromConference: false,
820
+ consultDestinationAgentJoined: false,
821
+ consultCallHeld: false,
822
+ });
823
+
824
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, pendingTaskData as any);
825
+
826
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: false});
827
+ expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: false});
828
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
829
+
830
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false});
831
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: false});
832
+ expect(uiControls.consult.mergeToConference).toEqual({isVisible: true, isEnabled: false});
833
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
834
+ });
835
+
836
+ it('collapses stale consult leg controls during wrapup', () => {
837
+ const context = createVoiceContext();
838
+
839
+ const uiControls = computeUIControls(TaskState.WRAPPING_UP, context, context.taskData);
840
+
841
+ expect(uiControls.activeLeg).toBe('main');
842
+ expect(uiControls.main.wrapup).toEqual({isVisible: true, isEnabled: true});
843
+ expect(uiControls.consult).toEqual(getDefaultUIControls().consult);
844
+ });
845
+
846
+ it('disables consult for non-initiators when another agent has active consult', () => {
847
+ const taskData = createConferenceWithOtherAgentConsultPendingTaskData();
848
+ const baseContext = createVoiceContext();
849
+ const context = createVoiceContext({
850
+ consultInitiator: false,
851
+ consultFromConference: false,
852
+ taskData,
853
+ uiControlConfig: {
854
+ ...baseContext.uiControlConfig,
855
+ agentId: 'agent-2',
856
+ },
857
+ });
858
+
859
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
860
+
861
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
862
+ });
863
+
864
+ it('enables end and exitConference for non-initiators on held main leg while another agent consults', () => {
865
+ const taskData = createHeldConferenceWithActiveConsultTaskData();
866
+ const baseContext = createVoiceContext();
867
+ const context = createVoiceContext({
868
+ consultInitiator: false,
869
+ taskData,
870
+ uiControlConfig: {
871
+ ...baseContext.uiControlConfig,
872
+ agentId: 'agent-2',
873
+ },
874
+ });
875
+
876
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
877
+
878
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
879
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
880
+ });
881
+
882
+ it('enables end and exitConference on held main leg when consultInitiator flag is stale true', () => {
883
+ const taskData = createHeldConferenceWithActiveConsultTaskData();
884
+ const baseContext = createVoiceContext();
885
+ const context = createVoiceContext({
886
+ consultInitiator: true,
887
+ taskData,
888
+ uiControlConfig: {
889
+ ...baseContext.uiControlConfig,
890
+ agentId: 'agent-2',
891
+ },
892
+ });
893
+
894
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
895
+
896
+ expect(uiControls.activeLeg).toBe('main');
897
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
898
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
899
+ });
900
+
901
+ it('keeps main leg active for conference owner when another agent consults (CAI-8329)', () => {
902
+ const taskData = createTaskData({
903
+ agentId: 'agent-3',
904
+ type: 'AgentConsultCreated' as any,
905
+ consultMediaResourceId: 'consult-media-1',
906
+ consultingAgentId: 'agent-3',
907
+ isConsulted: false,
908
+ interaction: {
909
+ state: 'conference',
910
+ interactionId: 'interaction-1',
911
+ mainInteractionId: 'interaction-1',
912
+ owner: 'agent-1',
913
+ callProcessingDetails: {
914
+ conferenceHoldParticipant: 'true',
915
+ recordInProgress: 'true',
916
+ recordingStarted: 'true',
917
+ },
918
+ participants: {
919
+ 'agent-1': {
920
+ id: 'agent-1',
921
+ pType: 'Agent',
922
+ hasJoined: true,
923
+ hasLeft: false,
924
+ consultState: 'conferencing',
925
+ },
926
+ 'agent-2': {
927
+ id: 'agent-2',
928
+ pType: 'Agent',
929
+ hasJoined: false,
930
+ hasLeft: false,
931
+ isConsulted: true,
932
+ },
933
+ 'agent-3': {
934
+ id: 'agent-3',
935
+ pType: 'Agent',
936
+ hasJoined: true,
937
+ hasLeft: false,
938
+ consultState: 'consultInitiated',
939
+ },
940
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: false},
941
+ } as any,
942
+ media: {
943
+ 'interaction-1': {
944
+ mediaResourceId: 'interaction-1',
945
+ mType: 'mainCall',
946
+ isHold: true,
947
+ participants: ['customer-1', 'agent-1', 'agent-3'],
948
+ },
949
+ 'consult-media-1': {
950
+ mediaResourceId: 'consult-media-1',
951
+ mType: 'consult',
952
+ isHold: false,
953
+ participants: ['agent-2', 'agent-3'],
954
+ },
955
+ } as any,
956
+ } as any,
957
+ });
958
+ const baseContext = createVoiceContext();
959
+ const context = createVoiceContext({
960
+ consultInitiator: true,
961
+ consultFromConference: true,
962
+ consultDestinationAgentJoined: true,
963
+ recordingControlsAvailable: true,
964
+ recordingInProgress: true,
965
+ taskData,
966
+ uiControlConfig: {
967
+ ...baseContext.uiControlConfig,
968
+ agentId: 'agent-1',
969
+ },
970
+ });
971
+
972
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
973
+
974
+ expect(uiControls.activeLeg).toBe('main');
975
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: false});
976
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
977
+ expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: true});
978
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
979
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
980
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
981
+ expect(uiControls.main.transferConference).toEqual({isVisible: false, isEnabled: false});
982
+ });
983
+
984
+ it('detects other-agent consult in progress with mixed-case Agent pType (CAI-8329)', () => {
985
+ const taskData = createConferenceWithOtherAgentConsultPendingTaskData();
986
+ const mixedCaseTaskData = createTaskData({
987
+ ...taskData,
988
+ interaction: {
989
+ ...taskData.interaction,
990
+ participants: {
991
+ ...taskData.interaction?.participants,
992
+ 'agent-1': {
993
+ ...(taskData.interaction?.participants as any)['agent-1'],
994
+ pType: 'Agent',
995
+ },
996
+ },
997
+ } as any,
998
+ });
999
+ const baseContext = createVoiceContext();
1000
+ const context = createVoiceContext({
1001
+ consultInitiator: false,
1002
+ taskData: mixedCaseTaskData,
1003
+ uiControlConfig: {
1004
+ ...baseContext.uiControlConfig,
1005
+ agentId: 'agent-2',
1006
+ },
1007
+ });
1008
+
1009
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, mixedCaseTaskData);
1010
+
1011
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
1012
+ });
1013
+
1014
+ it('enables end and exitConference on unheld main leg for non-initiator during active conference consult', () => {
1015
+ const taskData = createUnheldConferenceWithActiveConsultTaskData();
1016
+ const baseContext = createVoiceContext();
1017
+ const context = createVoiceContext({
1018
+ consultInitiator: false,
1019
+ taskData,
1020
+ uiControlConfig: {
1021
+ ...baseContext.uiControlConfig,
1022
+ agentId: 'agent-2',
1023
+ },
1024
+ });
1025
+
1026
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
1027
+
1028
+ expect(uiControls.activeLeg).toBe('main');
1029
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
1030
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
1031
+ });
1032
+
1033
+ it('keeps consult disabled for non-owner after owner consult completes in multi-agent conference', () => {
1034
+ const taskData = createPostConsultCompletedMultiAgentTaskData('agent-2');
1035
+ const baseContext = createVoiceContext();
1036
+ const context = createVoiceContext({
1037
+ consultInitiator: false,
1038
+ taskData,
1039
+ uiControlConfig: {
1040
+ ...baseContext.uiControlConfig,
1041
+ agentId: 'agent-2',
1042
+ },
1043
+ });
1044
+
1045
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
1046
+
1047
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
1048
+ });
1049
+
1050
+ it('keeps consult enabled for owner after consult completes in multi-agent conference', () => {
1051
+ const taskData = createPostConsultCompletedMultiAgentTaskData('agent-1');
1052
+ const baseContext = createVoiceContext();
1053
+ const context = createVoiceContext({
1054
+ consultInitiator: false,
1055
+ taskData,
1056
+ uiControlConfig: {
1057
+ ...baseContext.uiControlConfig,
1058
+ agentId: 'agent-1',
1059
+ },
1060
+ });
1061
+
1062
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
1063
+
1064
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1065
+ });
1066
+
1067
+ it('hides transfer and enables transferConference on consult leg for conference initiator', () => {
1068
+ const taskData = createConferenceConsultingInitiatorTaskData();
1069
+ const baseContext = createVoiceContext();
1070
+ const context = createVoiceContext({
1071
+ consultInitiator: true,
1072
+ consultFromConference: true,
1073
+ consultDestinationAgentJoined: true,
1074
+ consultCallHeld: false,
1075
+ taskData,
1076
+ uiControlConfig: {
1077
+ ...baseContext.uiControlConfig,
1078
+ agentId: 'agent-1',
1079
+ },
1080
+ });
1081
+
1082
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
1083
+
1084
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
1085
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
1086
+ expect(uiControls.main.transferConference).toEqual({isVisible: true, isEnabled: false});
1087
+ });
1088
+
1089
+ it('keeps consult leg controls active and main leg conference controls disabled on hydrate-like context', () => {
1090
+ const taskData = createConferenceConsultingInitiatorTaskData();
1091
+ const baseContext = createVoiceContext();
1092
+ const context = createVoiceContext({
1093
+ consultInitiator: true,
1094
+ consultFromConference: true,
1095
+ consultDestinationAgentJoined: true,
1096
+ consultCallHeld: false,
1097
+ taskData,
1098
+ uiControlConfig: {
1099
+ ...baseContext.uiControlConfig,
1100
+ agentId: 'agent-1',
1101
+ },
1102
+ });
1103
+
1104
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
1105
+
1106
+ expect(uiControls.activeLeg).toBe('consult');
1107
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
1108
+ expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: false});
1109
+ expect(uiControls.main.transferConference).toEqual({isVisible: true, isEnabled: false});
1110
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: true});
1111
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
1112
+ expect(uiControls.consult.mergeToConference).toEqual({isVisible: true, isEnabled: true});
1113
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
1114
+ });
1115
+
1116
+ it('enables consult leg controls for conference DN AgentConsulting after destination joined', () => {
1117
+ const taskData = createTaskData({
1118
+ agentId: 'agent-1',
1119
+ consultingAgentId: 'agent-1',
1120
+ consultMediaResourceId: 'consult-media-1',
1121
+ isConsulted: false,
1122
+ destinationType: 'DN',
1123
+ type: 'AgentConsulting' as any,
1124
+ interaction: {
1125
+ state: 'conference',
1126
+ interactionId: 'interaction-1',
1127
+ mainInteractionId: 'interaction-1',
1128
+ owner: 'agent-1',
1129
+ participants: {
1130
+ 'agent-1': {
1131
+ id: 'agent-1',
1132
+ pType: 'Agent',
1133
+ hasLeft: false,
1134
+ consultState: 'consulting',
1135
+ isConsulted: false,
1136
+ },
1137
+ 'agent-2': {
1138
+ id: 'agent-2',
1139
+ pType: 'Agent',
1140
+ hasLeft: false,
1141
+ consultState: 'conferencing',
1142
+ },
1143
+ 'dn-dest': {
1144
+ id: 'dn-dest',
1145
+ pType: 'DN',
1146
+ hasLeft: false,
1147
+ hasJoined: true,
1148
+ },
1149
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
1150
+ } as any,
1151
+ media: {
1152
+ 'interaction-1': {
1153
+ mediaResourceId: 'interaction-1',
1154
+ mType: 'mainCall',
1155
+ participants: ['agent-1', 'agent-2', 'customer-1'],
1156
+ isHold: false,
1157
+ },
1158
+ 'consult-media-1': {
1159
+ mediaResourceId: 'consult-media-1',
1160
+ mType: 'consult',
1161
+ participants: ['agent-1', 'dn-dest'],
1162
+ isHold: false,
1163
+ },
1164
+ } as any,
1165
+ } as any,
1166
+ });
1167
+ const baseContext = createVoiceContext();
1168
+ const context = createVoiceContext({
1169
+ consultInitiator: true,
1170
+ consultFromConference: true,
1171
+ consultDestinationAgentJoined: true,
1172
+ consultDestinationType: 'entryPoint',
1173
+ consultCallHeld: false,
1174
+ taskData,
1175
+ uiControlConfig: {
1176
+ ...baseContext.uiControlConfig,
1177
+ agentId: 'agent-1',
1178
+ },
1179
+ });
1180
+
1181
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
1182
+
1183
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: true});
1184
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
1185
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
1186
+ expect(uiControls.consult.mergeToConference).toEqual({isVisible: true, isEnabled: true});
1187
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
1188
+ });
1189
+
1190
+ it('keeps transferConference visible on consult leg for initiator even when state is conferencing', () => {
1191
+ const taskData = createConferenceConsultingInitiatorTaskData();
1192
+ const baseContext = createVoiceContext();
1193
+ const context = createVoiceContext({
1194
+ consultInitiator: true,
1195
+ consultFromConference: true,
1196
+ consultDestinationAgentJoined: true,
1197
+ consultCallHeld: false,
1198
+ taskData,
1199
+ uiControlConfig: {
1200
+ ...baseContext.uiControlConfig,
1201
+ agentId: 'agent-1',
1202
+ },
1203
+ });
1204
+
1205
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
1206
+
1207
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
1208
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
1209
+ });
1210
+
1211
+ it('shows transferConference for initiator in AgentContactUnheld-style conference consult context', () => {
1212
+ const taskData = createAgentContactUnheldInitiatorConsultTaskData();
1213
+ const baseContext = createVoiceContext();
1214
+ const context = createVoiceContext({
1215
+ consultInitiator: true,
1216
+ consultFromConference: false,
1217
+ consultDestinationAgentJoined: true,
1218
+ consultCallHeld: false,
1219
+ taskData,
1220
+ uiControlConfig: {
1221
+ ...baseContext.uiControlConfig,
1222
+ agentId: 'agent-1',
1223
+ },
1224
+ });
1225
+
1226
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
1227
+
1228
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
1229
+ });
1230
+
1231
+ it('shows transferConference on main leg when initiator has active conference consult', () => {
1232
+ const taskData = createConferenceConsultingInitiatorTaskData();
1233
+ const baseContext = createVoiceContext();
1234
+ const context = createVoiceContext({
1235
+ consultInitiator: true,
1236
+ consultFromConference: true,
1237
+ consultDestinationAgentJoined: true,
1238
+ consultCallHeld: true,
1239
+ taskData,
1240
+ uiControlConfig: {
1241
+ ...baseContext.uiControlConfig,
1242
+ agentId: 'agent-1',
1243
+ },
1244
+ });
1245
+
1246
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
1247
+
1248
+ expect(uiControls.activeLeg).toBe('main');
1249
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
1250
+ expect(uiControls.main.transferConference).toEqual({isVisible: true, isEnabled: true});
1251
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: false});
1252
+ });
1253
+
1254
+ it('keeps transferConference visible on consult leg for taskData1-style payload', () => {
1255
+ const taskData = createTaskData1LikeConferenceConsultTaskData();
1256
+ const baseContext = createVoiceContext();
1257
+ const context = createVoiceContext({
1258
+ consultInitiator: true,
1259
+ consultFromConference: true,
1260
+ consultDestinationAgentJoined: true,
1261
+ consultCallHeld: false,
1262
+ taskData,
1263
+ uiControlConfig: {
1264
+ ...baseContext.uiControlConfig,
1265
+ agentId: '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
1266
+ },
1267
+ });
1268
+
1269
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
1270
+
1271
+ expect(uiControls.activeLeg).toBe('consult');
1272
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
1273
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
1274
+ });
1275
+
1276
+ it('enables transferConference after AgentConsulting follows AgentConsultCreated for initiator', () => {
1277
+ const baseContext = createVoiceContext();
1278
+ const createdTaskData = createConferenceConsultInitiatedInitiatorTaskData();
1279
+ const createdContext = createVoiceContext({
1280
+ consultInitiator: true,
1281
+ consultFromConference: true,
1282
+ consultDestinationAgentJoined: false,
1283
+ consultCallHeld: false,
1284
+ taskData: createdTaskData,
1285
+ uiControlConfig: {
1286
+ ...baseContext.uiControlConfig,
1287
+ agentId: 'agent-1',
1288
+ },
1289
+ });
1290
+ const createdControls = computeUIControls(
1291
+ TaskState.CONSULTING,
1292
+ createdContext,
1293
+ createdTaskData
1294
+ );
1295
+
1296
+ expect(createdControls.consult.transferConference).toEqual({isVisible: true, isEnabled: false});
1297
+
1298
+ const consultingTaskData = createConferenceConsultingInitiatorTaskData();
1299
+ const consultingContext = createVoiceContext({
1300
+ ...createdContext,
1301
+ taskData: consultingTaskData,
1302
+ consultDestinationAgentJoined: true,
1303
+ });
1304
+ const consultingControls = computeUIControls(
1305
+ TaskState.CONSULTING,
1306
+ consultingContext,
1307
+ consultingTaskData
1308
+ );
1309
+
1310
+ expect(consultingControls.consult.transferConference).toEqual({
1311
+ isVisible: true,
1312
+ isEnabled: true,
1313
+ });
1314
+ });
1315
+
1316
+ it('hides exitConference on main leg for consult initiator before destination joins', () => {
1317
+ const taskData = createConferenceConsultInitiatedInitiatorTaskData();
1318
+ const baseContext = createVoiceContext();
1319
+ const context = createVoiceContext({
1320
+ consultInitiator: true,
1321
+ consultFromConference: true,
1322
+ consultDestinationAgentJoined: false,
1323
+ consultCallHeld: false,
1324
+ taskData,
1325
+ uiControlConfig: {
1326
+ ...baseContext.uiControlConfig,
1327
+ agentId: 'agent-1',
1328
+ },
1329
+ });
1330
+
1331
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
1332
+
1333
+ expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
1334
+ });
1335
+
1336
+ it('hides exitConference on main leg while consulting and destination has not joined', () => {
1337
+ const taskData = createConferenceConsultInitiatedInitiatorTaskData();
1338
+ const baseContext = createVoiceContext();
1339
+ const context = createVoiceContext({
1340
+ consultInitiator: true,
1341
+ consultFromConference: true,
1342
+ consultDestinationAgentJoined: false,
1343
+ consultCallHeld: false,
1344
+ taskData,
1345
+ uiControlConfig: {
1346
+ ...baseContext.uiControlConfig,
1347
+ agentId: 'agent-1',
1348
+ },
1349
+ });
1350
+
1351
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
1352
+
1353
+ expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
1354
+ });
1355
+
1356
+ it('hides exitConference on main leg for pending self consult even with stale initiator flags', () => {
1357
+ const taskData = createConferenceConsultInitiatedInitiatorTaskData();
1358
+ const baseContext = createVoiceContext();
1359
+ const context = createVoiceContext({
1360
+ consultInitiator: false,
1361
+ consultFromConference: false,
1362
+ consultDestinationAgentJoined: false,
1363
+ consultCallHeld: false,
1364
+ taskData,
1365
+ uiControlConfig: {
1366
+ ...baseContext.uiControlConfig,
1367
+ agentId: 'agent-1',
1368
+ },
1369
+ });
1370
+
1371
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
1372
+
1373
+ expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
1374
+ });
1375
+
1376
+ function createSimpleHeldConsultInitiatedTaskData() {
1377
+ return createTaskData({
1378
+ agentId: 'agent-1',
1379
+ mediaResourceId: 'interaction-1',
1380
+ consultMediaResourceId: 'consult-media',
1381
+ destAgentId: 'agent-2',
1382
+ destinationType: 'Agent',
1383
+ isConsulted: false,
1384
+ type: 'AgentConsultCreated' as any,
1385
+ interaction: {
1386
+ state: 'consult',
1387
+ interactionId: 'interaction-1',
1388
+ mainInteractionId: 'interaction-1',
1389
+ owner: 'agent-1',
1390
+ participants: {
1391
+ 'agent-1': {
1392
+ id: 'agent-1',
1393
+ pType: 'Agent',
1394
+ hasLeft: false,
1395
+ consultState: 'consultInitiated',
1396
+ isConsulted: false,
1397
+ },
1398
+ 'agent-2': {
1399
+ id: 'agent-2',
1400
+ pType: 'Agent',
1401
+ hasLeft: false,
1402
+ hasJoined: false,
1403
+ consultState: 'consultReserved',
1404
+ isConsulted: true,
1405
+ },
1406
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1407
+ } as any,
1408
+ media: {
1409
+ 'interaction-1': {
1410
+ mediaResourceId: 'interaction-1',
1411
+ mType: 'mainCall',
1412
+ isHold: true,
1413
+ participants: ['customer-1', 'agent-1'],
1414
+ },
1415
+ 'consult-media': {
1416
+ mediaResourceId: 'consult-media',
1417
+ mType: 'consult',
1418
+ isHold: false,
1419
+ participants: ['agent-2', 'agent-1'],
1420
+ },
1421
+ } as any,
1422
+ } as any,
1423
+ });
1424
+ }
1425
+
1426
+ function createConsultFailedHeldMainTaskData() {
1427
+ return createTaskData({
1428
+ agentId: 'agent-1',
1429
+ mediaResourceId: 'interaction-1',
1430
+ consultMediaResourceId: 'consult-media',
1431
+ destAgentId: 'agent-2',
1432
+ destinationType: 'Agent',
1433
+ isConsulted: false,
1434
+ type: 'AgentConsultFailed' as any,
1435
+ interaction: {
1436
+ state: 'consult',
1437
+ interactionId: 'interaction-1',
1438
+ mainInteractionId: 'interaction-1',
1439
+ owner: 'agent-1',
1440
+ participants: {
1441
+ 'agent-1': {
1442
+ id: 'agent-1',
1443
+ pType: 'Agent',
1444
+ hasLeft: false,
1445
+ consultState: 'consultCompleted',
1446
+ isConsulted: false,
1447
+ },
1448
+ 'agent-2': {
1449
+ id: 'agent-2',
1450
+ pType: 'Agent',
1451
+ hasLeft: false,
1452
+ hasJoined: false,
1453
+ consultState: 'consultReserved',
1454
+ isConsulted: true,
1455
+ },
1456
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1457
+ } as any,
1458
+ media: {
1459
+ 'interaction-1': {
1460
+ mediaResourceId: 'interaction-1',
1461
+ mType: 'mainCall',
1462
+ isHold: true,
1463
+ participants: ['customer-1', 'agent-1'],
1464
+ },
1465
+ 'consult-media': {
1466
+ mediaResourceId: 'consult-media',
1467
+ mType: 'consult',
1468
+ isHold: false,
1469
+ participants: ['agent-2', 'agent-1'],
1470
+ },
1471
+ } as any,
1472
+ } as any,
1473
+ });
1474
+ }
1475
+
1476
+ it('infers HELD (not CONSULTING) while consultee has not joined', () => {
1477
+ const taskData = createSimpleHeldConsultInitiatedTaskData();
1478
+
1479
+ expect(getTaskStateForUiControls(taskData as any, 'agent-1')).toBe(TaskState.HELD);
1480
+ });
1481
+
1482
+ it('matches Stable Prod consult-requested controls for AgentConsultCreated while HELD', () => {
1483
+ const taskData = createSimpleHeldConsultInitiatedTaskData();
1484
+ const context = createVoiceContext({
1485
+ taskData: taskData as any,
1486
+ consultInitiator: true,
1487
+ consultDestinationAgentJoined: false,
1488
+ consultCallHeld: false,
1489
+ });
1490
+
1491
+ const uiControls = computeUIControls(TaskState.HELD, context, taskData as any);
1492
+
1493
+ expect(uiControls.activeLeg).toBe('consult');
1494
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: false});
1495
+ expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: false});
1496
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
1497
+ expect(uiControls.main.hold).toEqual({isVisible: false, isEnabled: false});
1498
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
1499
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: false});
1500
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false});
1501
+ expect(uiControls.consult.mergeToConference).toEqual({isVisible: true, isEnabled: false});
1502
+ });
1503
+
1504
+ it('clears consult leg and restores HELD main controls after AgentConsultEnded from Stable Prod', () => {
1505
+ const taskData = createTaskData({
1506
+ agentId: 'agent-1',
1507
+ mediaResourceId: 'interaction-1',
1508
+ destAgentId: 'agent-2',
1509
+ destinationType: 'Agent',
1510
+ isConsulted: false,
1511
+ type: 'AgentConsultEnded' as any,
1512
+ interaction: {
1513
+ state: 'connected',
1514
+ interactionId: 'interaction-1',
1515
+ mainInteractionId: 'interaction-1',
1516
+ owner: 'agent-1',
1517
+ participants: {
1518
+ 'agent-1': {
1519
+ id: 'agent-1',
1520
+ pType: 'Agent',
1521
+ hasLeft: false,
1522
+ consultState: 'consultCompleted',
1523
+ isConsulted: false,
1524
+ },
1525
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1526
+ } as any,
1527
+ media: {
1528
+ 'interaction-1': {
1529
+ mediaResourceId: 'interaction-1',
1530
+ mType: 'mainCall',
1531
+ isHold: true,
1532
+ holdTimestamp: 1780495564872,
1533
+ participants: ['customer-1', 'agent-1'],
1534
+ },
1535
+ } as any,
1536
+ } as any,
1537
+ });
1538
+ const staleContext = createVoiceContext({
1539
+ taskData: {
1540
+ ...(taskData as any),
1541
+ interaction: {
1542
+ ...(taskData as any).interaction,
1543
+ media: {
1544
+ ...(taskData as any).interaction.media,
1545
+ 'consult-media': {
1546
+ mediaResourceId: 'consult-media',
1547
+ mType: 'consult',
1548
+ isHold: false,
1549
+ participants: ['agent-2', 'agent-1'],
1550
+ },
1551
+ },
1552
+ },
1553
+ },
1554
+ consultInitiator: true,
1555
+ consultDestinationAgentJoined: true,
1556
+ consultCallHeld: false,
1557
+ });
1558
+
1559
+ const uiControls = computeUIControls(TaskState.CONSULTING, staleContext, taskData as any);
1560
+
1561
+ expect(uiControls.activeLeg).toBe('main');
1562
+ expect(uiControls.consult.endConsult).toEqual({isVisible: false, isEnabled: false});
1563
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: true});
1564
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1565
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
1566
+ expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: true});
1567
+ });
1568
+
1569
+ it('enables switch, transfer, and merge on consult leg after AgentConsulting accept', () => {
1570
+ const taskData = createTaskData({
1571
+ agentId: 'agent-1',
1572
+ mediaResourceId: 'interaction-1',
1573
+ consultMediaResourceId: 'consult-media',
1574
+ consultingAgentId: 'agent-1',
1575
+ destAgentId: 'agent-2',
1576
+ isConsulted: false,
1577
+ type: 'AgentConsulting' as any,
1578
+ interaction: {
1579
+ state: 'consulting',
1580
+ interactionId: 'interaction-1',
1581
+ mainInteractionId: 'interaction-1',
1582
+ owner: 'agent-1',
1583
+ participants: {
1584
+ 'agent-1': {
1585
+ id: 'agent-1',
1586
+ pType: 'Agent',
1587
+ hasLeft: false,
1588
+ consultState: 'consulting',
1589
+ isConsulted: false,
1590
+ },
1591
+ 'agent-2': {
1592
+ id: 'agent-2',
1593
+ pType: 'Agent',
1594
+ hasLeft: false,
1595
+ hasJoined: true,
1596
+ consultState: 'consulting',
1597
+ isConsulted: true,
1598
+ },
1599
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1600
+ } as any,
1601
+ media: {
1602
+ 'interaction-1': {
1603
+ mediaResourceId: 'interaction-1',
1604
+ mType: 'mainCall',
1605
+ isHold: true,
1606
+ participants: ['customer-1', 'agent-1'],
1607
+ },
1608
+ 'consult-media': {
1609
+ mediaResourceId: 'consult-media',
1610
+ mType: 'consult',
1611
+ isHold: false,
1612
+ participants: ['agent-2', 'agent-1'],
1613
+ },
1614
+ } as any,
1615
+ } as any,
1616
+ });
1617
+ const context = createVoiceContext({
1618
+ taskData: taskData as any,
1619
+ consultInitiator: true,
1620
+ consultDestinationAgentJoined: true,
1621
+ consultCallHeld: false,
1622
+ });
1623
+
1624
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData as any);
1625
+
1626
+ expect(uiControls.activeLeg).toBe('consult');
1627
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: true});
1628
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: true});
1629
+ expect(uiControls.consult.mergeToConference).toEqual({isVisible: true, isEnabled: true});
1630
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
1631
+ });
1632
+
1633
+ it('shows RONA failure controls after AgentConsultFailed while HELD', () => {
1634
+ const taskData = createConsultFailedHeldMainTaskData();
1635
+ const context = createVoiceContext({
1636
+ taskData: taskData as any,
1637
+ consultInitiator: false,
1638
+ consultDestinationAgentJoined: false,
1639
+ consultCallHeld: false,
1640
+ });
1641
+
1642
+ const uiControls = computeUIControls(TaskState.HELD, context, taskData as any);
1643
+
1644
+ expect(uiControls.activeLeg).toBe('main');
1645
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: true});
1646
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
1647
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1648
+ expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: true});
1649
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
1650
+ expect(uiControls.main.conference).toEqual({isVisible: false, isEnabled: false});
1651
+ expect(uiControls.consult.endConsult).toEqual({isVisible: false, isEnabled: false});
1652
+ });
1653
+
1654
+ it('enables main.consult on AgentConsultFailed while consult media remains and destinationJoined is stale', () => {
1655
+ const taskData = createConsultFailedHeldMainTaskData();
1656
+ const context = createVoiceContext({
1657
+ taskData: taskData as any,
1658
+ consultInitiator: true,
1659
+ consultDestinationAgentJoined: true,
1660
+ consultCallHeld: false,
1661
+ });
1662
+
1663
+ const uiControls = computeUIControls(TaskState.HELD, context, taskData as any);
1664
+
1665
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1666
+ });
1667
+
1668
+ it('enables main.consult when consultCompleted with stale consultMediaResourceId only (RONA cleanup)', () => {
1669
+ const taskData = createTaskData({
1670
+ agentId: 'agent-1',
1671
+ mediaResourceId: 'interaction-1',
1672
+ consultMediaResourceId: 'consult-media',
1673
+ isConsulted: false,
1674
+ type: 'AgentConsultEnded' as any,
1675
+ interaction: {
1676
+ state: 'connected',
1677
+ interactionId: 'interaction-1',
1678
+ mainInteractionId: 'interaction-1',
1679
+ owner: 'agent-1',
1680
+ participants: {
1681
+ 'agent-1': {
1682
+ id: 'agent-1',
1683
+ pType: 'Agent',
1684
+ hasLeft: false,
1685
+ consultState: 'consultCompleted',
1686
+ isConsulted: false,
1687
+ },
1688
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1689
+ } as any,
1690
+ media: {
1691
+ 'interaction-1': {
1692
+ mediaResourceId: 'interaction-1',
1693
+ mType: 'mainCall',
1694
+ isHold: true,
1695
+ participants: ['customer-1', 'agent-1'],
1696
+ },
1697
+ } as any,
1698
+ } as any,
1699
+ });
1700
+ const context = createVoiceContext({
1701
+ taskData: taskData as any,
1702
+ consultInitiator: true,
1703
+ consultDestinationAgentJoined: true,
1704
+ consultCallHeld: false,
1705
+ });
1706
+
1707
+ const uiControls = computeUIControls(TaskState.CONSULT_INITIATING, context, taskData as any);
1708
+
1709
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1710
+ });
1711
+
1712
+ it('enables main.consult after AgentConsultFailed then AgentConsultEnded on held main leg (RONA)', () => {
1713
+ const consultEndedTaskData = createTaskData({
1714
+ agentId: 'agent-1',
1715
+ mediaResourceId: 'interaction-1',
1716
+ consultMediaResourceId: 'consult-media',
1717
+ destAgentId: 'agent-2',
1718
+ destinationType: 'Agent',
1719
+ isConsulted: false,
1720
+ type: 'AgentConsultEnded' as any,
1721
+ interaction: {
1722
+ state: 'connected',
1723
+ interactionId: 'interaction-1',
1724
+ mainInteractionId: 'interaction-1',
1725
+ owner: 'agent-1',
1726
+ participants: {
1727
+ 'agent-1': {
1728
+ id: 'agent-1',
1729
+ pType: 'Agent',
1730
+ hasLeft: false,
1731
+ consultState: 'consultCompleted',
1732
+ isConsulted: false,
1733
+ },
1734
+ 'agent-2': {
1735
+ id: 'agent-2',
1736
+ pType: 'Agent',
1737
+ hasLeft: false,
1738
+ hasJoined: false,
1739
+ consultState: 'consultReserved',
1740
+ isConsulted: true,
1741
+ },
1742
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1743
+ } as any,
1744
+ media: {
1745
+ 'interaction-1': {
1746
+ mediaResourceId: 'interaction-1',
1747
+ mType: 'mainCall',
1748
+ isHold: true,
1749
+ participants: ['customer-1', 'agent-1'],
1750
+ },
1751
+ } as any,
1752
+ } as any,
1753
+ });
1754
+ const staleContext = createVoiceContext({
1755
+ taskData: consultEndedTaskData as any,
1756
+ consultInitiator: true,
1757
+ consultDestinationAgentJoined: false,
1758
+ consultCallHeld: false,
1759
+ consultFromConference: false,
1760
+ });
1761
+
1762
+ const uiControls = computeUIControls(TaskState.HELD, staleContext, consultEndedTaskData as any);
1763
+
1764
+ expect(uiControls.activeLeg).toBe('main');
1765
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1766
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: true});
1767
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
1768
+ });
1769
+
1770
+ it('enables main.consult after AgentConsultEnded when consult ended before consultee answered (held main leg)', () => {
1771
+ // Agent 1 initiates consult to Agent 2 and ends it before Agent 2 answers.
1772
+ // Backend AgentConsultEnded: main on hold, self consultCompleted, no consult media,
1773
+ // and the consultee (destAgent) is not present in the participants map.
1774
+ const consultEndedTaskData = createTaskData({
1775
+ agentId: 'agent-1',
1776
+ mediaResourceId: 'interaction-1',
1777
+ consultMediaResourceId: 'consult-media',
1778
+ destAgentId: 'agent-2',
1779
+ destinationType: 'Agent',
1780
+ isConsulted: false,
1781
+ type: 'AgentConsultEnded' as any,
1782
+ interaction: {
1783
+ state: 'connected',
1784
+ interactionId: 'interaction-1',
1785
+ mainInteractionId: 'interaction-1',
1786
+ owner: 'agent-1',
1787
+ participants: {
1788
+ 'agent-1': {
1789
+ id: 'agent-1',
1790
+ pType: 'Agent',
1791
+ hasLeft: false,
1792
+ hasJoined: true,
1793
+ consultState: 'consultCompleted',
1794
+ isConsulted: false,
1795
+ },
1796
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1797
+ } as any,
1798
+ media: {
1799
+ 'interaction-1': {
1800
+ mediaResourceId: 'interaction-1',
1801
+ mType: 'mainCall',
1802
+ isHold: true,
1803
+ participants: ['customer-1', 'agent-1'],
1804
+ },
1805
+ } as any,
1806
+ } as any,
1807
+ });
1808
+ // Stale context from CONSULT_INITIATING: initiator true, consultee never joined.
1809
+ const staleContext = createVoiceContext({
1810
+ taskData: consultEndedTaskData as any,
1811
+ consultInitiator: true,
1812
+ consultDestinationAgentJoined: false,
1813
+ consultCallHeld: false,
1814
+ consultFromConference: false,
1815
+ });
1816
+
1817
+ const uiControls = computeUIControls(TaskState.HELD, staleContext, consultEndedTaskData as any);
1818
+
1819
+ expect(uiControls.activeLeg).toBe('main');
1820
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1821
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: true});
1822
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
1823
+ expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: true});
1824
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
1825
+ expect(uiControls.consult.endConsult).toEqual({isVisible: false, isEnabled: false});
1826
+ });
1827
+
1828
+ it('enables main.consult after AgentConsultEnded even when reconciled task.data still carries stale consult media/participant', () => {
1829
+ // Reproduces the real runtime data: Task.reconcileData deep-merges and never deletes keys,
1830
+ // so after AgentConsultEnded the stale consult-media entry and consultee participant from
1831
+ // AgentConsultCreated persist in task.data. The consult button must still be enabled.
1832
+ const consultEndedTaskData = createTaskData({
1833
+ agentId: 'agent-1',
1834
+ mediaResourceId: 'interaction-1',
1835
+ consultMediaResourceId: 'consult-media',
1836
+ destAgentId: 'agent-2',
1837
+ destinationType: 'Agent',
1838
+ isConsulted: false,
1839
+ type: 'AgentConsultEnded' as any,
1840
+ interaction: {
1841
+ state: 'connected',
1842
+ interactionId: 'interaction-1',
1843
+ mainInteractionId: 'interaction-1',
1844
+ owner: 'agent-1',
1845
+ participants: {
1846
+ 'agent-1': {
1847
+ id: 'agent-1',
1848
+ pType: 'Agent',
1849
+ hasLeft: false,
1850
+ hasJoined: true,
1851
+ consultState: 'consultCompleted',
1852
+ isConsulted: false,
1853
+ },
1854
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1855
+ // Stale consultee retained by reconcileData (never joined, consult leg gone).
1856
+ 'agent-2': {
1857
+ id: 'agent-2',
1858
+ pType: 'Agent',
1859
+ hasLeft: false,
1860
+ hasJoined: false,
1861
+ consultState: 'consulting',
1862
+ isConsulted: true,
1863
+ },
1864
+ } as any,
1865
+ media: {
1866
+ 'interaction-1': {
1867
+ mediaResourceId: 'interaction-1',
1868
+ mType: 'mainCall',
1869
+ isHold: true,
1870
+ participants: ['customer-1', 'agent-1'],
1871
+ },
1872
+ // Stale consult media retained by reconcileData merge.
1873
+ 'consult-media': {
1874
+ mediaResourceId: 'consult-media',
1875
+ mType: 'consult',
1876
+ isHold: false,
1877
+ participants: ['agent-1', 'agent-2'],
1878
+ },
1879
+ } as any,
1880
+ } as any,
1881
+ });
1882
+ const staleContext = createVoiceContext({
1883
+ taskData: consultEndedTaskData as any,
1884
+ consultInitiator: true,
1885
+ consultDestinationAgentJoined: false,
1886
+ consultCallHeld: false,
1887
+ consultFromConference: false,
1888
+ });
1889
+
1890
+ const uiControls = computeUIControls(TaskState.HELD, staleContext, consultEndedTaskData as any);
1891
+
1892
+ expect(uiControls.activeLeg).toBe('main');
1893
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1894
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: true});
1895
+ expect(uiControls.consult.endConsult).toEqual({isVisible: false, isEnabled: false});
1896
+ });
1897
+
1898
+ it('enables main.end after resume when stale consult media persists after RONA (AgentContactUnheld)', () => {
1899
+ const unheldTaskData = createTaskData({
1900
+ agentId: 'agent-1',
1901
+ mediaResourceId: 'interaction-1',
1902
+ consultMediaResourceId: 'consult-media',
1903
+ isConsulted: false,
1904
+ type: 'AgentContactUnheld' as any,
1905
+ interaction: {
1906
+ state: 'connected',
1907
+ interactionId: 'interaction-1',
1908
+ mainInteractionId: 'interaction-1',
1909
+ owner: 'agent-1',
1910
+ participants: {
1911
+ 'agent-1': {
1912
+ id: 'agent-1',
1913
+ pType: 'Agent',
1914
+ hasLeft: false,
1915
+ consultState: 'consultCompleted',
1916
+ isConsulted: false,
1917
+ },
1918
+ 'agent-2': {
1919
+ id: 'agent-2',
1920
+ pType: 'Agent',
1921
+ hasLeft: false,
1922
+ hasJoined: false,
1923
+ consultState: 'consultReserved',
1924
+ isConsulted: true,
1925
+ },
1926
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1927
+ } as any,
1928
+ media: {
1929
+ 'interaction-1': {
1930
+ mediaResourceId: 'interaction-1',
1931
+ mType: 'mainCall',
1932
+ isHold: false,
1933
+ participants: ['customer-1', 'agent-1'],
1934
+ },
1935
+ 'consult-media': {
1936
+ mediaResourceId: 'consult-media',
1937
+ mType: 'consult',
1938
+ isHold: false,
1939
+ participants: ['agent-1', 'agent-2'],
1940
+ },
1941
+ } as any,
1942
+ } as any,
1943
+ });
1944
+ const context = createVoiceContext({
1945
+ taskData: unheldTaskData as any,
1946
+ consultInitiator: false,
1947
+ consultDestinationAgentJoined: false,
1948
+ consultCallHeld: false,
1949
+ });
1950
+
1951
+ const uiControls = computeUIControls(TaskState.CONNECTED, context, unheldTaskData as any);
1952
+
1953
+ expect(uiControls.activeLeg).toBe('main');
1954
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: true});
1955
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
1956
+ expect(uiControls.consult.endConsult).toEqual({isVisible: false, isEnabled: false});
1957
+ });
1958
+ });
1959
+
1960
+ describe('uiControlsComputer outdial accept/decline controls', () => {
1961
+ function createOutdialContext(voiceVariant: 'webrtc' | 'pstn' = 'webrtc'): TaskContext {
1962
+ const taskData = createTaskData({
1963
+ interaction: {
1964
+ outboundType: 'OUTDIAL',
1965
+ state: 'new',
1966
+ isTerminated: false,
1967
+ } as any,
1968
+ });
1969
+ return {
1970
+ taskData,
1971
+ consultInitiator: false,
1972
+ exitingConference: false,
1973
+ consultFromConference: false,
1974
+ transferConferenceRequested: false,
1975
+ consultDestinationType: null,
1976
+ consultDestinationAgentId: null,
1977
+ consultDestinationAgentJoined: false,
1978
+ consultCallHeld: false,
1979
+ recordingControlsAvailable: false,
1980
+ recordingInProgress: false,
1981
+ uiControlConfig: {
1982
+ isEndTaskEnabled: true,
1983
+ isEndConsultEnabled: true,
1984
+ channelType: TASK_CHANNEL_TYPE.VOICE,
1985
+ isRecordingEnabled: false,
1986
+ agentId: 'agent-1',
1987
+ voiceVariant,
1988
+ },
1989
+ uiControls: getDefaultUIControls(),
1990
+ };
1991
+ }
1992
+
1993
+ it('accept is visible but disabled for WebRTC outdial in OFFERED state', () => {
1994
+ const context = createOutdialContext('webrtc');
1995
+ const uiControls = computeUIControls(TaskState.OFFERED, context, context.taskData);
1996
+
1997
+ expect(uiControls.main.accept).toEqual({isVisible: true, isEnabled: false});
1998
+ });
1999
+
2000
+ it('decline is visible but disabled for WebRTC outdial in OFFERED state', () => {
2001
+ const context = createOutdialContext('webrtc');
2002
+ const uiControls = computeUIControls(TaskState.OFFERED, context, context.taskData);
2003
+
2004
+ expect(uiControls.main.decline).toEqual({isVisible: true, isEnabled: false});
2005
+ });
2006
+ });
2007
+
2008
+ describe('uiControlsComputer WebRTC call-control end button', () => {
2009
+ function createConnectedContext(voiceVariant: 'webrtc' | 'pstn'): TaskContext {
2010
+ const taskData = createTaskData({
2011
+ interaction: {
2012
+ state: 'connected',
2013
+ } as any,
2014
+ });
2015
+
2016
+ return {
2017
+ taskData,
2018
+ consultInitiator: false,
2019
+ exitingConference: false,
2020
+ consultFromConference: false,
2021
+ transferConferenceRequested: false,
2022
+ consultDestinationType: null,
2023
+ consultDestinationAgentId: null,
2024
+ consultDestinationAgentJoined: false,
2025
+ consultCallHeld: false,
2026
+ hideBlindTransferForEpDnPendingMerge: false,
2027
+ recordingControlsAvailable: true,
2028
+ recordingInProgress: true,
2029
+ uiControlConfig: {
2030
+ isEndTaskEnabled: false,
2031
+ isEndConsultEnabled: true,
2032
+ channelType: TASK_CHANNEL_TYPE.VOICE,
2033
+ isRecordingEnabled: true,
2034
+ agentId: taskData.agentId,
2035
+ voiceVariant,
2036
+ },
2037
+ uiControls: getDefaultUIControls(),
2038
+ };
2039
+ }
2040
+
2041
+ it('shows main end for connected WebRTC call-control tasks even when end task is disabled', () => {
2042
+ const context = createConnectedContext('webrtc');
2043
+ const uiControls = computeUIControls(TaskState.CONNECTED, context, context.taskData);
2044
+
2045
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
2046
+ });
2047
+
2048
+ it('keeps main end hidden for non-WebRTC voice tasks when end task is disabled', () => {
2049
+ const context = createConnectedContext('pstn');
2050
+ const uiControls = computeUIControls(TaskState.CONNECTED, context, context.taskData);
2051
+
2052
+ expect(uiControls.main.end).toEqual({isVisible: false, isEnabled: false});
2053
+ });
2054
+ });
2055
+
2056
+ describe('uiControlsComputer conference controls', () => {
2057
+ function createConferenceTaskData(participantCount: number) {
2058
+ const participants: Record<string, any> = {
2059
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: false},
2060
+ 'agent-1': {
2061
+ id: 'agent-1',
2062
+ pType: 'Agent',
2063
+ hasJoined: true,
2064
+ hasLeft: false,
2065
+ consultState: 'conferencing',
2066
+ },
2067
+ };
2068
+ const mainCallParticipants = ['customer-1', 'agent-1'];
2069
+
2070
+ if (participantCount > 1) {
2071
+ participants['agent-2'] = {
2072
+ id: 'agent-2',
2073
+ pType: 'Agent',
2074
+ hasJoined: true,
2075
+ hasLeft: false,
2076
+ consultState: 'conferencing',
2077
+ };
2078
+ mainCallParticipants.push('agent-2');
2079
+ }
2080
+
2081
+ return createTaskData({
2082
+ agentId: 'agent-1',
2083
+ mediaResourceId: 'interaction-1',
2084
+ consultMediaResourceId: '' as any,
2085
+ interaction: {
2086
+ interactionId: 'interaction-1',
2087
+ mainInteractionId: 'interaction-1',
2088
+ state: 'conference',
2089
+ participants,
2090
+ media: {
2091
+ 'interaction-1': {
2092
+ mediaResourceId: 'interaction-1',
2093
+ mType: 'mainCall',
2094
+ isHold: false,
2095
+ participants: mainCallParticipants,
2096
+ },
2097
+ },
2098
+ } as any,
2099
+ });
2100
+ }
2101
+
2102
+ function createConferenceContext(
2103
+ participantCount: number,
2104
+ overrides: Partial<TaskContext> = {}
2105
+ ): TaskContext {
2106
+ return {
2107
+ taskData: createConferenceTaskData(participantCount),
2108
+ consultInitiator: true,
2109
+ exitingConference: false,
2110
+ consultFromConference: false,
2111
+ transferConferenceRequested: false,
2112
+ consultDestinationType: null,
2113
+ consultDestinationAgentId: null,
2114
+ consultDestinationAgentJoined: false,
2115
+ consultCallHeld: false,
2116
+ hideBlindTransferForEpDnPendingMerge: false,
2117
+ recordingControlsAvailable: true,
2118
+ recordingInProgress: true,
2119
+ uiControlConfig: {
2120
+ isEndTaskEnabled: true,
2121
+ isEndConsultEnabled: true,
2122
+ channelType: TASK_CHANNEL_TYPE.VOICE,
2123
+ isRecordingEnabled: true,
2124
+ agentId: 'agent-1',
2125
+ },
2126
+ uiControls: getDefaultUIControls(),
2127
+ ...overrides,
2128
+ };
2129
+ }
2130
+
2131
+ it('pending conference (participantCount <= 1): transfer/recording enabled, consult disabled, exitConference hidden', () => {
2132
+ const context = createConferenceContext(1);
2133
+
2134
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, context.taskData);
2135
+
2136
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
2137
+ expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: true});
2138
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
2139
+ expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
2140
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
2141
+ });
2142
+
2143
+ it('hides blind transfer on main after EP-DN consult merge before destination joins (CAI-8329)', () => {
2144
+ const taskData = createTaskData({
2145
+ agentId: 'agent-1',
2146
+ consultingAgentId: 'agent-1',
2147
+ consultMediaResourceId: 'consult-media-1',
2148
+ isConsulted: false,
2149
+ destinationType: 'EP-DN',
2150
+ type: 'AgentConsultCreated' as any,
2151
+ interaction: {
2152
+ state: 'conference',
2153
+ interactionId: 'interaction-1',
2154
+ mainInteractionId: 'interaction-1',
2155
+ owner: 'agent-1',
2156
+ participants: {
2157
+ 'agent-1': {
2158
+ id: 'agent-1',
2159
+ pType: 'Agent',
2160
+ hasJoined: true,
2161
+ hasLeft: false,
2162
+ consultState: 'consulting',
2163
+ },
2164
+ 'dn-dest': {
2165
+ id: 'dn-dest',
2166
+ pType: 'DN',
2167
+ hasLeft: false,
2168
+ hasJoined: false,
2169
+ },
2170
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: false},
2171
+ } as any,
2172
+ media: {
2173
+ 'interaction-1': {
2174
+ mediaResourceId: 'interaction-1',
2175
+ mType: 'mainCall',
2176
+ participants: ['agent-1', 'customer-1'],
2177
+ isHold: false,
2178
+ },
2179
+ 'consult-media-1': {
2180
+ mediaResourceId: 'consult-media-1',
2181
+ mType: 'consult',
2182
+ participants: ['agent-1', 'dn-dest'],
2183
+ isHold: false,
2184
+ },
2185
+ } as any,
2186
+ } as any,
2187
+ });
2188
+ const context = createConferenceContext(1, {
2189
+ consultInitiator: false,
2190
+ consultFromConference: false,
2191
+ consultDestinationType: null,
2192
+ hideBlindTransferForEpDnPendingMerge: true,
2193
+ consultCallHeld: false,
2194
+ taskData,
2195
+ uiControlConfig: {
2196
+ isEndTaskEnabled: true,
2197
+ isEndConsultEnabled: true,
2198
+ channelType: TASK_CHANNEL_TYPE.VOICE,
2199
+ isRecordingEnabled: true,
2200
+ voiceVariant: VOICE_VARIANT.WEBRTC,
2201
+ agentId: 'agent-1',
2202
+ },
2203
+ });
2204
+
2205
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
2206
+
2207
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
2208
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
2209
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
2210
+ });
2211
+
2212
+ it('hides blind transfer on main after EP-DN consult merge when destination is hydrated as joined (CAI-8329)', () => {
2213
+ const taskData = createTaskData({
2214
+ agentId: 'agent-1',
2215
+ consultingAgentId: 'agent-1',
2216
+ consultMediaResourceId: 'consult-media-1',
2217
+ isConsulted: false,
2218
+ destinationType: 'EP-DN',
2219
+ type: 'AgentConsultCreated' as any,
2220
+ interaction: {
2221
+ state: 'conference',
2222
+ interactionId: 'interaction-1',
2223
+ mainInteractionId: 'interaction-1',
2224
+ owner: 'agent-1',
2225
+ callProcessingDetails: {
2226
+ consultDestinationAgentJoined: 'true',
2227
+ },
2228
+ participants: {
2229
+ 'agent-1': {
2230
+ id: 'agent-1',
2231
+ pType: 'Agent',
2232
+ hasJoined: true,
2233
+ hasLeft: false,
2234
+ consultState: 'consulting',
2235
+ },
2236
+ 'dn-dest': {
2237
+ id: 'dn-dest',
2238
+ pType: 'DN',
2239
+ hasLeft: false,
2240
+ hasJoined: false,
2241
+ },
2242
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: false},
2243
+ } as any,
2244
+ media: {
2245
+ 'interaction-1': {
2246
+ mediaResourceId: 'interaction-1',
2247
+ mType: 'mainCall',
2248
+ participants: ['agent-1', 'customer-1'],
2249
+ isHold: false,
2250
+ },
2251
+ 'consult-media-1': {
2252
+ mediaResourceId: 'consult-media-1',
2253
+ mType: 'consult',
2254
+ participants: ['agent-1', 'dn-dest'],
2255
+ isHold: false,
2256
+ },
2257
+ } as any,
2258
+ } as any,
2259
+ });
2260
+ const context = createConferenceContext(1, {
2261
+ consultInitiator: false,
2262
+ consultFromConference: false,
2263
+ consultDestinationType: null,
2264
+ hideBlindTransferForEpDnPendingMerge: true,
2265
+ consultCallHeld: false,
2266
+ taskData,
2267
+ uiControlConfig: {
2268
+ isEndTaskEnabled: true,
2269
+ isEndConsultEnabled: true,
2270
+ channelType: TASK_CHANNEL_TYPE.VOICE,
2271
+ isRecordingEnabled: true,
2272
+ voiceVariant: VOICE_VARIANT.WEBRTC,
2273
+ agentId: 'agent-1',
2274
+ },
2275
+ });
2276
+
2277
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
2278
+
2279
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
2280
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
2281
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
2282
+ });
2283
+
2284
+ it('hides blind transfer on AgentConsultConferencing after clearConsultState sets flag (CAI-8329)', () => {
2285
+ const agentId = '4b496e73-55e4-4d74-a53c-469babdd8326';
2286
+ const mainInteractionId = '08f427ed-71d8-41df-8a44-43e1bb2b2074';
2287
+ const consultMediaId = 'e6592ab7-8f56-4132-87a9-48c4c262df3c';
2288
+ const taskData = createTaskData({
2289
+ agentId,
2290
+ consultMediaResourceId: consultMediaId,
2291
+ isConsulted: false,
2292
+ type: 'AgentConsultConferencing' as any,
2293
+ interaction: {
2294
+ state: 'conference',
2295
+ interactionId: mainInteractionId,
2296
+ mainInteractionId,
2297
+ owner: agentId,
2298
+ callProcessingDetails: {
2299
+ relationshipType: 'consult',
2300
+ childInteractionId: consultMediaId,
2301
+ },
2302
+ participants: {
2303
+ [agentId]: {
2304
+ id: agentId,
2305
+ pType: 'Agent',
2306
+ hasJoined: true,
2307
+ hasLeft: false,
2308
+ consultState: 'conferencing',
2309
+ isConsulted: false,
2310
+ },
2311
+ '+13159998087': {
2312
+ id: '+13159998087',
2313
+ pType: 'EP-DN',
2314
+ type: 'EpDn',
2315
+ hasLeft: false,
2316
+ },
2317
+ '+14696762938': {
2318
+ id: '+14696762938',
2319
+ pType: 'Customer',
2320
+ hasJoined: true,
2321
+ hasLeft: false,
2322
+ },
2323
+ } as any,
2324
+ media: {
2325
+ [mainInteractionId]: {
2326
+ mediaResourceId: mainInteractionId,
2327
+ mType: 'mainCall',
2328
+ participants: ['+14696762938', agentId],
2329
+ isHold: false,
2330
+ },
2331
+ [consultMediaId]: {
2332
+ mediaResourceId: consultMediaId,
2333
+ mType: 'consult',
2334
+ participants: ['+13159998087', agentId],
2335
+ isHold: false,
2336
+ },
2337
+ } as any,
2338
+ } as any,
2339
+ });
2340
+ const context = createConferenceContext(1, {
2341
+ consultInitiator: false,
2342
+ consultFromConference: false,
2343
+ consultDestinationType: null,
2344
+ hideBlindTransferForEpDnPendingMerge: true,
2345
+ consultCallHeld: false,
2346
+ taskData,
2347
+ uiControlConfig: {
2348
+ isEndTaskEnabled: true,
2349
+ isEndConsultEnabled: true,
2350
+ channelType: TASK_CHANNEL_TYPE.VOICE,
2351
+ isRecordingEnabled: true,
2352
+ voiceVariant: VOICE_VARIANT.WEBRTC,
2353
+ agentId,
2354
+ },
2355
+ });
2356
+
2357
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
2358
+
2359
+ expect(uiControls.activeLeg).toBe('main');
2360
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
2361
+ expect(uiControls.main.mute).toEqual({isVisible: true, isEnabled: true});
2362
+ expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: true});
2363
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
2364
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
2365
+ });
2366
+
2367
+ it('shows blind transfer on pending conference when consult context is absent (CAI-8329)', () => {
2368
+ const context = createConferenceContext(1, {
2369
+ consultInitiator: false,
2370
+ consultFromConference: false,
2371
+ consultDestinationType: null,
2372
+ });
2373
+
2374
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, context.taskData);
2375
+
2376
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
2377
+ });
2378
+
2379
+ it('real conference (participantCount > 1): transfer hidden, recording visible when controls available, consult enabled, exitConference visible', () => {
2380
+ const context = createConferenceContext(2);
2381
+
2382
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, context.taskData);
2383
+
2384
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
2385
+ expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: true});
2386
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
2387
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
2388
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
2389
+ });
2390
+
2391
+ it('real conference (participantCount > 1): recording hidden when recording controls unavailable', () => {
2392
+ const context = createConferenceContext(2, {
2393
+ recordingControlsAvailable: false,
2394
+ recordingInProgress: false,
2395
+ });
2396
+
2397
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, context.taskData);
2398
+
2399
+ expect(uiControls.main.recording).toEqual({isVisible: false, isEnabled: false});
2400
+ });
2401
+
2402
+ it('shows recording when second agent joins EP-DN merged conference (CAI-8329)', () => {
2403
+ const taskData = createTaskData({
2404
+ agentId: 'agent-1',
2405
+ type: 'ContactUpdated' as any,
2406
+ interaction: {
2407
+ state: 'conference',
2408
+ interactionId: 'interaction-1',
2409
+ mainInteractionId: 'interaction-1',
2410
+ owner: 'agent-1',
2411
+ callProcessingDetails: {
2412
+ recordInProgress: 'true',
2413
+ recordingStarted: 'true',
2414
+ epDnParticipantRemoved: '+13159998059',
2415
+ },
2416
+ participants: {
2417
+ 'agent-1': {
2418
+ id: 'agent-1',
2419
+ pType: 'Agent',
2420
+ hasJoined: true,
2421
+ hasLeft: false,
2422
+ consultState: 'conferencing',
2423
+ },
2424
+ 'agent-2': {
2425
+ id: 'agent-2',
2426
+ pType: 'Agent',
2427
+ hasJoined: true,
2428
+ hasLeft: false,
2429
+ consultState: 'conferencing',
2430
+ },
2431
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: false},
2432
+ } as any,
2433
+ media: {
2434
+ 'interaction-1': {
2435
+ mediaResourceId: 'interaction-1',
2436
+ mType: 'mainCall',
2437
+ participants: ['customer-1', 'agent-1', 'agent-2'],
2438
+ isHold: false,
2439
+ },
2440
+ } as any,
2441
+ } as any,
2442
+ });
2443
+ const context = createConferenceContext(2, {
2444
+ hideBlindTransferForEpDnPendingMerge: false,
2445
+ taskData,
2446
+ });
2447
+
2448
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
2449
+
2450
+ expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: true});
2451
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
2452
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
2453
+ });
2454
+
2455
+ it('post_call after customer exit: only exitConference and end enabled during EP-DN pending merge (CAI-8329)', () => {
2456
+ const taskData = createTaskData({
2457
+ agentId: 'agent-1',
2458
+ consultingAgentId: 'agent-1',
2459
+ consultMediaResourceId: 'consult-media-1',
2460
+ isConsulted: false,
2461
+ destinationType: 'EP-DN',
2462
+ type: 'ParticipantPostCallActivity' as any,
2463
+ interaction: {
2464
+ state: 'post_call',
2465
+ interactionId: 'interaction-1',
2466
+ mainInteractionId: 'interaction-1',
2467
+ owner: 'agent-1',
2468
+ callProcessingDetails: {
2469
+ hasCustomerLeft: 'true',
2470
+ },
2471
+ participants: {
2472
+ 'agent-1': {
2473
+ id: 'agent-1',
2474
+ pType: 'Agent',
2475
+ hasJoined: true,
2476
+ hasLeft: false,
2477
+ consultState: 'conferencing',
2478
+ },
2479
+ '+13159998059': {
2480
+ id: '+13159998059',
2481
+ pType: 'EP-DN',
2482
+ hasLeft: false,
2483
+ hasJoined: false,
2484
+ },
2485
+ '+14696762938': {
2486
+ id: '+14696762938',
2487
+ pType: 'Customer',
2488
+ hasJoined: true,
2489
+ hasLeft: true,
2490
+ },
2491
+ } as any,
2492
+ media: {
2493
+ 'interaction-1': {
2494
+ mediaResourceId: 'interaction-1',
2495
+ mType: 'mainCall',
2496
+ participants: ['agent-1'],
2497
+ isHold: false,
2498
+ },
2499
+ 'consult-media-1': {
2500
+ mediaResourceId: 'consult-media-1',
2501
+ mType: 'consult',
2502
+ participants: ['+13159998059', 'agent-1'],
2503
+ isHold: false,
2504
+ },
2505
+ } as any,
2506
+ } as any,
2507
+ });
2508
+ const context = createConferenceContext(1, {
2509
+ consultInitiator: true,
2510
+ consultFromConference: true,
2511
+ hideBlindTransferForEpDnPendingMerge: true,
2512
+ taskData,
2513
+ uiControlConfig: {
2514
+ isEndTaskEnabled: true,
2515
+ isEndConsultEnabled: true,
2516
+ channelType: TASK_CHANNEL_TYPE.VOICE,
2517
+ isRecordingEnabled: true,
2518
+ voiceVariant: VOICE_VARIANT.WEBRTC,
2519
+ agentId: 'agent-1',
2520
+ },
2521
+ });
2522
+
2523
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
2524
+
2525
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
2526
+ expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: false});
2527
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: false});
2528
+ expect(uiControls.main.mute).toEqual({isVisible: true, isEnabled: false});
2529
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
2530
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
2531
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
2532
+ });
2533
+
2534
+ it('hides blind transfer after AgentConsultFailed ANOTHER_CONSULT_IN_PROGRESS during EP-DN pending merge (CAI-8329)', () => {
2535
+ const taskData = createTaskData({
2536
+ agentId: 'agent-1',
2537
+ consultingAgentId: 'agent-1',
2538
+ consultMediaResourceId: 'consult-media-1',
2539
+ isConsulted: false,
2540
+ destinationType: 'Agent',
2541
+ reason: 'ANOTHER_CONSULT_IN_PROGRESS',
2542
+ type: 'AgentConsultFailed' as any,
2543
+ interaction: {
2544
+ state: 'conference',
2545
+ interactionId: 'interaction-1',
2546
+ mainInteractionId: 'interaction-1',
2547
+ owner: 'agent-1',
2548
+ participants: {
2549
+ 'agent-1': {
2550
+ id: 'agent-1',
2551
+ pType: 'Agent',
2552
+ hasJoined: true,
2553
+ hasLeft: false,
2554
+ consultState: 'conferencing',
2555
+ },
2556
+ '+13159998059': {
2557
+ id: '+13159998059',
2558
+ pType: 'EP-DN',
2559
+ hasLeft: false,
2560
+ hasJoined: false,
2561
+ },
2562
+ '+14696762938': {
2563
+ id: '+14696762938',
2564
+ pType: 'Customer',
2565
+ hasJoined: true,
2566
+ hasLeft: false,
2567
+ },
2568
+ } as any,
2569
+ media: {
2570
+ 'interaction-1': {
2571
+ mediaResourceId: 'interaction-1',
2572
+ mType: 'mainCall',
2573
+ participants: ['agent-1', '+14696762938'],
2574
+ isHold: false,
2575
+ },
2576
+ 'consult-media-1': {
2577
+ mediaResourceId: 'consult-media-1',
2578
+ mType: 'consult',
2579
+ participants: ['+13159998059', 'agent-1'],
2580
+ isHold: false,
2581
+ },
2582
+ } as any,
2583
+ } as any,
2584
+ });
2585
+ const context = createConferenceContext(1, {
2586
+ consultInitiator: true,
2587
+ consultFromConference: true,
2588
+ hideBlindTransferForEpDnPendingMerge: true,
2589
+ taskData,
2590
+ uiControlConfig: {
2591
+ isEndTaskEnabled: true,
2592
+ isEndConsultEnabled: true,
2593
+ channelType: TASK_CHANNEL_TYPE.VOICE,
2594
+ isRecordingEnabled: true,
2595
+ voiceVariant: VOICE_VARIANT.WEBRTC,
2596
+ agentId: 'agent-1',
2597
+ },
2598
+ });
2599
+
2600
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
2601
+
2602
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
2603
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
2604
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
2605
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
2606
+ });
2607
+
2608
+ it('hold button shows visible but disabled in conference', () => {
2609
+ const context = createConferenceContext(2);
2610
+
2611
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, context.taskData);
2612
+
2613
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: false});
2614
+ });
2615
+ });
2616
+
2617
+ describe('uiControlsComputer consult/transfer destinations', () => {
2618
+ const AGENT = CONSULT_TRANSFER_DESTINATION_TYPE.AGENT;
2619
+ const QUEUE = CONSULT_TRANSFER_DESTINATION_TYPE.QUEUE;
2620
+ const DIAL_NUMBER = CONSULT_TRANSFER_DESTINATION_TYPE.DIALNUMBER;
2621
+ const ENTRY_POINT = CONSULT_TRANSFER_DESTINATION_TYPE.ENTRYPOINT;
2622
+
2623
+ it('returns the default destination order and applies the action-specific inbound voice queue rule', () => {
2624
+ const taskData = createTaskData({
2625
+ interaction: {contactDirection: {type: 'INBOUND'}} as any,
2626
+ });
2627
+ const context = createConsultTransferContext(taskData, {allowConsultToQueue: false});
2628
+
2629
+ const controls = computeUIControls(TaskState.CONNECTED, context, taskData);
2630
+
2631
+ expect(controls.consultTransferDestinations.consult).toEqual([AGENT, DIAL_NUMBER, ENTRY_POINT]);
2632
+ expect(controls.consultTransferDestinations.transfer).toEqual([
2633
+ AGENT,
2634
+ QUEUE,
2635
+ DIAL_NUMBER,
2636
+ ENTRY_POINT,
2637
+ ]);
2638
+ });
2639
+
2640
+ it('reads the outbound queue-transfer flag from callProcessingDetails', () => {
2641
+ const disabledTaskData = createTaskData({
2642
+ interaction: {
2643
+ contactDirection: {type: 'OUTBOUND'},
2644
+ callProcessingDetails: {outdialTransferToQueueEnabled: false},
2645
+ } as any,
2646
+ });
2647
+ const disabledContext = createConsultTransferContext(disabledTaskData);
2648
+ const disabledControls = computeUIControls(
2649
+ TaskState.CONNECTED,
2650
+ disabledContext,
2651
+ disabledTaskData
2652
+ );
2653
+
2654
+ expect(disabledControls.consultTransferDestinations.transfer).toEqual([
2655
+ AGENT,
2656
+ DIAL_NUMBER,
2657
+ ENTRY_POINT,
2658
+ ]);
2659
+
2660
+ const enabledTaskData = createTaskData({
2661
+ interaction: {
2662
+ contactDirection: {type: 'OUTBOUND'},
2663
+ callProcessingDetails: {outdialTransferToQueueEnabled: true},
2664
+ } as any,
2665
+ });
2666
+ const enabledContext = createConsultTransferContext(enabledTaskData);
2667
+ const enabledControls = computeUIControls(TaskState.CONNECTED, enabledContext, enabledTaskData);
2668
+
2669
+ expect(enabledControls.consultTransferDestinations.transfer).toEqual([
2670
+ AGENT,
2671
+ QUEUE,
2672
+ DIAL_NUMBER,
2673
+ ENTRY_POINT,
2674
+ ]);
2675
+ expect(haveUIControlsChanged(disabledControls, enabledControls)).toBe(true);
2676
+ });
2677
+
2678
+ it('hides profile-disabled destinations and does not allow queue transfer for unknown voice direction', () => {
2679
+ const taskData = createTaskData({
2680
+ interaction: {contactDirection: {type: 'UNKNOWN'}} as any,
2681
+ });
2682
+ const context = createConsultTransferContext(taskData, {
2683
+ accessQueue: 'NONE',
2684
+ accessEntryPoint: 'NONE',
2685
+ accessBuddyTeam: 'NONE',
2686
+ });
2687
+
2688
+ const controls = computeUIControls(TaskState.CONNECTED, context, taskData);
2689
+
2690
+ expect(controls.consultTransferDestinations).toEqual({
2691
+ consult: [DIAL_NUMBER],
2692
+ transfer: [DIAL_NUMBER],
2693
+ });
2694
+ });
2695
+
2696
+ it('returns only agents and queues for digital tasks', () => {
2697
+ const taskData = createTaskData({
2698
+ interaction: {mediaType: 'chat'} as any,
2699
+ });
2700
+ const voiceContext = createConsultTransferContext(taskData, {allowConsultToQueue: false});
2701
+ const context: TaskContext = {
2702
+ ...voiceContext,
2703
+ uiControlConfig: {
2704
+ ...voiceContext.uiControlConfig,
2705
+ channelType: TASK_CHANNEL_TYPE.DIGITAL,
2706
+ },
2707
+ };
2708
+
2709
+ const controls = computeUIControls(TaskState.CONNECTED, context, taskData);
2710
+
2711
+ expect(controls.consultTransferDestinations).toEqual({
2712
+ consult: [AGENT, QUEUE],
2713
+ transfer: [AGENT, QUEUE],
2714
+ });
2715
+ });
2716
+
2717
+ it('returns no destinations when no agent-profile policy was configured', () => {
2718
+ const context = createVoiceContext();
2719
+
2720
+ const controls = computeUIControls(TaskState.CONNECTED, context, context.taskData);
2721
+
2722
+ expect(controls.consultTransferDestinations).toEqual({consult: [], transfer: []});
2723
+ });
2724
+ });
2725
+
2726
+ describe('uiControlsComputer post-call consult controls (customer left)', () => {
2727
+ function createPostCallConsultTaskData() {
2728
+ return createTaskData({
2729
+ agentId: 'agent-1',
2730
+ mediaResourceId: 'interaction-1',
2731
+ consultMediaResourceId: 'consult-media',
2732
+ consultingAgentId: 'agent-1',
2733
+ destAgentId: 'agent-2',
2734
+ interaction: {
2735
+ interactionId: 'interaction-1',
2736
+ mainInteractionId: 'interaction-1',
2737
+ state: 'post_call',
2738
+ isTerminated: false,
2739
+ participants: {
2740
+ 'agent-1': {
2741
+ id: 'agent-1',
2742
+ pType: 'Agent',
2743
+ hasJoined: true,
2744
+ hasLeft: false,
2745
+ consultState: 'consulting',
2746
+ },
2747
+ 'agent-2': {
2748
+ id: 'agent-2',
2749
+ pType: 'Agent',
2750
+ hasLeft: false,
2751
+ consultState: 'consulting',
2752
+ isConsulted: true,
2753
+ },
2754
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: true},
2755
+ } as any,
2756
+ media: {
2757
+ 'interaction-1': {
2758
+ mediaResourceId: 'interaction-1',
2759
+ mType: 'mainCall',
2760
+ isHold: false,
2761
+ participants: ['agent-1'],
2762
+ },
2763
+ 'consult-media': {
2764
+ mediaResourceId: 'consult-media',
2765
+ mType: 'consult',
2766
+ isHold: false,
2767
+ participants: ['agent-1', 'agent-2'],
2768
+ },
2769
+ } as any,
2770
+ } as any,
2771
+ });
2772
+ }
2773
+
2774
+ it('consult controls are visible but disabled when customer has left', () => {
2775
+ const taskData = createPostCallConsultTaskData();
2776
+ const context: TaskContext = {
2777
+ taskData,
2778
+ consultInitiator: true,
2779
+ exitingConference: false,
2780
+ consultFromConference: false,
2781
+ transferConferenceRequested: false,
2782
+ consultDestinationType: null,
2783
+ consultDestinationAgentId: null,
2784
+ consultDestinationAgentJoined: true,
2785
+ consultCallHeld: false,
2786
+ hideBlindTransferForEpDnPendingMerge: false,
2787
+ recordingControlsAvailable: true,
2788
+ recordingInProgress: true,
2789
+ uiControlConfig: {
2790
+ isEndTaskEnabled: true,
2791
+ isEndConsultEnabled: true,
2792
+ channelType: TASK_CHANNEL_TYPE.VOICE,
2793
+ isRecordingEnabled: true,
2794
+ agentId: 'agent-1',
2795
+ },
2796
+ uiControls: getDefaultUIControls(),
2797
+ };
2798
+
2799
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, context.taskData);
2800
+
2801
+ // Consult leg: transfer/conference/merge/switch should be visible but disabled
2802
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false});
2803
+ expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: false});
2804
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: false});
2805
+
2806
+ // Main leg end should be visible but disabled
2807
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
2808
+ });
2809
+ });
2810
+
2811
+ // ---------------------------------------------------------------------------
2812
+ // WXCC-6026: wxApp thick-client answer UI controls
2813
+ // ---------------------------------------------------------------------------
2814
+
2815
+ const WX_APP_DEVICE = {
2816
+ deviceType: 'wxApp',
2817
+ deviceId: 'device-id-1',
2818
+ deviceCallId: 'call-id-1',
2819
+ };
2820
+
2821
+ function createWxAppContext(
2822
+ overrides: Partial<{
2823
+ enableWxBetterTogether: boolean;
2824
+ wxAppAnswerPending: boolean;
2825
+ wxAppAcceptInFlight: boolean;
2826
+ }> = {}
2827
+ ): TaskContext {
2828
+ return {
2829
+ taskData: createTaskData({
2830
+ agentId: 'agent-1',
2831
+ mediaResourceId: 'interaction-1',
2832
+ interaction: {
2833
+ interactionId: 'interaction-1',
2834
+ participants: {
2835
+ 'agent-1': {id: 'agent-1', pType: 'Agent', ...WX_APP_DEVICE},
2836
+ 'customer-1': {id: 'customer-1', pType: 'Customer'},
2837
+ } as any,
2838
+ media: {
2839
+ 'interaction-1': {mediaResourceId: 'interaction-1', isHold: false},
2840
+ } as any,
2841
+ } as any,
2842
+ }),
2843
+ consultInitiator: false,
2844
+ exitingConference: false,
2845
+ consultFromConference: false,
2846
+ transferConferenceRequested: false,
2847
+ consultDestinationType: null,
2848
+ consultDestinationAgentId: null,
2849
+ consultDestinationAgentJoined: false,
2850
+ consultCallHeld: false,
2851
+ hideBlindTransferForEpDnPendingMerge: false,
2852
+ recordingControlsAvailable: false,
2853
+ recordingInProgress: false,
2854
+ uiControlConfig: {
2855
+ isEndTaskEnabled: true,
2856
+ isEndConsultEnabled: false,
2857
+ channelType: TASK_CHANNEL_TYPE.VOICE,
2858
+ isRecordingEnabled: false,
2859
+ agentId: 'agent-1',
2860
+ enableWxBetterTogether: overrides.enableWxBetterTogether ?? true,
2861
+ wxAppAnswerPending: overrides.wxAppAnswerPending,
2862
+ wxAppAcceptInFlight: overrides.wxAppAcceptInFlight,
2863
+ },
2864
+ uiControls: getDefaultUIControls(),
2865
+ };
2866
+ }
2867
+
2868
+ describe('uiControlsComputer wxApp thick-client answer (WXCC-6026)', () => {
2869
+ describe('accept and decline when wxApp offer and enableWxBetterTogether=true', () => {
2870
+ it('accept is visible and enabled in OFFERED state with wxApp participant', () => {
2871
+ const ctx = createWxAppContext();
2872
+ const controls = computeUIControls(TaskState.OFFERED, ctx, ctx.taskData);
2873
+
2874
+ expect(controls.main.accept).toEqual({isVisible: true, isEnabled: true});
2875
+ });
2876
+
2877
+ it('decline is visible and enabled in OFFERED state with wxApp participant', () => {
2878
+ const ctx = createWxAppContext();
2879
+ const controls = computeUIControls(TaskState.OFFERED, ctx, ctx.taskData);
2880
+
2881
+ expect(controls.main.decline).toEqual({isVisible: true, isEnabled: true});
2882
+ });
2883
+
2884
+ it('accept is visible but disabled while wxAppAcceptInFlight on inbound offer', () => {
2885
+ const ctx = createWxAppContext({wxAppAcceptInFlight: true});
2886
+ const controls = computeUIControls(TaskState.OFFERED, ctx, ctx.taskData);
2887
+
2888
+ expect(controls.main.accept).toEqual({isVisible: true, isEnabled: false});
2889
+ });
2890
+
2891
+ it('accept is visible but disabled while wxAppAnswerPending on inbound offer', () => {
2892
+ const ctx = createWxAppContext({wxAppAnswerPending: true});
2893
+ const controls = computeUIControls(TaskState.OFFERED, ctx, ctx.taskData);
2894
+
2895
+ expect(controls.main.accept).toEqual({isVisible: true, isEnabled: false});
2896
+ });
2897
+
2898
+ it('decline is visible but disabled while wxAppAnswerPending on inbound offer', () => {
2899
+ const ctx = createWxAppContext({wxAppAnswerPending: true});
2900
+ const controls = computeUIControls(TaskState.OFFERED, ctx, ctx.taskData);
2901
+
2902
+ expect(controls.main.decline).toEqual({isVisible: true, isEnabled: false});
2903
+ });
2904
+
2905
+ it('decline is visible but disabled while wxAppAcceptInFlight on outdial offer', () => {
2906
+ const ctx = createWxAppContext({wxAppAcceptInFlight: true, wxAppAnswerPending: true});
2907
+ ctx.taskData.interaction.outboundType = 'OUTDIAL';
2908
+ const controls = computeUIControls(TaskState.OFFERED, ctx, ctx.taskData);
2909
+
2910
+ expect(controls.main.decline).toEqual({isVisible: true, isEnabled: false});
2911
+ });
2912
+
2913
+ it('decline is visible and enabled while wxAppAnswerPending on outdial offer (Calling phase)', () => {
2914
+ const ctx = createWxAppContext({wxAppAnswerPending: true});
2915
+ ctx.taskData.interaction.outboundType = 'OUTDIAL';
2916
+ const controls = computeUIControls(TaskState.OFFERED, ctx, ctx.taskData);
2917
+
2918
+ expect(controls.main.decline).toEqual({isVisible: true, isEnabled: true});
2919
+ });
2920
+
2921
+ it('keypad is disabled in OFFERED state (not yet engaged)', () => {
2922
+ const ctx = createWxAppContext();
2923
+ const controls = computeUIControls(TaskState.OFFERED, ctx, ctx.taskData);
2924
+
2925
+ expect(controls.main.keypad).toEqual({isVisible: false, isEnabled: false});
2926
+ });
2927
+ });
2928
+
2929
+ describe('mute and keypad when wxApp engaged (CONNECTED state)', () => {
2930
+ it('mute is visible and enabled in CONNECTED state', () => {
2931
+ const ctx = createWxAppContext();
2932
+ const controls = computeUIControls(TaskState.CONNECTED, ctx, ctx.taskData);
2933
+
2934
+ expect(controls.main.mute).toEqual({isVisible: true, isEnabled: true});
2935
+ });
2936
+
2937
+ it('keypad is visible and enabled in CONNECTED state', () => {
2938
+ const ctx = createWxAppContext();
2939
+ const controls = computeUIControls(TaskState.CONNECTED, ctx, ctx.taskData);
2940
+
2941
+ expect(controls.main.keypad).toEqual({isVisible: true, isEnabled: true});
2942
+ });
2943
+
2944
+ it('keypad is disabled in WRAPPING_UP state', () => {
2945
+ const ctx = createWxAppContext();
2946
+ const controls = computeUIControls(TaskState.WRAPPING_UP, ctx, ctx.taskData);
2947
+
2948
+ expect(controls.main.keypad).toEqual({isVisible: false, isEnabled: false});
2949
+ });
2950
+
2951
+ it('mute is disabled in WRAPPING_UP state', () => {
2952
+ const ctx = createWxAppContext();
2953
+ const controls = computeUIControls(TaskState.WRAPPING_UP, ctx, ctx.taskData);
2954
+
2955
+ expect(controls.main.mute).toEqual({isVisible: false, isEnabled: false});
2956
+ });
2957
+ });
2958
+
2959
+ describe('when enableWxBetterTogether=false', () => {
2960
+ it('accept falls back to standard WebRTC logic (disabled for non-WebRTC)', () => {
2961
+ const ctx = createWxAppContext({enableWxBetterTogether: false});
2962
+ const controls = computeUIControls(TaskState.OFFERED, ctx, ctx.taskData);
2963
+
2964
+ // Without WebRTC variant, accept should be visible but disabled per existing logic
2965
+ expect(controls.main.accept.isVisible).toBe(true);
2966
+ // isEnabled is false without webRTC variant
2967
+ expect(controls.main.accept.isEnabled).toBe(false);
2968
+ });
2969
+
2970
+ it('keypad is always disabled when enableWxBetterTogether=false', () => {
2971
+ const ctx = createWxAppContext({enableWxBetterTogether: false});
2972
+ const controls = computeUIControls(TaskState.CONNECTED, ctx, ctx.taskData);
2973
+
2974
+ expect(controls.main.keypad).toEqual({isVisible: false, isEnabled: false});
2975
+ });
2976
+
2977
+ it('mute is disabled for non-WebRTC when enableWxBetterTogether=false', () => {
2978
+ const ctx = createWxAppContext({enableWxBetterTogether: false});
2979
+ const controls = computeUIControls(TaskState.CONNECTED, ctx, ctx.taskData);
2980
+
2981
+ expect(controls.main.mute).toEqual({isVisible: false, isEnabled: false});
2982
+ });
2983
+ });
2984
+
2985
+ describe('default keypad state without wxApp', () => {
2986
+ it('keypad is disabled for standard non-wxApp tasks', () => {
2987
+ const ctx: TaskContext = {
2988
+ taskData: createTaskData(),
2989
+ consultInitiator: false,
2990
+ exitingConference: false,
2991
+ consultFromConference: false,
2992
+ transferConferenceRequested: false,
2993
+ consultDestinationType: null,
2994
+ consultDestinationAgentId: null,
2995
+ consultDestinationAgentJoined: false,
2996
+ consultCallHeld: false,
2997
+ hideBlindTransferForEpDnPendingMerge: false,
2998
+ recordingControlsAvailable: false,
2999
+ recordingInProgress: false,
3000
+ uiControlConfig: {
3001
+ isEndTaskEnabled: true,
3002
+ isEndConsultEnabled: false,
3003
+ channelType: TASK_CHANNEL_TYPE.VOICE,
3004
+ isRecordingEnabled: false,
3005
+ },
3006
+ uiControls: getDefaultUIControls(),
3007
+ };
3008
+ const controls = computeUIControls(TaskState.CONNECTED, ctx, ctx.taskData);
3009
+
3010
+ expect(controls.main.keypad).toEqual({isVisible: false, isEnabled: false});
3011
+ });
3012
+ });
3013
+
3014
+ describe('wxApp engaged through consult and hold phases', () => {
3015
+ it('enables main mute and keypad during consult-requested phase on main leg', () => {
3016
+ const ctx = createWxAppContext();
3017
+ ctx.taskData = createTaskData({
3018
+ agentId: 'agent-1',
3019
+ consultMediaResourceId: 'consult-media',
3020
+ interaction: {
3021
+ interactionId: 'interaction-1',
3022
+ mainInteractionId: 'interaction-1',
3023
+ participants: {
3024
+ 'agent-1': {
3025
+ id: 'agent-1',
3026
+ pType: 'Agent',
3027
+ consultState: 'consultInitiated',
3028
+ ...WX_APP_DEVICE,
3029
+ },
3030
+ 'customer-1': {id: 'customer-1', pType: 'Customer'},
3031
+ } as any,
3032
+ media: {
3033
+ 'interaction-1': {
3034
+ mediaResourceId: 'interaction-1',
3035
+ isHold: false,
3036
+ participants: ['agent-1', 'customer-1'],
3037
+ },
3038
+ 'consult-media': {
3039
+ mediaResourceId: 'consult-media',
3040
+ mType: 'consult',
3041
+ participants: ['agent-1'],
3042
+ },
3043
+ } as any,
3044
+ } as any,
3045
+ });
3046
+ ctx.consultInitiator = true;
3047
+ ctx.consultDestinationAgentJoined = false;
3048
+
3049
+ const controls = computeUIControls(TaskState.CONNECTED, ctx, ctx.taskData);
3050
+
3051
+ expect(controls.main.mute).toEqual({isVisible: true, isEnabled: true});
3052
+ expect(controls.main.keypad).toEqual({isVisible: true, isEnabled: true});
3053
+ });
3054
+
3055
+ it('enables main mute and keypad while consulting with main leg held', () => {
3056
+ const taskData = createConsultTaskData();
3057
+ taskData.interaction!.participants!['agent-1'] = {
3058
+ id: 'agent-1',
3059
+ pType: 'AGENT',
3060
+ hasLeft: false,
3061
+ consultState: 'consulting',
3062
+ ...WX_APP_DEVICE,
3063
+ };
3064
+ const ctx = createVoiceContext({
3065
+ taskData,
3066
+ uiControlConfig: {
3067
+ ...createVoiceContext().uiControlConfig,
3068
+ enableWxBetterTogether: true,
3069
+ },
3070
+ });
3071
+
3072
+ const controls = computeUIControls(TaskState.CONSULTING, ctx, taskData);
3073
+
3074
+ expect(controls.main.mute).toEqual({isVisible: true, isEnabled: true});
3075
+ expect(controls.main.keypad).toEqual({isVisible: true, isEnabled: true});
3076
+ expect(controls.consult.mute).toEqual({isVisible: false, isEnabled: false});
3077
+ });
3078
+
3079
+ it('keeps BROWSER consult mute enabled when enableWxBetterTogether is true and wxApp is not engaged', () => {
3080
+ const consultedTaskData = createConsultedAgentInconsistentTaskData();
3081
+ const consultedContext = createVoiceContext({
3082
+ consultInitiator: false,
3083
+ taskData: consultedTaskData,
3084
+ uiControlConfig: {
3085
+ ...createVoiceContext().uiControlConfig,
3086
+ agentId: 'agent-2',
3087
+ voiceVariant: VOICE_VARIANT.WEBRTC,
3088
+ enableWxBetterTogether: true,
3089
+ },
3090
+ });
3091
+
3092
+ const controls = computeUIControls(
3093
+ TaskState.CONNECTED,
3094
+ consultedContext,
3095
+ consultedTaskData
3096
+ );
3097
+
3098
+ expect(controls.main.mute).toEqual({isVisible: true, isEnabled: true});
3099
+ });
3100
+ });
3101
+ });