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

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