@webex/contact-center 3.12.0-next.9 → 3.12.0-next.91

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 (229) hide show
  1. package/.sdd/manifest.json +876 -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 +265 -29
  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 +17 -1
  44. package/dist/constants.js.map +1 -1
  45. package/dist/index.js +20 -5
  46. package/dist/index.js.map +1 -1
  47. package/dist/metrics/behavioral-events.js +101 -0
  48. package/dist/metrics/behavioral-events.js.map +1 -1
  49. package/dist/metrics/constants.js +25 -4
  50. package/dist/metrics/constants.js.map +1 -1
  51. package/dist/services/ApiAiAssistant.js +153 -8
  52. package/dist/services/ApiAiAssistant.js.map +1 -1
  53. package/dist/services/UserPreference.js +427 -0
  54. package/dist/services/UserPreference.js.map +1 -0
  55. package/dist/services/config/Util.js +3 -3
  56. package/dist/services/config/Util.js.map +1 -1
  57. package/dist/services/config/constants.js +23 -2
  58. package/dist/services/config/constants.js.map +1 -1
  59. package/dist/services/config/types.js +49 -9
  60. package/dist/services/config/types.js.map +1 -1
  61. package/dist/services/core/Err.js.map +1 -1
  62. package/dist/services/core/Utils.js +107 -32
  63. package/dist/services/core/Utils.js.map +1 -1
  64. package/dist/services/core/websocket/WebSocketManager.js +2 -1
  65. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  66. package/dist/services/core/websocket/types.js.map +1 -1
  67. package/dist/services/index.js +1 -1
  68. package/dist/services/index.js.map +1 -1
  69. package/dist/services/task/Task.js +688 -0
  70. package/dist/services/task/Task.js.map +1 -0
  71. package/dist/services/task/TaskFactory.js +45 -0
  72. package/dist/services/task/TaskFactory.js.map +1 -0
  73. package/dist/services/task/TaskManager.js +728 -527
  74. package/dist/services/task/TaskManager.js.map +1 -1
  75. package/dist/services/task/TaskUtils.js +162 -26
  76. package/dist/services/task/TaskUtils.js.map +1 -1
  77. package/dist/services/task/constants.js +9 -2
  78. package/dist/services/task/constants.js.map +1 -1
  79. package/dist/services/task/dialer.js +78 -0
  80. package/dist/services/task/dialer.js.map +1 -1
  81. package/dist/services/task/digital/Digital.js +77 -0
  82. package/dist/services/task/digital/Digital.js.map +1 -0
  83. package/dist/services/task/state-machine/TaskStateMachine.js +837 -0
  84. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  85. package/dist/services/task/state-machine/actions.js +543 -0
  86. package/dist/services/task/state-machine/actions.js.map +1 -0
  87. package/dist/services/task/state-machine/constants.js +161 -0
  88. package/dist/services/task/state-machine/constants.js.map +1 -0
  89. package/dist/services/task/state-machine/guards.js +340 -0
  90. package/dist/services/task/state-machine/guards.js.map +1 -0
  91. package/dist/services/task/state-machine/index.js +53 -0
  92. package/dist/services/task/state-machine/index.js.map +1 -0
  93. package/dist/services/task/state-machine/types.js +54 -0
  94. package/dist/services/task/state-machine/types.js.map +1 -0
  95. package/dist/services/task/state-machine/uiControlsComputer.js +553 -0
  96. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  97. package/dist/services/task/taskDataNormalizer.js +99 -0
  98. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  99. package/dist/services/task/types.js +212 -4
  100. package/dist/services/task/types.js.map +1 -1
  101. package/dist/services/task/voice/Voice.js +1042 -0
  102. package/dist/services/task/voice/Voice.js.map +1 -0
  103. package/dist/services/task/voice/WebRTC.js +149 -0
  104. package/dist/services/task/voice/WebRTC.js.map +1 -0
  105. package/dist/types/cc.d.ts +94 -1
  106. package/dist/types/config.d.ts +6 -0
  107. package/dist/types/constants.d.ts +17 -1
  108. package/dist/types/index.d.ts +21 -6
  109. package/dist/types/metrics/constants.d.ts +21 -1
  110. package/dist/types/services/ApiAiAssistant.d.ts +22 -4
  111. package/dist/types/services/UserPreference.d.ts +118 -0
  112. package/dist/types/services/config/constants.d.ts +21 -0
  113. package/dist/types/services/config/types.d.ts +171 -10
  114. package/dist/types/services/core/Err.d.ts +4 -0
  115. package/dist/types/services/core/Utils.d.ts +33 -13
  116. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
  117. package/dist/types/services/core/websocket/types.d.ts +1 -1
  118. package/dist/types/services/index.d.ts +1 -1
  119. package/dist/types/services/task/Task.d.ts +157 -0
  120. package/dist/types/services/task/TaskFactory.d.ts +12 -0
  121. package/dist/types/services/task/TaskUtils.d.ts +46 -2
  122. package/dist/types/services/task/constants.d.ts +7 -0
  123. package/dist/types/services/task/dialer.d.ts +30 -0
  124. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  125. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1144 -0
  126. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  127. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  128. package/dist/types/services/task/state-machine/guards.d.ts +90 -0
  129. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  130. package/dist/types/services/task/state-machine/types.d.ts +267 -0
  131. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  132. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  133. package/dist/types/services/task/types.d.ts +603 -66
  134. package/dist/types/services/task/voice/Voice.d.ts +184 -0
  135. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  136. package/dist/types/types.d.ts +135 -0
  137. package/dist/types/webex.d.ts +1 -0
  138. package/dist/types.js +118 -2
  139. package/dist/types.js.map +1 -1
  140. package/dist/webex.js +14 -2
  141. package/dist/webex.js.map +1 -1
  142. package/package.json +15 -12
  143. package/src/cc.ts +329 -30
  144. package/src/config.ts +6 -0
  145. package/src/constants.ts +17 -1
  146. package/src/index.ts +23 -5
  147. package/src/metrics/ai-docs/AGENTS.md +350 -0
  148. package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
  149. package/src/metrics/ai-docs/metrics-spec.md +854 -0
  150. package/src/metrics/behavioral-events.ts +106 -0
  151. package/src/metrics/constants.ts +27 -4
  152. package/src/services/ApiAiAssistant.ts +203 -8
  153. package/src/services/UserPreference.ts +509 -0
  154. package/src/services/agent/ai-docs/AGENTS.md +240 -0
  155. package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
  156. package/src/services/agent/ai-docs/agent-spec.md +504 -0
  157. package/src/services/ai-docs/AGENTS.md +386 -0
  158. package/src/services/ai-docs/services-spec.md +492 -0
  159. package/src/services/config/Util.ts +3 -3
  160. package/src/services/config/ai-docs/AGENTS.md +255 -0
  161. package/src/services/config/ai-docs/ARCHITECTURE.md +426 -0
  162. package/src/services/config/ai-docs/config-spec.md +669 -0
  163. package/src/services/config/constants.ts +25 -1
  164. package/src/services/config/types.ts +174 -11
  165. package/src/services/core/Err.ts +2 -0
  166. package/src/services/core/Utils.ts +123 -37
  167. package/src/services/core/ai-docs/AGENTS.md +381 -0
  168. package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
  169. package/src/services/core/ai-docs/core-spec.md +783 -0
  170. package/src/services/core/websocket/WebSocketManager.ts +2 -0
  171. package/src/services/core/websocket/types.ts +1 -1
  172. package/src/services/index.ts +1 -1
  173. package/src/services/task/Task.ts +837 -0
  174. package/src/services/task/TaskFactory.ts +55 -0
  175. package/src/services/task/TaskManager.ts +739 -614
  176. package/src/services/task/TaskUtils.ts +205 -25
  177. package/src/services/task/ai-docs/AGENTS.md +457 -0
  178. package/src/services/task/ai-docs/ARCHITECTURE.md +594 -0
  179. package/src/services/task/ai-docs/task-spec.md +1319 -0
  180. package/src/services/task/constants.ts +7 -0
  181. package/src/services/task/dialer.ts +80 -0
  182. package/src/services/task/digital/Digital.ts +95 -0
  183. package/src/services/task/state-machine/TaskStateMachine.ts +1077 -0
  184. package/src/services/task/state-machine/actions.ts +685 -0
  185. package/src/services/task/state-machine/ai-docs/AGENTS.md +458 -0
  186. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1137 -0
  187. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2115 -0
  188. package/src/services/task/state-machine/constants.ts +172 -0
  189. package/src/services/task/state-machine/guards.ts +406 -0
  190. package/src/services/task/state-machine/index.ts +28 -0
  191. package/src/services/task/state-machine/types.ts +241 -0
  192. package/src/services/task/state-machine/uiControlsComputer.ts +867 -0
  193. package/src/services/task/taskDataNormalizer.ts +137 -0
  194. package/src/services/task/types.ts +710 -71
  195. package/src/services/task/voice/Voice.ts +1267 -0
  196. package/src/services/task/voice/WebRTC.ts +187 -0
  197. package/src/types.ts +166 -2
  198. package/src/utils/AGENTS.md +278 -0
  199. package/src/utils/ai-docs/utils-spec.md +381 -0
  200. package/src/webex.js +2 -0
  201. package/test/unit/spec/cc.ts +343 -23
  202. package/test/unit/spec/logger-proxy.ts +70 -0
  203. package/test/unit/spec/services/ApiAiAssistant.ts +178 -20
  204. package/test/unit/spec/services/UserPreference.ts +401 -0
  205. package/test/unit/spec/services/WebCallingService.ts +7 -1
  206. package/test/unit/spec/services/config/index.ts +30 -30
  207. package/test/unit/spec/services/core/Utils.ts +425 -8
  208. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +66 -40
  209. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  210. package/test/unit/spec/services/task/Task.ts +477 -0
  211. package/test/unit/spec/services/task/TaskFactory.ts +62 -0
  212. package/test/unit/spec/services/task/TaskManager.ts +834 -1704
  213. package/test/unit/spec/services/task/TaskUtils.ts +206 -0
  214. package/test/unit/spec/services/task/dialer.ts +190 -0
  215. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  216. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +1825 -0
  217. package/test/unit/spec/services/task/state-machine/guards.ts +479 -0
  218. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  219. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +2020 -0
  220. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  221. package/test/unit/spec/services/task/voice/Voice.ts +631 -0
  222. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  223. package/umd/contact-center.min.js +2 -2
  224. package/umd/contact-center.min.js.map +1 -1
  225. package/dist/services/task/index.js +0 -1525
  226. package/dist/services/task/index.js.map +0 -1
  227. package/dist/types/services/task/index.d.ts +0 -650
  228. package/src/services/task/index.ts +0 -1801
  229. package/test/unit/spec/services/task/index.ts +0 -2184
@@ -0,0 +1,2020 @@
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
+ recordingControlsAvailable: true,
543
+ recordingInProgress: true,
544
+ uiControlConfig: {
545
+ isEndTaskEnabled: true,
546
+ isEndConsultEnabled: true,
547
+ channelType: TASK_CHANNEL_TYPE.VOICE,
548
+ isRecordingEnabled: true,
549
+ agentId: 'agent-1',
550
+ },
551
+ uiControls: getDefaultUIControls(),
552
+ ...overrides,
553
+ };
554
+ }
555
+
556
+ function createTaskData1LikeConferenceConsultTaskData() {
557
+ return createTaskData({
558
+ agentId: '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
559
+ mediaResourceId: '72402b8c-802d-4537-84d4-f244c3e586b1',
560
+ consultMediaResourceId: '66cc5edd-8ac9-4f27-8f48-286edea460b2',
561
+ consultingAgentId: '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
562
+ destAgentId: '747a2138-0a24-48fc-8d69-3a336d9b7158',
563
+ interaction: {
564
+ state: 'conference',
565
+ type: 'AgentConsulting',
566
+ interactionId: '72402b8c-802d-4537-84d4-f244c3e586b1',
567
+ mainInteractionId: '72402b8c-802d-4537-84d4-f244c3e586b1',
568
+ participants: {
569
+ '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9': {
570
+ id: '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
571
+ pType: 'Agent',
572
+ hasLeft: false,
573
+ consultState: 'consulting',
574
+ isConsulted: false,
575
+ },
576
+ '747a2138-0a24-48fc-8d69-3a336d9b7158': {
577
+ id: '747a2138-0a24-48fc-8d69-3a336d9b7158',
578
+ pType: 'Agent',
579
+ hasLeft: false,
580
+ consultState: 'consulting',
581
+ isConsulted: true,
582
+ },
583
+ 'e271075a-077d-42d0-9ae4-2a43cb847664': {
584
+ id: 'e271075a-077d-42d0-9ae4-2a43cb847664',
585
+ pType: 'Agent',
586
+ hasLeft: false,
587
+ consultState: 'conferencing',
588
+ isConsulted: false,
589
+ },
590
+ 'ed612aec-bafe-404d-b9b2-ea7de23a04f0': {
591
+ id: 'ed612aec-bafe-404d-b9b2-ea7de23a04f0',
592
+ pType: 'Agent',
593
+ hasLeft: false,
594
+ consultState: 'conferencing',
595
+ isConsulted: false,
596
+ },
597
+ '+14696762938': {id: '+14696762938', pType: 'Customer', hasLeft: false},
598
+ } as any,
599
+ media: {
600
+ '72402b8c-802d-4537-84d4-f244c3e586b1': {
601
+ mediaResourceId: '72402b8c-802d-4537-84d4-f244c3e586b1',
602
+ mType: 'mainCall',
603
+ isHold: true,
604
+ participants: [
605
+ '+14696762938',
606
+ 'e271075a-077d-42d0-9ae4-2a43cb847664',
607
+ 'ed612aec-bafe-404d-b9b2-ea7de23a04f0',
608
+ '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
609
+ ],
610
+ },
611
+ '66cc5edd-8ac9-4f27-8f48-286edea460b2': {
612
+ mediaResourceId: '66cc5edd-8ac9-4f27-8f48-286edea460b2',
613
+ mType: 'consult',
614
+ isHold: false,
615
+ participants: ['747a2138-0a24-48fc-8d69-3a336d9b7158', '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9'],
616
+ },
617
+ } as any,
618
+ owner: 'e271075a-077d-42d0-9ae4-2a43cb847664',
619
+ callProcessingDetails: {
620
+ conferenceHoldParticipant: 'true',
621
+ },
622
+ } as any,
623
+ });
624
+ }
625
+
626
+ describe('uiControlsComputer consult initiator controls', () => {
627
+ it('returns separate main and consult controls when consult leg is active', () => {
628
+ const context = createVoiceContext();
629
+
630
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, context.taskData);
631
+
632
+ expect(uiControls.activeLeg).toBe('consult');
633
+ expect(uiControls.consult).toBeDefined();
634
+
635
+ expect(uiControls.main.hold).toEqual({isVisible: false, isEnabled: false});
636
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: false});
637
+ expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: false});
638
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
639
+
640
+ expect(uiControls.consult.hold).toEqual({isVisible: false, isEnabled: false});
641
+ expect(uiControls.consult.mute).toEqual({isVisible: false, isEnabled: false});
642
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: true});
643
+ expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: true});
644
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
645
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: true});
646
+ });
647
+
648
+ it('switches top-level controls to main leg while keeping consult leg visible', () => {
649
+ const context = createVoiceContext({
650
+ consultCallHeld: true,
651
+ });
652
+
653
+ const uiControls = computeUIControls(TaskState.CONNECTED, context, context.taskData);
654
+
655
+ expect(uiControls.activeLeg).toBe('main');
656
+ expect(uiControls.consult).toBeDefined();
657
+
658
+ expect(uiControls.main.hold).toEqual({isVisible: false, isEnabled: false});
659
+ expect(uiControls.main.switch).toEqual({isVisible: true, isEnabled: true});
660
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
661
+ expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: true});
662
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
663
+
664
+ expect(uiControls.consult.hold).toEqual({isVisible: false, isEnabled: false});
665
+ expect(uiControls.consult.mute).toEqual({isVisible: false, isEnabled: false});
666
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false});
667
+ expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: false});
668
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
669
+ expect(uiControls.consult.end).toEqual({isVisible: false, isEnabled: false});
670
+ expect(uiControls.consult.switch).toEqual({isVisible: false, isEnabled: false});
671
+ });
672
+
673
+ it('enables mute only on active leg when main leg is active', () => {
674
+ const baseTaskData = createConsultTaskData();
675
+ const taskData = createTaskData({
676
+ ...baseTaskData,
677
+ interaction: {
678
+ ...baseTaskData.interaction,
679
+ media: {
680
+ ...baseTaskData.interaction.media,
681
+ 'consult-media': {
682
+ ...baseTaskData.interaction.media['consult-media'],
683
+ participants: ['agent-1', 'agent-2'],
684
+ },
685
+ },
686
+ },
687
+ });
688
+ const baseContext = createVoiceContext();
689
+ const context = createVoiceContext({
690
+ taskData,
691
+ consultCallHeld: true,
692
+ uiControlConfig: {
693
+ ...baseContext.uiControlConfig,
694
+ voiceVariant: VOICE_VARIANT.WEBRTC,
695
+ },
696
+ });
697
+
698
+ const uiControls = computeUIControls(TaskState.CONNECTED, context, context.taskData);
699
+
700
+ expect(uiControls.activeLeg).toBe('main');
701
+ expect(uiControls.main.mute).toEqual({isVisible: true, isEnabled: true});
702
+ expect(uiControls.consult.mute).toEqual({isVisible: true, isEnabled: false});
703
+ });
704
+
705
+ it('enables mute only on active leg when consult leg is active', () => {
706
+ const baseTaskData = createConsultTaskData();
707
+ const taskData = createTaskData({
708
+ ...baseTaskData,
709
+ interaction: {
710
+ ...baseTaskData.interaction,
711
+ media: {
712
+ ...baseTaskData.interaction.media,
713
+ 'consult-media': {
714
+ ...baseTaskData.interaction.media['consult-media'],
715
+ participants: ['agent-1', 'agent-2'],
716
+ },
717
+ },
718
+ },
719
+ });
720
+ const baseContext = createVoiceContext();
721
+ const context = createVoiceContext({
722
+ taskData,
723
+ consultCallHeld: false,
724
+ uiControlConfig: {
725
+ ...baseContext.uiControlConfig,
726
+ voiceVariant: VOICE_VARIANT.WEBRTC,
727
+ },
728
+ });
729
+
730
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, context.taskData);
731
+
732
+ expect(uiControls.activeLeg).toBe('consult');
733
+ expect(uiControls.main.mute).toEqual({isVisible: true, isEnabled: false});
734
+ expect(uiControls.consult.mute).toEqual({isVisible: true, isEnabled: true});
735
+ });
736
+
737
+ it('hides transfer for the consulted agent during consult', () => {
738
+ const consultedTaskData = createConsultTaskData();
739
+ const consultedContext = createVoiceContext({
740
+ consultInitiator: false,
741
+ taskData: {
742
+ ...consultedTaskData,
743
+ isConsulted: true,
744
+ } as any,
745
+ });
746
+
747
+ const uiControls = computeUIControls(
748
+ TaskState.CONSULTING,
749
+ consultedContext,
750
+ consultedContext.taskData
751
+ );
752
+
753
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
754
+ });
755
+
756
+ it('hides main-leg controls for consulted agent when payload isConsulted is stale false', () => {
757
+ const taskData = createConsultedAgentInconsistentTaskData();
758
+ const baseContext = createVoiceContext();
759
+ const consultedContext = createVoiceContext({
760
+ consultInitiator: false,
761
+ taskData,
762
+ uiControlConfig: {
763
+ ...baseContext.uiControlConfig,
764
+ agentId: 'agent-2',
765
+ },
766
+ });
767
+
768
+ const uiControls = computeUIControls(TaskState.CONNECTED, consultedContext, taskData);
769
+
770
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
771
+ expect(uiControls.main.end).toEqual({isVisible: false, isEnabled: false});
772
+ });
773
+
774
+ it('keeps consult-ring controls disabled except endConsult during hydrate pending state', () => {
775
+ const pendingTaskData = createPendingConsultHydrateTaskData();
776
+ const context = createVoiceContext({
777
+ taskData: pendingTaskData as any,
778
+ consultInitiator: false,
779
+ consultFromConference: false,
780
+ consultDestinationAgentJoined: false,
781
+ consultCallHeld: false,
782
+ });
783
+
784
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, pendingTaskData as any);
785
+
786
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: false});
787
+ expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: false});
788
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
789
+
790
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false});
791
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: false});
792
+ expect(uiControls.consult.mergeToConference).toEqual({isVisible: true, isEnabled: false});
793
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
794
+ });
795
+
796
+ it('collapses stale consult leg controls during wrapup', () => {
797
+ const context = createVoiceContext();
798
+
799
+ const uiControls = computeUIControls(TaskState.WRAPPING_UP, context, context.taskData);
800
+
801
+ expect(uiControls.activeLeg).toBe('main');
802
+ expect(uiControls.main.wrapup).toEqual({isVisible: true, isEnabled: true});
803
+ expect(uiControls.consult).toEqual(getDefaultUIControls().consult);
804
+ });
805
+
806
+ it('disables consult for non-initiators when another agent has active consult', () => {
807
+ const taskData = createConferenceWithOtherAgentConsultPendingTaskData();
808
+ const baseContext = createVoiceContext();
809
+ const context = createVoiceContext({
810
+ consultInitiator: false,
811
+ consultFromConference: false,
812
+ taskData,
813
+ uiControlConfig: {
814
+ ...baseContext.uiControlConfig,
815
+ agentId: 'agent-2',
816
+ },
817
+ });
818
+
819
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
820
+
821
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
822
+ });
823
+
824
+ it('enables end and exitConference for non-initiators on held main leg while another agent consults', () => {
825
+ const taskData = createHeldConferenceWithActiveConsultTaskData();
826
+ const baseContext = createVoiceContext();
827
+ const context = createVoiceContext({
828
+ consultInitiator: false,
829
+ taskData,
830
+ uiControlConfig: {
831
+ ...baseContext.uiControlConfig,
832
+ agentId: 'agent-2',
833
+ },
834
+ });
835
+
836
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
837
+
838
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
839
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
840
+ });
841
+
842
+ it('enables end and exitConference on held main leg when consultInitiator flag is stale true', () => {
843
+ const taskData = createHeldConferenceWithActiveConsultTaskData();
844
+ const baseContext = createVoiceContext();
845
+ const context = createVoiceContext({
846
+ consultInitiator: true,
847
+ taskData,
848
+ uiControlConfig: {
849
+ ...baseContext.uiControlConfig,
850
+ agentId: 'agent-2',
851
+ },
852
+ });
853
+
854
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
855
+
856
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
857
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
858
+ });
859
+
860
+ it('enables end and exitConference on unheld main leg for non-initiator during active conference consult', () => {
861
+ const taskData = createUnheldConferenceWithActiveConsultTaskData();
862
+ const baseContext = createVoiceContext();
863
+ const context = createVoiceContext({
864
+ consultInitiator: false,
865
+ taskData,
866
+ uiControlConfig: {
867
+ ...baseContext.uiControlConfig,
868
+ agentId: 'agent-2',
869
+ },
870
+ });
871
+
872
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
873
+
874
+ expect(uiControls.activeLeg).toBe('main');
875
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
876
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
877
+ });
878
+
879
+ it('keeps consult disabled for non-owner after owner consult completes in multi-agent conference', () => {
880
+ const taskData = createPostConsultCompletedMultiAgentTaskData('agent-2');
881
+ const baseContext = createVoiceContext();
882
+ const context = createVoiceContext({
883
+ consultInitiator: false,
884
+ taskData,
885
+ uiControlConfig: {
886
+ ...baseContext.uiControlConfig,
887
+ agentId: 'agent-2',
888
+ },
889
+ });
890
+
891
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
892
+
893
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
894
+ });
895
+
896
+ it('keeps consult enabled for owner after consult completes in multi-agent conference', () => {
897
+ const taskData = createPostConsultCompletedMultiAgentTaskData('agent-1');
898
+ const baseContext = createVoiceContext();
899
+ const context = createVoiceContext({
900
+ consultInitiator: false,
901
+ taskData,
902
+ uiControlConfig: {
903
+ ...baseContext.uiControlConfig,
904
+ agentId: 'agent-1',
905
+ },
906
+ });
907
+
908
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
909
+
910
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
911
+ });
912
+
913
+ it('hides transfer and enables transferConference on consult leg for conference initiator', () => {
914
+ const taskData = createConferenceConsultingInitiatorTaskData();
915
+ const baseContext = createVoiceContext();
916
+ const context = createVoiceContext({
917
+ consultInitiator: true,
918
+ consultFromConference: true,
919
+ consultDestinationAgentJoined: true,
920
+ consultCallHeld: false,
921
+ taskData,
922
+ uiControlConfig: {
923
+ ...baseContext.uiControlConfig,
924
+ agentId: 'agent-1',
925
+ },
926
+ });
927
+
928
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
929
+
930
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
931
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
932
+ expect(uiControls.main.transferConference).toEqual({isVisible: true, isEnabled: false});
933
+ });
934
+
935
+ it('keeps consult leg controls active and main leg conference controls disabled on hydrate-like context', () => {
936
+ const taskData = createConferenceConsultingInitiatorTaskData();
937
+ const baseContext = createVoiceContext();
938
+ const context = createVoiceContext({
939
+ consultInitiator: true,
940
+ consultFromConference: true,
941
+ consultDestinationAgentJoined: true,
942
+ consultCallHeld: false,
943
+ taskData,
944
+ uiControlConfig: {
945
+ ...baseContext.uiControlConfig,
946
+ agentId: 'agent-1',
947
+ },
948
+ });
949
+
950
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
951
+
952
+ expect(uiControls.activeLeg).toBe('consult');
953
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
954
+ expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: false});
955
+ expect(uiControls.main.transferConference).toEqual({isVisible: true, isEnabled: false});
956
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: true});
957
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
958
+ expect(uiControls.consult.mergeToConference).toEqual({isVisible: true, isEnabled: true});
959
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
960
+ });
961
+
962
+ it('enables consult leg controls for conference DN AgentConsulting after destination joined', () => {
963
+ const taskData = createTaskData({
964
+ agentId: 'agent-1',
965
+ consultingAgentId: 'agent-1',
966
+ consultMediaResourceId: 'consult-media-1',
967
+ isConsulted: false,
968
+ destinationType: 'DN',
969
+ type: 'AgentConsulting' as any,
970
+ interaction: {
971
+ state: 'conference',
972
+ interactionId: 'interaction-1',
973
+ mainInteractionId: 'interaction-1',
974
+ owner: 'agent-1',
975
+ participants: {
976
+ 'agent-1': {
977
+ id: 'agent-1',
978
+ pType: 'Agent',
979
+ hasLeft: false,
980
+ consultState: 'consulting',
981
+ isConsulted: false,
982
+ },
983
+ 'agent-2': {
984
+ id: 'agent-2',
985
+ pType: 'Agent',
986
+ hasLeft: false,
987
+ consultState: 'conferencing',
988
+ },
989
+ 'dn-dest': {
990
+ id: 'dn-dest',
991
+ pType: 'DN',
992
+ hasLeft: false,
993
+ hasJoined: true,
994
+ },
995
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
996
+ } as any,
997
+ media: {
998
+ 'interaction-1': {
999
+ mediaResourceId: 'interaction-1',
1000
+ mType: 'mainCall',
1001
+ participants: ['agent-1', 'agent-2', 'customer-1'],
1002
+ isHold: false,
1003
+ },
1004
+ 'consult-media-1': {
1005
+ mediaResourceId: 'consult-media-1',
1006
+ mType: 'consult',
1007
+ participants: ['agent-1', 'dn-dest'],
1008
+ isHold: false,
1009
+ },
1010
+ } as any,
1011
+ } as any,
1012
+ });
1013
+ const baseContext = createVoiceContext();
1014
+ const context = createVoiceContext({
1015
+ consultInitiator: true,
1016
+ consultFromConference: true,
1017
+ consultDestinationAgentJoined: true,
1018
+ consultDestinationType: 'entryPoint',
1019
+ consultCallHeld: false,
1020
+ taskData,
1021
+ uiControlConfig: {
1022
+ ...baseContext.uiControlConfig,
1023
+ agentId: 'agent-1',
1024
+ },
1025
+ });
1026
+
1027
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
1028
+
1029
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: true});
1030
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
1031
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
1032
+ expect(uiControls.consult.mergeToConference).toEqual({isVisible: true, isEnabled: true});
1033
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
1034
+ });
1035
+
1036
+ it('keeps transferConference visible on consult leg for initiator even when state is conferencing', () => {
1037
+ const taskData = createConferenceConsultingInitiatorTaskData();
1038
+ const baseContext = createVoiceContext();
1039
+ const context = createVoiceContext({
1040
+ consultInitiator: true,
1041
+ consultFromConference: true,
1042
+ consultDestinationAgentJoined: true,
1043
+ consultCallHeld: false,
1044
+ taskData,
1045
+ uiControlConfig: {
1046
+ ...baseContext.uiControlConfig,
1047
+ agentId: 'agent-1',
1048
+ },
1049
+ });
1050
+
1051
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
1052
+
1053
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
1054
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
1055
+ });
1056
+
1057
+ it('shows transferConference for initiator in AgentContactUnheld-style conference consult context', () => {
1058
+ const taskData = createAgentContactUnheldInitiatorConsultTaskData();
1059
+ const baseContext = createVoiceContext();
1060
+ const context = createVoiceContext({
1061
+ consultInitiator: true,
1062
+ consultFromConference: false,
1063
+ consultDestinationAgentJoined: true,
1064
+ consultCallHeld: false,
1065
+ taskData,
1066
+ uiControlConfig: {
1067
+ ...baseContext.uiControlConfig,
1068
+ agentId: 'agent-1',
1069
+ },
1070
+ });
1071
+
1072
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
1073
+
1074
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
1075
+ });
1076
+
1077
+ it('shows transferConference on main leg when initiator has active conference consult', () => {
1078
+ const taskData = createConferenceConsultingInitiatorTaskData();
1079
+ const baseContext = createVoiceContext();
1080
+ const context = createVoiceContext({
1081
+ consultInitiator: true,
1082
+ consultFromConference: true,
1083
+ consultDestinationAgentJoined: true,
1084
+ consultCallHeld: true,
1085
+ taskData,
1086
+ uiControlConfig: {
1087
+ ...baseContext.uiControlConfig,
1088
+ agentId: 'agent-1',
1089
+ },
1090
+ });
1091
+
1092
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
1093
+
1094
+ expect(uiControls.activeLeg).toBe('main');
1095
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
1096
+ expect(uiControls.main.transferConference).toEqual({isVisible: true, isEnabled: true});
1097
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: false});
1098
+ });
1099
+
1100
+ it('keeps transferConference visible on consult leg for taskData1-style payload', () => {
1101
+ const taskData = createTaskData1LikeConferenceConsultTaskData();
1102
+ const baseContext = createVoiceContext();
1103
+ const context = createVoiceContext({
1104
+ consultInitiator: true,
1105
+ consultFromConference: true,
1106
+ consultDestinationAgentJoined: true,
1107
+ consultCallHeld: false,
1108
+ taskData,
1109
+ uiControlConfig: {
1110
+ ...baseContext.uiControlConfig,
1111
+ agentId: '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
1112
+ },
1113
+ });
1114
+
1115
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
1116
+
1117
+ expect(uiControls.activeLeg).toBe('consult');
1118
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
1119
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
1120
+ });
1121
+
1122
+ it('enables transferConference after AgentConsulting follows AgentConsultCreated for initiator', () => {
1123
+ const baseContext = createVoiceContext();
1124
+ const createdTaskData = createConferenceConsultInitiatedInitiatorTaskData();
1125
+ const createdContext = createVoiceContext({
1126
+ consultInitiator: true,
1127
+ consultFromConference: true,
1128
+ consultDestinationAgentJoined: false,
1129
+ consultCallHeld: false,
1130
+ taskData: createdTaskData,
1131
+ uiControlConfig: {
1132
+ ...baseContext.uiControlConfig,
1133
+ agentId: 'agent-1',
1134
+ },
1135
+ });
1136
+ const createdControls = computeUIControls(TaskState.CONSULTING, createdContext, createdTaskData);
1137
+
1138
+ expect(createdControls.consult.transferConference).toEqual({isVisible: true, isEnabled: false});
1139
+
1140
+ const consultingTaskData = createConferenceConsultingInitiatorTaskData();
1141
+ const consultingContext = createVoiceContext({
1142
+ ...createdContext,
1143
+ taskData: consultingTaskData,
1144
+ consultDestinationAgentJoined: true,
1145
+ });
1146
+ const consultingControls = computeUIControls(
1147
+ TaskState.CONSULTING,
1148
+ consultingContext,
1149
+ consultingTaskData
1150
+ );
1151
+
1152
+ expect(consultingControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
1153
+ });
1154
+
1155
+ it('hides exitConference on main leg for consult initiator before destination joins', () => {
1156
+ const taskData = createConferenceConsultInitiatedInitiatorTaskData();
1157
+ const baseContext = createVoiceContext();
1158
+ const context = createVoiceContext({
1159
+ consultInitiator: true,
1160
+ consultFromConference: true,
1161
+ consultDestinationAgentJoined: false,
1162
+ consultCallHeld: false,
1163
+ taskData,
1164
+ uiControlConfig: {
1165
+ ...baseContext.uiControlConfig,
1166
+ agentId: 'agent-1',
1167
+ },
1168
+ });
1169
+
1170
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
1171
+
1172
+ expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
1173
+ });
1174
+
1175
+ it('hides exitConference on main leg while consulting and destination has not joined', () => {
1176
+ const taskData = createConferenceConsultInitiatedInitiatorTaskData();
1177
+ const baseContext = createVoiceContext();
1178
+ const context = createVoiceContext({
1179
+ consultInitiator: true,
1180
+ consultFromConference: true,
1181
+ consultDestinationAgentJoined: false,
1182
+ consultCallHeld: false,
1183
+ taskData,
1184
+ uiControlConfig: {
1185
+ ...baseContext.uiControlConfig,
1186
+ agentId: 'agent-1',
1187
+ },
1188
+ });
1189
+
1190
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
1191
+
1192
+ expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
1193
+ });
1194
+
1195
+ it('hides exitConference on main leg for pending self consult even with stale initiator flags', () => {
1196
+ const taskData = createConferenceConsultInitiatedInitiatorTaskData();
1197
+ const baseContext = createVoiceContext();
1198
+ const context = createVoiceContext({
1199
+ consultInitiator: false,
1200
+ consultFromConference: false,
1201
+ consultDestinationAgentJoined: false,
1202
+ consultCallHeld: false,
1203
+ taskData,
1204
+ uiControlConfig: {
1205
+ ...baseContext.uiControlConfig,
1206
+ agentId: 'agent-1',
1207
+ },
1208
+ });
1209
+
1210
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
1211
+
1212
+ expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
1213
+ });
1214
+
1215
+ function createSimpleHeldConsultInitiatedTaskData() {
1216
+ return createTaskData({
1217
+ agentId: 'agent-1',
1218
+ mediaResourceId: 'interaction-1',
1219
+ consultMediaResourceId: 'consult-media',
1220
+ destAgentId: 'agent-2',
1221
+ destinationType: 'Agent',
1222
+ isConsulted: false,
1223
+ type: 'AgentConsultCreated' as any,
1224
+ interaction: {
1225
+ state: 'consult',
1226
+ interactionId: 'interaction-1',
1227
+ mainInteractionId: 'interaction-1',
1228
+ owner: 'agent-1',
1229
+ participants: {
1230
+ 'agent-1': {
1231
+ id: 'agent-1',
1232
+ pType: 'Agent',
1233
+ hasLeft: false,
1234
+ consultState: 'consultInitiated',
1235
+ isConsulted: false,
1236
+ },
1237
+ 'agent-2': {
1238
+ id: 'agent-2',
1239
+ pType: 'Agent',
1240
+ hasLeft: false,
1241
+ hasJoined: false,
1242
+ consultState: 'consultReserved',
1243
+ isConsulted: true,
1244
+ },
1245
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1246
+ } as any,
1247
+ media: {
1248
+ 'interaction-1': {
1249
+ mediaResourceId: 'interaction-1',
1250
+ mType: 'mainCall',
1251
+ isHold: true,
1252
+ participants: ['customer-1', 'agent-1'],
1253
+ },
1254
+ 'consult-media': {
1255
+ mediaResourceId: 'consult-media',
1256
+ mType: 'consult',
1257
+ isHold: false,
1258
+ participants: ['agent-2', 'agent-1'],
1259
+ },
1260
+ } as any,
1261
+ } as any,
1262
+ });
1263
+ }
1264
+
1265
+ function createConsultFailedHeldMainTaskData() {
1266
+ return createTaskData({
1267
+ agentId: 'agent-1',
1268
+ mediaResourceId: 'interaction-1',
1269
+ consultMediaResourceId: 'consult-media',
1270
+ destAgentId: 'agent-2',
1271
+ destinationType: 'Agent',
1272
+ isConsulted: false,
1273
+ type: 'AgentConsultFailed' as any,
1274
+ interaction: {
1275
+ state: 'consult',
1276
+ interactionId: 'interaction-1',
1277
+ mainInteractionId: 'interaction-1',
1278
+ owner: 'agent-1',
1279
+ participants: {
1280
+ 'agent-1': {
1281
+ id: 'agent-1',
1282
+ pType: 'Agent',
1283
+ hasLeft: false,
1284
+ consultState: 'consultCompleted',
1285
+ isConsulted: false,
1286
+ },
1287
+ 'agent-2': {
1288
+ id: 'agent-2',
1289
+ pType: 'Agent',
1290
+ hasLeft: false,
1291
+ hasJoined: false,
1292
+ consultState: 'consultReserved',
1293
+ isConsulted: true,
1294
+ },
1295
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1296
+ } as any,
1297
+ media: {
1298
+ 'interaction-1': {
1299
+ mediaResourceId: 'interaction-1',
1300
+ mType: 'mainCall',
1301
+ isHold: true,
1302
+ participants: ['customer-1', 'agent-1'],
1303
+ },
1304
+ 'consult-media': {
1305
+ mediaResourceId: 'consult-media',
1306
+ mType: 'consult',
1307
+ isHold: false,
1308
+ participants: ['agent-2', 'agent-1'],
1309
+ },
1310
+ } as any,
1311
+ } as any,
1312
+ });
1313
+ }
1314
+
1315
+ it('infers HELD (not CONSULTING) while consultee has not joined', () => {
1316
+ const taskData = createSimpleHeldConsultInitiatedTaskData();
1317
+
1318
+ expect(getTaskStateForUiControls(taskData as any, 'agent-1')).toBe(TaskState.HELD);
1319
+ });
1320
+
1321
+ it('matches Stable Prod consult-requested controls for AgentConsultCreated while HELD', () => {
1322
+ const taskData = createSimpleHeldConsultInitiatedTaskData();
1323
+ const context = createVoiceContext({
1324
+ taskData: taskData as any,
1325
+ consultInitiator: true,
1326
+ consultDestinationAgentJoined: false,
1327
+ consultCallHeld: false,
1328
+ });
1329
+
1330
+ const uiControls = computeUIControls(TaskState.HELD, context, taskData as any);
1331
+
1332
+ expect(uiControls.activeLeg).toBe('consult');
1333
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: false});
1334
+ expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: false});
1335
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
1336
+ expect(uiControls.main.hold).toEqual({isVisible: false, isEnabled: false});
1337
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
1338
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: false});
1339
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false});
1340
+ expect(uiControls.consult.mergeToConference).toEqual({isVisible: true, isEnabled: false});
1341
+ });
1342
+
1343
+ it('clears consult leg and restores HELD main controls after AgentConsultEnded from Stable Prod', () => {
1344
+ const taskData = createTaskData({
1345
+ agentId: 'agent-1',
1346
+ mediaResourceId: 'interaction-1',
1347
+ destAgentId: 'agent-2',
1348
+ destinationType: 'Agent',
1349
+ isConsulted: false,
1350
+ type: 'AgentConsultEnded' as any,
1351
+ interaction: {
1352
+ state: 'connected',
1353
+ interactionId: 'interaction-1',
1354
+ mainInteractionId: 'interaction-1',
1355
+ owner: 'agent-1',
1356
+ participants: {
1357
+ 'agent-1': {
1358
+ id: 'agent-1',
1359
+ pType: 'Agent',
1360
+ hasLeft: false,
1361
+ consultState: 'consultCompleted',
1362
+ isConsulted: false,
1363
+ },
1364
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1365
+ } as any,
1366
+ media: {
1367
+ 'interaction-1': {
1368
+ mediaResourceId: 'interaction-1',
1369
+ mType: 'mainCall',
1370
+ isHold: true,
1371
+ holdTimestamp: 1780495564872,
1372
+ participants: ['customer-1', 'agent-1'],
1373
+ },
1374
+ } as any,
1375
+ } as any,
1376
+ });
1377
+ const staleContext = createVoiceContext({
1378
+ taskData: {
1379
+ ...(taskData as any),
1380
+ interaction: {
1381
+ ...(taskData as any).interaction,
1382
+ media: {
1383
+ ...(taskData as any).interaction.media,
1384
+ 'consult-media': {
1385
+ mediaResourceId: 'consult-media',
1386
+ mType: 'consult',
1387
+ isHold: false,
1388
+ participants: ['agent-2', 'agent-1'],
1389
+ },
1390
+ },
1391
+ },
1392
+ },
1393
+ consultInitiator: true,
1394
+ consultDestinationAgentJoined: true,
1395
+ consultCallHeld: false,
1396
+ });
1397
+
1398
+ const uiControls = computeUIControls(TaskState.CONSULTING, staleContext, taskData as any);
1399
+
1400
+ expect(uiControls.activeLeg).toBe('main');
1401
+ expect(uiControls.consult.endConsult).toEqual({isVisible: false, isEnabled: false});
1402
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: true});
1403
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1404
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
1405
+ expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: true});
1406
+ });
1407
+
1408
+ it('enables switch, transfer, and merge on consult leg after AgentConsulting accept', () => {
1409
+ const taskData = createTaskData({
1410
+ agentId: 'agent-1',
1411
+ mediaResourceId: 'interaction-1',
1412
+ consultMediaResourceId: 'consult-media',
1413
+ consultingAgentId: 'agent-1',
1414
+ destAgentId: 'agent-2',
1415
+ isConsulted: false,
1416
+ type: 'AgentConsulting' as any,
1417
+ interaction: {
1418
+ state: 'consulting',
1419
+ interactionId: 'interaction-1',
1420
+ mainInteractionId: 'interaction-1',
1421
+ owner: 'agent-1',
1422
+ participants: {
1423
+ 'agent-1': {
1424
+ id: 'agent-1',
1425
+ pType: 'Agent',
1426
+ hasLeft: false,
1427
+ consultState: 'consulting',
1428
+ isConsulted: false,
1429
+ },
1430
+ 'agent-2': {
1431
+ id: 'agent-2',
1432
+ pType: 'Agent',
1433
+ hasLeft: false,
1434
+ hasJoined: true,
1435
+ consultState: 'consulting',
1436
+ isConsulted: true,
1437
+ },
1438
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1439
+ } as any,
1440
+ media: {
1441
+ 'interaction-1': {
1442
+ mediaResourceId: 'interaction-1',
1443
+ mType: 'mainCall',
1444
+ isHold: true,
1445
+ participants: ['customer-1', 'agent-1'],
1446
+ },
1447
+ 'consult-media': {
1448
+ mediaResourceId: 'consult-media',
1449
+ mType: 'consult',
1450
+ isHold: false,
1451
+ participants: ['agent-2', 'agent-1'],
1452
+ },
1453
+ } as any,
1454
+ } as any,
1455
+ });
1456
+ const context = createVoiceContext({
1457
+ taskData: taskData as any,
1458
+ consultInitiator: true,
1459
+ consultDestinationAgentJoined: true,
1460
+ consultCallHeld: false,
1461
+ });
1462
+
1463
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData as any);
1464
+
1465
+ expect(uiControls.activeLeg).toBe('consult');
1466
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: true});
1467
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: true});
1468
+ expect(uiControls.consult.mergeToConference).toEqual({isVisible: true, isEnabled: true});
1469
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
1470
+ });
1471
+
1472
+ it('shows RONA failure controls after AgentConsultFailed while HELD', () => {
1473
+ const taskData = createConsultFailedHeldMainTaskData();
1474
+ const context = createVoiceContext({
1475
+ taskData: taskData as any,
1476
+ consultInitiator: false,
1477
+ consultDestinationAgentJoined: false,
1478
+ consultCallHeld: false,
1479
+ });
1480
+
1481
+ const uiControls = computeUIControls(TaskState.HELD, context, taskData as any);
1482
+
1483
+ expect(uiControls.activeLeg).toBe('main');
1484
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: true});
1485
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
1486
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1487
+ expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: true});
1488
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
1489
+ expect(uiControls.main.conference).toEqual({isVisible: false, isEnabled: false});
1490
+ expect(uiControls.consult.endConsult).toEqual({isVisible: false, isEnabled: false});
1491
+ });
1492
+
1493
+ it('enables main.consult on AgentConsultFailed while consult media remains and destinationJoined is stale', () => {
1494
+ const taskData = createConsultFailedHeldMainTaskData();
1495
+ const context = createVoiceContext({
1496
+ taskData: taskData as any,
1497
+ consultInitiator: true,
1498
+ consultDestinationAgentJoined: true,
1499
+ consultCallHeld: false,
1500
+ });
1501
+
1502
+ const uiControls = computeUIControls(TaskState.HELD, context, taskData as any);
1503
+
1504
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1505
+ });
1506
+
1507
+ it('enables main.consult when consultCompleted with stale consultMediaResourceId only (RONA cleanup)', () => {
1508
+ const taskData = createTaskData({
1509
+ agentId: 'agent-1',
1510
+ mediaResourceId: 'interaction-1',
1511
+ consultMediaResourceId: 'consult-media',
1512
+ isConsulted: false,
1513
+ type: 'AgentConsultEnded' as any,
1514
+ interaction: {
1515
+ state: 'connected',
1516
+ interactionId: 'interaction-1',
1517
+ mainInteractionId: 'interaction-1',
1518
+ owner: 'agent-1',
1519
+ participants: {
1520
+ 'agent-1': {
1521
+ id: 'agent-1',
1522
+ pType: 'Agent',
1523
+ hasLeft: false,
1524
+ consultState: 'consultCompleted',
1525
+ isConsulted: false,
1526
+ },
1527
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1528
+ } as any,
1529
+ media: {
1530
+ 'interaction-1': {
1531
+ mediaResourceId: 'interaction-1',
1532
+ mType: 'mainCall',
1533
+ isHold: true,
1534
+ participants: ['customer-1', 'agent-1'],
1535
+ },
1536
+ } as any,
1537
+ } as any,
1538
+ });
1539
+ const context = createVoiceContext({
1540
+ taskData: taskData as any,
1541
+ consultInitiator: true,
1542
+ consultDestinationAgentJoined: true,
1543
+ consultCallHeld: false,
1544
+ });
1545
+
1546
+ const uiControls = computeUIControls(TaskState.CONSULT_INITIATING, context, taskData as any);
1547
+
1548
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1549
+ });
1550
+
1551
+ it('enables main.consult after AgentConsultFailed then AgentConsultEnded on held main leg (RONA)', () => {
1552
+ const consultEndedTaskData = createTaskData({
1553
+ agentId: 'agent-1',
1554
+ mediaResourceId: 'interaction-1',
1555
+ consultMediaResourceId: 'consult-media',
1556
+ destAgentId: 'agent-2',
1557
+ destinationType: 'Agent',
1558
+ isConsulted: false,
1559
+ type: 'AgentConsultEnded' as any,
1560
+ interaction: {
1561
+ state: 'connected',
1562
+ interactionId: 'interaction-1',
1563
+ mainInteractionId: 'interaction-1',
1564
+ owner: 'agent-1',
1565
+ participants: {
1566
+ 'agent-1': {
1567
+ id: 'agent-1',
1568
+ pType: 'Agent',
1569
+ hasLeft: false,
1570
+ consultState: 'consultCompleted',
1571
+ isConsulted: false,
1572
+ },
1573
+ 'agent-2': {
1574
+ id: 'agent-2',
1575
+ pType: 'Agent',
1576
+ hasLeft: false,
1577
+ hasJoined: false,
1578
+ consultState: 'consultReserved',
1579
+ isConsulted: true,
1580
+ },
1581
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1582
+ } as any,
1583
+ media: {
1584
+ 'interaction-1': {
1585
+ mediaResourceId: 'interaction-1',
1586
+ mType: 'mainCall',
1587
+ isHold: true,
1588
+ participants: ['customer-1', 'agent-1'],
1589
+ },
1590
+ } as any,
1591
+ } as any,
1592
+ });
1593
+ const staleContext = createVoiceContext({
1594
+ taskData: consultEndedTaskData as any,
1595
+ consultInitiator: true,
1596
+ consultDestinationAgentJoined: false,
1597
+ consultCallHeld: false,
1598
+ consultFromConference: false,
1599
+ });
1600
+
1601
+ const uiControls = computeUIControls(TaskState.HELD, staleContext, consultEndedTaskData as any);
1602
+
1603
+ expect(uiControls.activeLeg).toBe('main');
1604
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1605
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: true});
1606
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
1607
+ });
1608
+
1609
+ it('enables main.consult after AgentConsultEnded when consult ended before consultee answered (held main leg)', () => {
1610
+ // Agent 1 initiates consult to Agent 2 and ends it before Agent 2 answers.
1611
+ // Backend AgentConsultEnded: main on hold, self consultCompleted, no consult media,
1612
+ // and the consultee (destAgent) is not present in the participants map.
1613
+ const consultEndedTaskData = createTaskData({
1614
+ agentId: 'agent-1',
1615
+ mediaResourceId: 'interaction-1',
1616
+ consultMediaResourceId: 'consult-media',
1617
+ destAgentId: 'agent-2',
1618
+ destinationType: 'Agent',
1619
+ isConsulted: false,
1620
+ type: 'AgentConsultEnded' as any,
1621
+ interaction: {
1622
+ state: 'connected',
1623
+ interactionId: 'interaction-1',
1624
+ mainInteractionId: 'interaction-1',
1625
+ owner: 'agent-1',
1626
+ participants: {
1627
+ 'agent-1': {
1628
+ id: 'agent-1',
1629
+ pType: 'Agent',
1630
+ hasLeft: false,
1631
+ hasJoined: true,
1632
+ consultState: 'consultCompleted',
1633
+ isConsulted: false,
1634
+ },
1635
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1636
+ } as any,
1637
+ media: {
1638
+ 'interaction-1': {
1639
+ mediaResourceId: 'interaction-1',
1640
+ mType: 'mainCall',
1641
+ isHold: true,
1642
+ participants: ['customer-1', 'agent-1'],
1643
+ },
1644
+ } as any,
1645
+ } as any,
1646
+ });
1647
+ // Stale context from CONSULT_INITIATING: initiator true, consultee never joined.
1648
+ const staleContext = createVoiceContext({
1649
+ taskData: consultEndedTaskData as any,
1650
+ consultInitiator: true,
1651
+ consultDestinationAgentJoined: false,
1652
+ consultCallHeld: false,
1653
+ consultFromConference: false,
1654
+ });
1655
+
1656
+ const uiControls = computeUIControls(TaskState.HELD, staleContext, consultEndedTaskData as any);
1657
+
1658
+ expect(uiControls.activeLeg).toBe('main');
1659
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1660
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: true});
1661
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
1662
+ expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: true});
1663
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
1664
+ expect(uiControls.consult.endConsult).toEqual({isVisible: false, isEnabled: false});
1665
+ });
1666
+
1667
+ it('enables main.consult after AgentConsultEnded even when reconciled task.data still carries stale consult media/participant', () => {
1668
+ // Reproduces the real runtime data: Task.reconcileData deep-merges and never deletes keys,
1669
+ // so after AgentConsultEnded the stale consult-media entry and consultee participant from
1670
+ // AgentConsultCreated persist in task.data. The consult button must still be enabled.
1671
+ const consultEndedTaskData = createTaskData({
1672
+ agentId: 'agent-1',
1673
+ mediaResourceId: 'interaction-1',
1674
+ consultMediaResourceId: 'consult-media',
1675
+ destAgentId: 'agent-2',
1676
+ destinationType: 'Agent',
1677
+ isConsulted: false,
1678
+ type: 'AgentConsultEnded' as any,
1679
+ interaction: {
1680
+ state: 'connected',
1681
+ interactionId: 'interaction-1',
1682
+ mainInteractionId: 'interaction-1',
1683
+ owner: 'agent-1',
1684
+ participants: {
1685
+ 'agent-1': {
1686
+ id: 'agent-1',
1687
+ pType: 'Agent',
1688
+ hasLeft: false,
1689
+ hasJoined: true,
1690
+ consultState: 'consultCompleted',
1691
+ isConsulted: false,
1692
+ },
1693
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false, hasJoined: true},
1694
+ // Stale consultee retained by reconcileData (never joined, consult leg gone).
1695
+ 'agent-2': {
1696
+ id: 'agent-2',
1697
+ pType: 'Agent',
1698
+ hasLeft: false,
1699
+ hasJoined: false,
1700
+ consultState: 'consulting',
1701
+ isConsulted: true,
1702
+ },
1703
+ } as any,
1704
+ media: {
1705
+ 'interaction-1': {
1706
+ mediaResourceId: 'interaction-1',
1707
+ mType: 'mainCall',
1708
+ isHold: true,
1709
+ participants: ['customer-1', 'agent-1'],
1710
+ },
1711
+ // Stale consult media retained by reconcileData merge.
1712
+ 'consult-media': {
1713
+ mediaResourceId: 'consult-media',
1714
+ mType: 'consult',
1715
+ isHold: false,
1716
+ participants: ['agent-1', 'agent-2'],
1717
+ },
1718
+ } as any,
1719
+ } as any,
1720
+ });
1721
+ const staleContext = createVoiceContext({
1722
+ taskData: consultEndedTaskData as any,
1723
+ consultInitiator: true,
1724
+ consultDestinationAgentJoined: false,
1725
+ consultCallHeld: false,
1726
+ consultFromConference: false,
1727
+ });
1728
+
1729
+ const uiControls = computeUIControls(TaskState.HELD, staleContext, consultEndedTaskData as any);
1730
+
1731
+ expect(uiControls.activeLeg).toBe('main');
1732
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1733
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: true});
1734
+ expect(uiControls.consult.endConsult).toEqual({isVisible: false, isEnabled: false});
1735
+ });
1736
+
1737
+ });
1738
+
1739
+ describe('uiControlsComputer outdial accept/decline controls', () => {
1740
+ function createOutdialContext(voiceVariant: 'webrtc' | 'pstn' = 'webrtc'): TaskContext {
1741
+ const taskData = createTaskData({
1742
+ interaction: {
1743
+ outboundType: 'OUTDIAL',
1744
+ state: 'new',
1745
+ isTerminated: false,
1746
+ } as any,
1747
+ });
1748
+ return {
1749
+ taskData,
1750
+ consultInitiator: false,
1751
+ exitingConference: false,
1752
+ consultFromConference: false,
1753
+ transferConferenceRequested: false,
1754
+ consultDestinationType: null,
1755
+ consultDestinationAgentId: null,
1756
+ consultDestinationAgentJoined: false,
1757
+ consultCallHeld: false,
1758
+ recordingControlsAvailable: false,
1759
+ recordingInProgress: false,
1760
+ uiControlConfig: {
1761
+ isEndTaskEnabled: true,
1762
+ isEndConsultEnabled: true,
1763
+ channelType: TASK_CHANNEL_TYPE.VOICE,
1764
+ isRecordingEnabled: false,
1765
+ agentId: 'agent-1',
1766
+ voiceVariant,
1767
+ },
1768
+ uiControls: getDefaultUIControls(),
1769
+ };
1770
+ }
1771
+
1772
+ it('accept is visible but disabled for WebRTC outdial in OFFERED state', () => {
1773
+ const context = createOutdialContext('webrtc');
1774
+ const uiControls = computeUIControls(TaskState.OFFERED, context, context.taskData);
1775
+
1776
+ expect(uiControls.main.accept).toEqual({isVisible: true, isEnabled: false});
1777
+ });
1778
+
1779
+ it('decline is visible but disabled for WebRTC outdial in OFFERED state', () => {
1780
+ const context = createOutdialContext('webrtc');
1781
+ const uiControls = computeUIControls(TaskState.OFFERED, context, context.taskData);
1782
+
1783
+ expect(uiControls.main.decline).toEqual({isVisible: true, isEnabled: false});
1784
+ });
1785
+ });
1786
+
1787
+ describe('uiControlsComputer WebRTC call-control end button', () => {
1788
+ function createConnectedContext(voiceVariant: 'webrtc' | 'pstn'): TaskContext {
1789
+ const taskData = createTaskData({
1790
+ interaction: {
1791
+ state: 'connected',
1792
+ } as any,
1793
+ });
1794
+
1795
+ return {
1796
+ taskData,
1797
+ consultInitiator: false,
1798
+ exitingConference: false,
1799
+ consultFromConference: false,
1800
+ transferConferenceRequested: false,
1801
+ consultDestinationType: null,
1802
+ consultDestinationAgentId: null,
1803
+ consultDestinationAgentJoined: false,
1804
+ consultCallHeld: false,
1805
+ recordingControlsAvailable: true,
1806
+ recordingInProgress: true,
1807
+ uiControlConfig: {
1808
+ isEndTaskEnabled: false,
1809
+ isEndConsultEnabled: true,
1810
+ channelType: TASK_CHANNEL_TYPE.VOICE,
1811
+ isRecordingEnabled: true,
1812
+ agentId: taskData.agentId,
1813
+ voiceVariant,
1814
+ },
1815
+ uiControls: getDefaultUIControls(),
1816
+ };
1817
+ }
1818
+
1819
+ it('shows main end for connected WebRTC call-control tasks even when end task is disabled', () => {
1820
+ const context = createConnectedContext('webrtc');
1821
+ const uiControls = computeUIControls(TaskState.CONNECTED, context, context.taskData);
1822
+
1823
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
1824
+ });
1825
+
1826
+ it('keeps main end hidden for non-WebRTC voice tasks when end task is disabled', () => {
1827
+ const context = createConnectedContext('pstn');
1828
+ const uiControls = computeUIControls(TaskState.CONNECTED, context, context.taskData);
1829
+
1830
+ expect(uiControls.main.end).toEqual({isVisible: false, isEnabled: false});
1831
+ });
1832
+ });
1833
+
1834
+ describe('uiControlsComputer conference controls', () => {
1835
+ function createConferenceTaskData(participantCount: number) {
1836
+ const participants: Record<string, any> = {
1837
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: false},
1838
+ 'agent-1': {
1839
+ id: 'agent-1',
1840
+ pType: 'Agent',
1841
+ hasJoined: true,
1842
+ hasLeft: false,
1843
+ consultState: 'conferencing',
1844
+ },
1845
+ };
1846
+ const mainCallParticipants = ['customer-1', 'agent-1'];
1847
+
1848
+ if (participantCount > 1) {
1849
+ participants['agent-2'] = {
1850
+ id: 'agent-2',
1851
+ pType: 'Agent',
1852
+ hasJoined: true,
1853
+ hasLeft: false,
1854
+ consultState: 'conferencing',
1855
+ };
1856
+ mainCallParticipants.push('agent-2');
1857
+ }
1858
+
1859
+ return createTaskData({
1860
+ agentId: 'agent-1',
1861
+ mediaResourceId: 'interaction-1',
1862
+ consultMediaResourceId: '' as any,
1863
+ interaction: {
1864
+ interactionId: 'interaction-1',
1865
+ mainInteractionId: 'interaction-1',
1866
+ state: 'conference',
1867
+ participants,
1868
+ media: {
1869
+ 'interaction-1': {
1870
+ mediaResourceId: 'interaction-1',
1871
+ mType: 'mainCall',
1872
+ isHold: false,
1873
+ participants: mainCallParticipants,
1874
+ },
1875
+ },
1876
+ } as any,
1877
+ });
1878
+ }
1879
+
1880
+ function createConferenceContext(participantCount: number, overrides: Partial<TaskContext> = {}): TaskContext {
1881
+ return {
1882
+ taskData: createConferenceTaskData(participantCount),
1883
+ consultInitiator: true,
1884
+ exitingConference: false,
1885
+ consultFromConference: false,
1886
+ transferConferenceRequested: false,
1887
+ consultDestinationType: null,
1888
+ consultDestinationAgentId: null,
1889
+ consultDestinationAgentJoined: false,
1890
+ consultCallHeld: false,
1891
+ recordingControlsAvailable: true,
1892
+ recordingInProgress: true,
1893
+ uiControlConfig: {
1894
+ isEndTaskEnabled: true,
1895
+ isEndConsultEnabled: true,
1896
+ channelType: TASK_CHANNEL_TYPE.VOICE,
1897
+ isRecordingEnabled: true,
1898
+ agentId: 'agent-1',
1899
+ },
1900
+ uiControls: getDefaultUIControls(),
1901
+ ...overrides,
1902
+ };
1903
+ }
1904
+
1905
+ it('pending conference (participantCount <= 1): transfer/recording enabled, consult disabled, exitConference hidden', () => {
1906
+ const context = createConferenceContext(1);
1907
+
1908
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, context.taskData);
1909
+
1910
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
1911
+ expect(uiControls.main.recording).toEqual({isVisible: true, isEnabled: true});
1912
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
1913
+ expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
1914
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
1915
+ });
1916
+
1917
+ it('real conference (participantCount > 1): transfer/recording hidden, consult enabled, exitConference visible', () => {
1918
+ const context = createConferenceContext(2);
1919
+
1920
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, context.taskData);
1921
+
1922
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
1923
+ expect(uiControls.main.recording).toEqual({isVisible: false, isEnabled: false});
1924
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
1925
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
1926
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
1927
+ });
1928
+
1929
+ it('hold button shows visible but disabled in conference', () => {
1930
+ const context = createConferenceContext(2);
1931
+
1932
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, context.taskData);
1933
+
1934
+ expect(uiControls.main.hold).toEqual({isVisible: true, isEnabled: false});
1935
+ });
1936
+ });
1937
+
1938
+ describe('uiControlsComputer post-call consult controls (customer left)', () => {
1939
+ function createPostCallConsultTaskData() {
1940
+ return createTaskData({
1941
+ agentId: 'agent-1',
1942
+ mediaResourceId: 'interaction-1',
1943
+ consultMediaResourceId: 'consult-media',
1944
+ consultingAgentId: 'agent-1',
1945
+ destAgentId: 'agent-2',
1946
+ interaction: {
1947
+ interactionId: 'interaction-1',
1948
+ mainInteractionId: 'interaction-1',
1949
+ state: 'post_call',
1950
+ isTerminated: false,
1951
+ participants: {
1952
+ 'agent-1': {
1953
+ id: 'agent-1',
1954
+ pType: 'Agent',
1955
+ hasJoined: true,
1956
+ hasLeft: false,
1957
+ consultState: 'consulting',
1958
+ },
1959
+ 'agent-2': {
1960
+ id: 'agent-2',
1961
+ pType: 'Agent',
1962
+ hasLeft: false,
1963
+ consultState: 'consulting',
1964
+ isConsulted: true,
1965
+ },
1966
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasJoined: true, hasLeft: true},
1967
+ } as any,
1968
+ media: {
1969
+ 'interaction-1': {
1970
+ mediaResourceId: 'interaction-1',
1971
+ mType: 'mainCall',
1972
+ isHold: false,
1973
+ participants: ['agent-1'],
1974
+ },
1975
+ 'consult-media': {
1976
+ mediaResourceId: 'consult-media',
1977
+ mType: 'consult',
1978
+ isHold: false,
1979
+ participants: ['agent-1', 'agent-2'],
1980
+ },
1981
+ } as any,
1982
+ } as any,
1983
+ });
1984
+ }
1985
+
1986
+ it('consult controls are visible but disabled when customer has left', () => {
1987
+ const taskData = createPostCallConsultTaskData();
1988
+ const context: TaskContext = {
1989
+ taskData,
1990
+ consultInitiator: true,
1991
+ exitingConference: false,
1992
+ consultFromConference: false,
1993
+ transferConferenceRequested: false,
1994
+ consultDestinationType: null,
1995
+ consultDestinationAgentId: null,
1996
+ consultDestinationAgentJoined: true,
1997
+ consultCallHeld: false,
1998
+ recordingControlsAvailable: true,
1999
+ recordingInProgress: true,
2000
+ uiControlConfig: {
2001
+ isEndTaskEnabled: true,
2002
+ isEndConsultEnabled: true,
2003
+ channelType: TASK_CHANNEL_TYPE.VOICE,
2004
+ isRecordingEnabled: true,
2005
+ agentId: 'agent-1',
2006
+ },
2007
+ uiControls: getDefaultUIControls(),
2008
+ };
2009
+
2010
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, context.taskData);
2011
+
2012
+ // Consult leg: transfer/conference/merge/switch should be visible but disabled
2013
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false});
2014
+ expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: false});
2015
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: false});
2016
+
2017
+ // Main leg end should be visible but disabled
2018
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
2019
+ });
2020
+ });