@webex/contact-center 3.12.0-task-refactor.4 → 3.12.0-task-refactor.6

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 (35) hide show
  1. package/dist/services/task/TaskManager.js +1 -0
  2. package/dist/services/task/TaskManager.js.map +1 -1
  3. package/dist/services/task/TaskUtils.js +8 -6
  4. package/dist/services/task/TaskUtils.js.map +1 -1
  5. package/dist/services/task/state-machine/TaskStateMachine.js +77 -14
  6. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  7. package/dist/services/task/state-machine/actions.js +85 -13
  8. package/dist/services/task/state-machine/actions.js.map +1 -1
  9. package/dist/services/task/state-machine/guards.js +35 -0
  10. package/dist/services/task/state-machine/guards.js.map +1 -1
  11. package/dist/services/task/state-machine/uiControlsComputer.js +76 -10
  12. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  13. package/dist/services/task/voice/Voice.js +10 -4
  14. package/dist/services/task/voice/Voice.js.map +1 -1
  15. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +68 -8
  16. package/dist/types/services/task/state-machine/guards.d.ts +5 -0
  17. package/dist/types/services/task/voice/Voice.d.ts +18 -17
  18. package/dist/webex.js +1 -1
  19. package/package.json +1 -1
  20. package/src/services/task/TaskManager.ts +1 -1
  21. package/src/services/task/TaskUtils.ts +8 -6
  22. package/src/services/task/state-machine/TaskStateMachine.ts +101 -16
  23. package/src/services/task/state-machine/actions.ts +148 -24
  24. package/src/services/task/state-machine/guards.ts +46 -0
  25. package/src/services/task/state-machine/uiControlsComputer.ts +158 -15
  26. package/src/services/task/voice/Voice.ts +12 -5
  27. package/test/unit/spec/services/WebCallingService.ts +7 -1
  28. package/test/unit/spec/services/task/TaskManager.ts +26 -0
  29. package/test/unit/spec/services/task/TaskUtils.ts +16 -0
  30. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +573 -0
  31. package/test/unit/spec/services/task/state-machine/guards.ts +88 -0
  32. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +1023 -46
  33. package/test/unit/spec/services/task/voice/Voice.ts +44 -0
  34. package/umd/contact-center.min.js +2 -2
  35. package/umd/contact-center.min.js.map +1 -1
@@ -46,6 +46,487 @@ function createConsultTaskData() {
46
46
  });
47
47
  }
48
48
 
49
+ function createConsultedAgentInconsistentTaskData() {
50
+ return createTaskData({
51
+ agentId: 'agent-2',
52
+ mediaResourceId: 'interaction-1',
53
+ consultMediaResourceId: 'consult-media',
54
+ consultingAgentId: 'agent-1',
55
+ isConsulted: false,
56
+ interaction: {
57
+ interactionId: 'interaction-1',
58
+ mainInteractionId: 'interaction-1',
59
+ participants: {
60
+ 'agent-1': {id: 'agent-1', pType: 'AGENT', hasLeft: false},
61
+ 'agent-2': {
62
+ id: 'agent-2',
63
+ pType: 'AGENT',
64
+ hasLeft: false,
65
+ consultState: 'consulting',
66
+ currentState: 'consulting',
67
+ isConsulted: true,
68
+ },
69
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
70
+ } as any,
71
+ media: {
72
+ 'interaction-1': {
73
+ mediaResourceId: 'interaction-1',
74
+ mType: 'mainCall',
75
+ isHold: false,
76
+ participants: ['agent-1', 'customer-1'],
77
+ },
78
+ 'consult-media': {
79
+ mediaResourceId: 'consult-media',
80
+ mType: 'consult',
81
+ isHold: false,
82
+ participants: ['agent-1', 'agent-2'],
83
+ },
84
+ } as any,
85
+ } as any,
86
+ });
87
+ }
88
+
89
+ function createPendingConsultHydrateTaskData() {
90
+ return createTaskData({
91
+ agentId: 'agent-1',
92
+ mediaResourceId: 'interaction-1',
93
+ consultMediaResourceId: 'consult-media',
94
+ isConsulted: false,
95
+ interaction: {
96
+ state: 'conference',
97
+ interactionId: 'interaction-1',
98
+ mainInteractionId: 'interaction-1',
99
+ participants: {
100
+ 'agent-1': {
101
+ id: 'agent-1',
102
+ pType: 'AGENT',
103
+ hasLeft: false,
104
+ consultState: 'consultInitiated',
105
+ isConsulted: false,
106
+ },
107
+ 'agent-2': {
108
+ id: 'agent-2',
109
+ pType: 'AGENT',
110
+ hasLeft: false,
111
+ hasJoined: false,
112
+ consultState: 'consultReserved',
113
+ isConsulted: true,
114
+ },
115
+ 'agent-3': {id: 'agent-3', pType: 'AGENT', hasLeft: false, consultState: 'conferencing'},
116
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
117
+ } as any,
118
+ media: {
119
+ 'interaction-1': {
120
+ mediaResourceId: 'interaction-1',
121
+ isHold: true,
122
+ mType: 'mainCall',
123
+ participants: ['agent-1', 'agent-3', 'customer-1'],
124
+ },
125
+ 'consult-media': {
126
+ mediaResourceId: 'consult-media',
127
+ isHold: false,
128
+ mType: 'consult',
129
+ participants: ['agent-1', 'agent-2'],
130
+ },
131
+ } as any,
132
+ callProcessingDetails: {
133
+ conferenceHoldParticipant: 'true',
134
+ },
135
+ } as any,
136
+ });
137
+ }
138
+
139
+ function createHeldConferenceWithActiveConsultTaskData() {
140
+ return createTaskData({
141
+ agentId: 'agent-2',
142
+ mediaResourceId: 'interaction-1',
143
+ consultMediaResourceId: 'consult-media',
144
+ consultingAgentId: 'agent-1',
145
+ isConsulted: false,
146
+ interaction: {
147
+ state: 'conference',
148
+ interactionId: 'interaction-1',
149
+ mainInteractionId: 'interaction-1',
150
+ participants: {
151
+ 'agent-1': {
152
+ id: 'agent-1',
153
+ pType: 'AGENT',
154
+ hasLeft: false,
155
+ consultState: 'consulting',
156
+ isConsulted: false,
157
+ },
158
+ 'agent-2': {
159
+ id: 'agent-2',
160
+ pType: 'AGENT',
161
+ hasLeft: false,
162
+ consultState: 'conferencing',
163
+ isConsulted: false,
164
+ },
165
+ 'agent-3': {
166
+ id: 'agent-3',
167
+ pType: 'AGENT',
168
+ hasLeft: false,
169
+ consultState: 'consultReserved',
170
+ isConsulted: true,
171
+ },
172
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
173
+ } as any,
174
+ media: {
175
+ 'interaction-1': {
176
+ mediaResourceId: 'interaction-1',
177
+ isHold: true,
178
+ mType: 'mainCall',
179
+ participants: ['agent-1', 'agent-2', 'customer-1'],
180
+ },
181
+ 'consult-media': {
182
+ mediaResourceId: 'consult-media',
183
+ isHold: false,
184
+ mType: 'consult',
185
+ participants: ['agent-1', 'agent-3'],
186
+ },
187
+ } as any,
188
+ } as any,
189
+ });
190
+ }
191
+
192
+ function createUnheldConferenceWithActiveConsultTaskData() {
193
+ return createTaskData({
194
+ agentId: 'agent-2',
195
+ mediaResourceId: 'interaction-1',
196
+ isConsulted: false,
197
+ interaction: {
198
+ state: 'conference',
199
+ type: 'AgentContactUnheld',
200
+ interactionId: 'interaction-1',
201
+ mainInteractionId: 'interaction-1',
202
+ owner: 'agent-5',
203
+ participants: {
204
+ 'agent-2': {
205
+ id: 'agent-2',
206
+ pType: 'AGENT',
207
+ hasLeft: false,
208
+ consultState: 'conferencing',
209
+ isConsulted: false,
210
+ },
211
+ 'agent-5': {
212
+ id: 'agent-5',
213
+ pType: 'AGENT',
214
+ hasLeft: false,
215
+ consultState: 'consulting',
216
+ isConsulted: false,
217
+ },
218
+ 'agent-15': {
219
+ id: 'agent-15',
220
+ pType: 'AGENT',
221
+ hasLeft: false,
222
+ consultState: 'consulting',
223
+ isConsulted: true,
224
+ },
225
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
226
+ } as any,
227
+ media: {
228
+ 'interaction-1': {
229
+ mediaResourceId: 'interaction-1',
230
+ isHold: false,
231
+ mType: 'mainCall',
232
+ participants: ['customer-1', 'agent-5', 'agent-2'],
233
+ },
234
+ 'consult-media': {
235
+ mediaResourceId: 'consult-media',
236
+ isHold: true,
237
+ mType: 'consult',
238
+ participants: ['agent-5', 'agent-15'],
239
+ },
240
+ } as any,
241
+ } as any,
242
+ });
243
+ }
244
+
245
+ function createConferenceWithOtherAgentConsultPendingTaskData() {
246
+ return createTaskData({
247
+ agentId: 'agent-2',
248
+ mediaResourceId: 'interaction-1',
249
+ consultMediaResourceId: 'consult-media',
250
+ isConsulted: false,
251
+ interaction: {
252
+ state: 'conference',
253
+ interactionId: 'interaction-1',
254
+ mainInteractionId: 'interaction-1',
255
+ participants: {
256
+ 'agent-1': {
257
+ id: 'agent-1',
258
+ pType: 'AGENT',
259
+ hasLeft: false,
260
+ consultState: 'consultInitiated',
261
+ isConsulted: false,
262
+ },
263
+ 'agent-2': {
264
+ id: 'agent-2',
265
+ pType: 'AGENT',
266
+ hasLeft: false,
267
+ consultState: 'conferencing',
268
+ isConsulted: false,
269
+ },
270
+ 'agent-3': {
271
+ id: 'agent-3',
272
+ pType: 'AGENT',
273
+ hasLeft: false,
274
+ consultState: 'consultReserved',
275
+ isConsulted: true,
276
+ },
277
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
278
+ } as any,
279
+ media: {
280
+ 'interaction-1': {
281
+ mediaResourceId: 'interaction-1',
282
+ isHold: false,
283
+ mType: 'mainCall',
284
+ participants: ['agent-1', 'agent-2', 'customer-1'],
285
+ },
286
+ 'consult-media': {
287
+ mediaResourceId: 'consult-media',
288
+ isHold: false,
289
+ mType: 'consult',
290
+ participants: ['agent-1', 'agent-3'],
291
+ },
292
+ } as any,
293
+ } as any,
294
+ });
295
+ }
296
+
297
+ function createPostConsultCompletedMultiAgentTaskData(agentId: string) {
298
+ return createTaskData({
299
+ agentId,
300
+ mediaResourceId: 'interaction-1',
301
+ consultMediaResourceId: null as any,
302
+ isConsulted: false,
303
+ interaction: {
304
+ state: 'conference',
305
+ interactionId: 'interaction-1',
306
+ mainInteractionId: 'interaction-1',
307
+ owner: 'agent-1',
308
+ participants: {
309
+ 'agent-1': {
310
+ id: 'agent-1',
311
+ pType: 'AGENT',
312
+ hasLeft: false,
313
+ consultState: 'consultCompleted',
314
+ isConsulted: false,
315
+ },
316
+ 'agent-2': {
317
+ id: 'agent-2',
318
+ pType: 'AGENT',
319
+ hasLeft: false,
320
+ consultState: 'conferencing',
321
+ isConsulted: false,
322
+ },
323
+ 'agent-3': {
324
+ id: 'agent-3',
325
+ pType: 'AGENT',
326
+ hasLeft: false,
327
+ consultState: 'conferencing',
328
+ isConsulted: false,
329
+ },
330
+ 'agent-4': {
331
+ id: 'agent-4',
332
+ pType: 'AGENT',
333
+ hasLeft: false,
334
+ consultState: 'conferencing',
335
+ isConsulted: false,
336
+ },
337
+ 'agent-5': {
338
+ id: 'agent-5',
339
+ pType: 'AGENT',
340
+ hasLeft: false,
341
+ consultState: 'conferencing',
342
+ isConsulted: false,
343
+ },
344
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
345
+ } as any,
346
+ media: {
347
+ 'interaction-1': {
348
+ mediaResourceId: 'interaction-1',
349
+ isHold: true,
350
+ mType: 'mainCall',
351
+ participants: ['agent-1', 'agent-2', 'agent-3', 'agent-4', 'agent-5', 'customer-1'],
352
+ },
353
+ } as any,
354
+ } as any,
355
+ });
356
+ }
357
+
358
+ function createConferenceConsultingInitiatorTaskData() {
359
+ return createTaskData({
360
+ agentId: 'agent-1',
361
+ mediaResourceId: 'interaction-1',
362
+ consultMediaResourceId: 'consult-media',
363
+ consultingAgentId: 'agent-1',
364
+ isConsulted: false,
365
+ interaction: {
366
+ state: 'conference',
367
+ interactionId: 'interaction-1',
368
+ mainInteractionId: 'interaction-1',
369
+ owner: 'agent-1',
370
+ participants: {
371
+ 'agent-1': {
372
+ id: 'agent-1',
373
+ pType: 'AGENT',
374
+ hasLeft: false,
375
+ consultState: 'consulting',
376
+ isConsulted: false,
377
+ },
378
+ 'agent-2': {
379
+ id: 'agent-2',
380
+ pType: 'AGENT',
381
+ hasLeft: false,
382
+ consultState: 'conferencing',
383
+ isConsulted: false,
384
+ },
385
+ 'agent-4': {
386
+ id: 'agent-4',
387
+ pType: 'AGENT',
388
+ hasLeft: false,
389
+ consultState: 'consultReserved',
390
+ isConsulted: true,
391
+ },
392
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
393
+ } as any,
394
+ media: {
395
+ 'interaction-1': {
396
+ mediaResourceId: 'interaction-1',
397
+ isHold: true,
398
+ mType: 'mainCall',
399
+ participants: ['agent-1', 'agent-2', 'customer-1'],
400
+ },
401
+ 'consult-media': {
402
+ mediaResourceId: 'consult-media',
403
+ isHold: false,
404
+ mType: 'consult',
405
+ participants: ['agent-1', 'agent-4'],
406
+ },
407
+ } as any,
408
+ } as any,
409
+ });
410
+ }
411
+
412
+ function createConferenceConsultInitiatedInitiatorTaskData() {
413
+ return createTaskData({
414
+ agentId: 'agent-1',
415
+ mediaResourceId: 'interaction-1',
416
+ consultMediaResourceId: 'consult-media',
417
+ destAgentId: 'agent-4',
418
+ destinationType: 'Agent',
419
+ isConsulted: false,
420
+ type: 'AgentConsultCreated' as any,
421
+ interaction: {
422
+ state: 'conference',
423
+ interactionId: 'interaction-1',
424
+ mainInteractionId: 'interaction-1',
425
+ owner: 'agent-5',
426
+ participants: {
427
+ 'agent-1': {
428
+ id: 'agent-1',
429
+ pType: 'AGENT',
430
+ hasLeft: false,
431
+ consultState: 'consultInitiated',
432
+ isConsulted: false,
433
+ },
434
+ 'agent-2': {
435
+ id: 'agent-2',
436
+ pType: 'AGENT',
437
+ hasLeft: false,
438
+ consultState: 'conferencing',
439
+ isConsulted: false,
440
+ },
441
+ 'agent-4': {
442
+ id: 'agent-4',
443
+ pType: 'AGENT',
444
+ hasLeft: false,
445
+ hasJoined: false,
446
+ consultState: null,
447
+ isConsulted: true,
448
+ },
449
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
450
+ } as any,
451
+ media: {
452
+ 'interaction-1': {
453
+ mediaResourceId: 'interaction-1',
454
+ isHold: true,
455
+ mType: 'mainCall',
456
+ participants: ['agent-1', 'agent-2', 'customer-1'],
457
+ },
458
+ 'consult-media': {
459
+ mediaResourceId: 'consult-media',
460
+ isHold: false,
461
+ mType: 'consult',
462
+ participants: ['agent-1', 'agent-4'],
463
+ },
464
+ } as any,
465
+ } as any,
466
+ });
467
+ }
468
+
469
+ function createAgentContactUnheldInitiatorConsultTaskData() {
470
+ return createTaskData({
471
+ agentId: 'agent-1',
472
+ mediaResourceId: 'interaction-1',
473
+ consultMediaResourceId: 'consult-media',
474
+ destAgentId: null as any,
475
+ isConsulted: false,
476
+ interaction: {
477
+ state: 'conference',
478
+ interactionId: 'interaction-1',
479
+ mainInteractionId: 'interaction-1',
480
+ owner: 'agent-5',
481
+ participants: {
482
+ 'agent-1': {
483
+ id: 'agent-1',
484
+ pType: 'AGENT',
485
+ hasLeft: false,
486
+ consultState: 'consulting',
487
+ isConsulted: false,
488
+ },
489
+ 'agent-15': {
490
+ id: 'agent-15',
491
+ pType: 'AGENT',
492
+ hasLeft: false,
493
+ consultState: 'consulting',
494
+ isConsulted: true,
495
+ },
496
+ 'agent-5': {
497
+ id: 'agent-5',
498
+ pType: 'AGENT',
499
+ hasLeft: false,
500
+ consultState: 'conferencing',
501
+ isConsulted: false,
502
+ },
503
+ 'agent-3': {
504
+ id: 'agent-3',
505
+ pType: 'AGENT',
506
+ hasLeft: false,
507
+ consultState: 'conferencing',
508
+ isConsulted: false,
509
+ },
510
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
511
+ } as any,
512
+ media: {
513
+ 'interaction-1': {
514
+ mediaResourceId: 'interaction-1',
515
+ isHold: true,
516
+ mType: 'mainCall',
517
+ participants: ['customer-1', 'agent-5', 'agent-3', 'agent-1'],
518
+ },
519
+ 'consult-media': {
520
+ mediaResourceId: 'consult-media',
521
+ isHold: false,
522
+ mType: 'consult',
523
+ participants: ['agent-15', 'agent-1'],
524
+ },
525
+ } as any,
526
+ } as any,
527
+ });
528
+ }
529
+
49
530
  function createVoiceContext(overrides: Partial<TaskContext> = {}): TaskContext {
50
531
  return {
51
532
  taskData: createConsultTaskData(),
@@ -71,78 +552,574 @@ function createVoiceContext(overrides: Partial<TaskContext> = {}): TaskContext {
71
552
  };
72
553
  }
73
554
 
74
- describe('uiControlsComputer consult initiator controls', () => {
75
- it('returns separate main and consult controls when consult leg is active', () => {
76
- const context = createVoiceContext();
555
+ function createTaskData1LikeConferenceConsultTaskData() {
556
+ return createTaskData({
557
+ agentId: '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
558
+ mediaResourceId: '72402b8c-802d-4537-84d4-f244c3e586b1',
559
+ consultMediaResourceId: '66cc5edd-8ac9-4f27-8f48-286edea460b2',
560
+ consultingAgentId: '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
561
+ destAgentId: '747a2138-0a24-48fc-8d69-3a336d9b7158',
562
+ interaction: {
563
+ state: 'conference',
564
+ type: 'AgentConsulting',
565
+ interactionId: '72402b8c-802d-4537-84d4-f244c3e586b1',
566
+ mainInteractionId: '72402b8c-802d-4537-84d4-f244c3e586b1',
567
+ participants: {
568
+ '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9': {
569
+ id: '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
570
+ pType: 'Agent',
571
+ hasLeft: false,
572
+ consultState: 'consulting',
573
+ isConsulted: false,
574
+ },
575
+ '747a2138-0a24-48fc-8d69-3a336d9b7158': {
576
+ id: '747a2138-0a24-48fc-8d69-3a336d9b7158',
577
+ pType: 'Agent',
578
+ hasLeft: false,
579
+ consultState: 'consulting',
580
+ isConsulted: true,
581
+ },
582
+ 'e271075a-077d-42d0-9ae4-2a43cb847664': {
583
+ id: 'e271075a-077d-42d0-9ae4-2a43cb847664',
584
+ pType: 'Agent',
585
+ hasLeft: false,
586
+ consultState: 'conferencing',
587
+ isConsulted: false,
588
+ },
589
+ 'ed612aec-bafe-404d-b9b2-ea7de23a04f0': {
590
+ id: 'ed612aec-bafe-404d-b9b2-ea7de23a04f0',
591
+ pType: 'Agent',
592
+ hasLeft: false,
593
+ consultState: 'conferencing',
594
+ isConsulted: false,
595
+ },
596
+ '+14696762938': {id: '+14696762938', pType: 'Customer', hasLeft: false},
597
+ } as any,
598
+ media: {
599
+ '72402b8c-802d-4537-84d4-f244c3e586b1': {
600
+ mediaResourceId: '72402b8c-802d-4537-84d4-f244c3e586b1',
601
+ mType: 'mainCall',
602
+ isHold: true,
603
+ participants: [
604
+ '+14696762938',
605
+ 'e271075a-077d-42d0-9ae4-2a43cb847664',
606
+ 'ed612aec-bafe-404d-b9b2-ea7de23a04f0',
607
+ '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
608
+ ],
609
+ },
610
+ '66cc5edd-8ac9-4f27-8f48-286edea460b2': {
611
+ mediaResourceId: '66cc5edd-8ac9-4f27-8f48-286edea460b2',
612
+ mType: 'consult',
613
+ isHold: false,
614
+ participants: ['747a2138-0a24-48fc-8d69-3a336d9b7158', '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9'],
615
+ },
616
+ } as any,
617
+ owner: 'e271075a-077d-42d0-9ae4-2a43cb847664',
618
+ callProcessingDetails: {
619
+ conferenceHoldParticipant: 'true',
620
+ },
621
+ } as any,
622
+ });
623
+ }
624
+
625
+ describe('uiControlsComputer consult initiator controls', () => {
626
+ it('returns separate main and consult controls when consult leg is active', () => {
627
+ const context = createVoiceContext();
628
+
629
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, context.taskData);
630
+
631
+ expect(uiControls.activeLeg).toBe('consult');
632
+ expect(uiControls.consult).toBeDefined();
633
+
634
+ expect(uiControls.main.hold).toEqual({isVisible: false, isEnabled: false});
635
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: false});
636
+ expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: false});
637
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
638
+
639
+ expect(uiControls.consult.hold).toEqual({isVisible: false, isEnabled: false});
640
+ expect(uiControls.consult.mute).toEqual({isVisible: false, isEnabled: false});
641
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: true});
642
+ expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: true});
643
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
644
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: true});
645
+ });
646
+
647
+ it('switches top-level controls to main leg while keeping consult leg visible', () => {
648
+ const context = createVoiceContext({
649
+ consultCallHeld: true,
650
+ });
651
+
652
+ const uiControls = computeUIControls(TaskState.CONNECTED, context, context.taskData);
653
+
654
+ expect(uiControls.activeLeg).toBe('main');
655
+ expect(uiControls.consult).toBeDefined();
656
+
657
+ expect(uiControls.main.hold).toEqual({isVisible: false, isEnabled: false});
658
+ expect(uiControls.main.switch).toEqual({isVisible: true, isEnabled: true});
659
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
660
+ expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: true});
661
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
662
+
663
+ expect(uiControls.consult.hold).toEqual({isVisible: false, isEnabled: false});
664
+ expect(uiControls.consult.mute).toEqual({isVisible: false, isEnabled: false});
665
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false});
666
+ expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: false});
667
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
668
+ expect(uiControls.consult.end).toEqual({isVisible: false, isEnabled: false});
669
+ expect(uiControls.consult.switch).toEqual({isVisible: false, isEnabled: false});
670
+ });
671
+
672
+ it('hides transfer for the consulted agent during consult', () => {
673
+ const consultedTaskData = createConsultTaskData();
674
+ const consultedContext = createVoiceContext({
675
+ consultInitiator: false,
676
+ taskData: {
677
+ ...consultedTaskData,
678
+ isConsulted: true,
679
+ } as any,
680
+ });
681
+
682
+ const uiControls = computeUIControls(
683
+ TaskState.CONSULTING,
684
+ consultedContext,
685
+ consultedContext.taskData
686
+ );
687
+
688
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
689
+ });
690
+
691
+ it('hides main-leg controls for consulted agent when payload isConsulted is stale false', () => {
692
+ const taskData = createConsultedAgentInconsistentTaskData();
693
+ const baseContext = createVoiceContext();
694
+ const consultedContext = createVoiceContext({
695
+ consultInitiator: false,
696
+ taskData,
697
+ uiControlConfig: {
698
+ ...baseContext.uiControlConfig,
699
+ agentId: 'agent-2',
700
+ },
701
+ });
702
+
703
+ const uiControls = computeUIControls(TaskState.CONNECTED, consultedContext, taskData);
704
+
705
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
706
+ expect(uiControls.main.end).toEqual({isVisible: false, isEnabled: false});
707
+ });
708
+
709
+ it('keeps consult-ring controls disabled except endConsult during hydrate pending state', () => {
710
+ const pendingTaskData = createPendingConsultHydrateTaskData();
711
+ const context = createVoiceContext({
712
+ taskData: pendingTaskData as any,
713
+ consultInitiator: false,
714
+ consultFromConference: false,
715
+ consultDestinationAgentJoined: false,
716
+ consultCallHeld: false,
717
+ });
718
+
719
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, pendingTaskData as any);
720
+
721
+ expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: false});
722
+ expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: false});
723
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
724
+
725
+ expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false});
726
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: false});
727
+ expect(uiControls.consult.mergeToConference).toEqual({isVisible: true, isEnabled: false});
728
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
729
+ });
730
+
731
+ it('collapses stale consult leg controls during wrapup', () => {
732
+ const context = createVoiceContext();
733
+
734
+ const uiControls = computeUIControls(TaskState.WRAPPING_UP, context, context.taskData);
735
+
736
+ expect(uiControls.activeLeg).toBe('main');
737
+ expect(uiControls.main.wrapup).toEqual({isVisible: true, isEnabled: true});
738
+ expect(uiControls.consult).toEqual(getDefaultUIControls().consult);
739
+ });
740
+
741
+ it('disables consult for non-initiators when another agent has active consult', () => {
742
+ const taskData = createConferenceWithOtherAgentConsultPendingTaskData();
743
+ const baseContext = createVoiceContext();
744
+ const context = createVoiceContext({
745
+ consultInitiator: false,
746
+ consultFromConference: false,
747
+ taskData,
748
+ uiControlConfig: {
749
+ ...baseContext.uiControlConfig,
750
+ agentId: 'agent-2',
751
+ },
752
+ });
753
+
754
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
755
+
756
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
757
+ });
758
+
759
+ it('enables end and exitConference for non-initiators on held main leg while another agent consults', () => {
760
+ const taskData = createHeldConferenceWithActiveConsultTaskData();
761
+ const baseContext = createVoiceContext();
762
+ const context = createVoiceContext({
763
+ consultInitiator: false,
764
+ taskData,
765
+ uiControlConfig: {
766
+ ...baseContext.uiControlConfig,
767
+ agentId: 'agent-2',
768
+ },
769
+ });
770
+
771
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
77
772
 
78
- const uiControls = computeUIControls(TaskState.CONSULTING, context, context.taskData);
773
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
774
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
775
+ });
79
776
 
80
- expect(uiControls.activeLeg).toBe('consult');
81
- expect(uiControls.consult).toBeDefined();
777
+ it('enables end and exitConference on held main leg when consultInitiator flag is stale true', () => {
778
+ const taskData = createHeldConferenceWithActiveConsultTaskData();
779
+ const baseContext = createVoiceContext();
780
+ const context = createVoiceContext({
781
+ consultInitiator: true,
782
+ taskData,
783
+ uiControlConfig: {
784
+ ...baseContext.uiControlConfig,
785
+ agentId: 'agent-2',
786
+ },
787
+ });
82
788
 
83
- expect(uiControls.main.hold).toEqual({isVisible: false, isEnabled: false});
84
- expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: false});
85
- expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: false});
86
- expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
789
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
87
790
 
88
- expect(uiControls.consult.hold).toEqual({isVisible: false, isEnabled: false});
89
- expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: true});
90
- expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: true});
91
- expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
791
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
792
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
793
+ });
794
+
795
+ it('enables end and exitConference on unheld main leg for non-initiator during active conference consult', () => {
796
+ const taskData = createUnheldConferenceWithActiveConsultTaskData();
797
+ const baseContext = createVoiceContext();
798
+ const context = createVoiceContext({
799
+ consultInitiator: false,
800
+ taskData,
801
+ uiControlConfig: {
802
+ ...baseContext.uiControlConfig,
803
+ agentId: 'agent-2',
804
+ },
805
+ });
806
+
807
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
808
+
809
+ expect(uiControls.activeLeg).toBe('main');
810
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
811
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
812
+ });
813
+
814
+ it('keeps consult disabled for non-owner after owner consult completes in multi-agent conference', () => {
815
+ const taskData = createPostConsultCompletedMultiAgentTaskData('agent-2');
816
+ const baseContext = createVoiceContext();
817
+ const context = createVoiceContext({
818
+ consultInitiator: false,
819
+ taskData,
820
+ uiControlConfig: {
821
+ ...baseContext.uiControlConfig,
822
+ agentId: 'agent-2',
823
+ },
824
+ });
825
+
826
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
827
+
828
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: false});
829
+ });
830
+
831
+ it('keeps consult enabled for owner after consult completes in multi-agent conference', () => {
832
+ const taskData = createPostConsultCompletedMultiAgentTaskData('agent-1');
833
+ const baseContext = createVoiceContext();
834
+ const context = createVoiceContext({
835
+ consultInitiator: false,
836
+ taskData,
837
+ uiControlConfig: {
838
+ ...baseContext.uiControlConfig,
839
+ agentId: 'agent-1',
840
+ },
841
+ });
842
+
843
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
844
+
845
+ expect(uiControls.main.consult).toEqual({isVisible: true, isEnabled: true});
846
+ });
847
+
848
+ it('hides transfer and enables transferConference on consult leg for conference initiator', () => {
849
+ const taskData = createConferenceConsultingInitiatorTaskData();
850
+ const baseContext = createVoiceContext();
851
+ const context = createVoiceContext({
852
+ consultInitiator: true,
853
+ consultFromConference: true,
854
+ consultDestinationAgentJoined: true,
855
+ consultCallHeld: false,
856
+ taskData,
857
+ uiControlConfig: {
858
+ ...baseContext.uiControlConfig,
859
+ agentId: 'agent-1',
860
+ },
861
+ });
862
+
863
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
864
+
865
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
866
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
867
+ expect(uiControls.main.transferConference).toEqual({isVisible: true, isEnabled: false});
868
+ });
869
+
870
+ it('keeps consult leg controls active and main leg conference controls disabled on hydrate-like context', () => {
871
+ const taskData = createConferenceConsultingInitiatorTaskData();
872
+ const baseContext = createVoiceContext();
873
+ const context = createVoiceContext({
874
+ consultInitiator: true,
875
+ consultFromConference: true,
876
+ consultDestinationAgentJoined: true,
877
+ consultCallHeld: false,
878
+ taskData,
879
+ uiControlConfig: {
880
+ ...baseContext.uiControlConfig,
881
+ agentId: 'agent-1',
882
+ },
883
+ });
884
+
885
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
886
+
887
+ expect(uiControls.activeLeg).toBe('consult');
888
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
889
+ expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: false});
890
+ expect(uiControls.main.transferConference).toEqual({isVisible: true, isEnabled: false});
92
891
  expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: true});
892
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
893
+ expect(uiControls.consult.mergeToConference).toEqual({isVisible: true, isEnabled: true});
894
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
93
895
  });
94
896
 
95
- it('switches top-level controls to main leg while keeping consult leg visible', () => {
897
+ it('keeps transferConference visible on consult leg for initiator even when state is conferencing', () => {
898
+ const taskData = createConferenceConsultingInitiatorTaskData();
899
+ const baseContext = createVoiceContext();
900
+ const context = createVoiceContext({
901
+ consultInitiator: true,
902
+ consultFromConference: true,
903
+ consultDestinationAgentJoined: true,
904
+ consultCallHeld: false,
905
+ taskData,
906
+ uiControlConfig: {
907
+ ...baseContext.uiControlConfig,
908
+ agentId: 'agent-1',
909
+ },
910
+ });
911
+
912
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
913
+
914
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
915
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
916
+ });
917
+
918
+ it('shows transferConference for initiator in AgentContactUnheld-style conference consult context', () => {
919
+ const taskData = createAgentContactUnheldInitiatorConsultTaskData();
920
+ const baseContext = createVoiceContext();
96
921
  const context = createVoiceContext({
922
+ consultInitiator: true,
923
+ consultFromConference: false,
924
+ consultDestinationAgentJoined: true,
925
+ consultCallHeld: false,
926
+ taskData,
927
+ uiControlConfig: {
928
+ ...baseContext.uiControlConfig,
929
+ agentId: 'agent-1',
930
+ },
931
+ });
932
+
933
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
934
+
935
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
936
+ });
937
+
938
+ it('shows transferConference on main leg when initiator has active conference consult', () => {
939
+ const taskData = createConferenceConsultingInitiatorTaskData();
940
+ const baseContext = createVoiceContext();
941
+ const context = createVoiceContext({
942
+ consultInitiator: true,
943
+ consultFromConference: true,
944
+ consultDestinationAgentJoined: true,
97
945
  consultCallHeld: true,
946
+ taskData,
947
+ uiControlConfig: {
948
+ ...baseContext.uiControlConfig,
949
+ agentId: 'agent-1',
950
+ },
98
951
  });
99
952
 
100
- const uiControls = computeUIControls(TaskState.CONNECTED, context, context.taskData);
953
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
101
954
 
102
955
  expect(uiControls.activeLeg).toBe('main');
103
- expect(uiControls.consult).toBeDefined();
956
+ expect(uiControls.main.transfer).toEqual({isVisible: false, isEnabled: false});
957
+ expect(uiControls.main.transferConference).toEqual({isVisible: true, isEnabled: true});
958
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: false});
959
+ });
104
960
 
105
- expect(uiControls.main.hold).toEqual({isVisible: false, isEnabled: false});
106
- expect(uiControls.main.switch).toEqual({isVisible: true, isEnabled: true});
107
- expect(uiControls.main.transfer).toEqual({isVisible: true, isEnabled: true});
108
- expect(uiControls.main.conference).toEqual({isVisible: true, isEnabled: true});
109
- expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
961
+ it('keeps transferConference visible on consult leg for taskData1-style payload', () => {
962
+ const taskData = createTaskData1LikeConferenceConsultTaskData();
963
+ const baseContext = createVoiceContext();
964
+ const context = createVoiceContext({
965
+ consultInitiator: true,
966
+ consultFromConference: true,
967
+ consultDestinationAgentJoined: true,
968
+ consultCallHeld: false,
969
+ taskData,
970
+ uiControlConfig: {
971
+ ...baseContext.uiControlConfig,
972
+ agentId: '058b3e7c-8fcf-45ee-b0c4-4ef546d360b9',
973
+ },
974
+ });
110
975
 
111
- expect(uiControls.consult.hold).toEqual({isVisible: false, isEnabled: false});
112
- expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false});
113
- expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: false});
114
- expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
115
- expect(uiControls.consult.end).toEqual({isVisible: false, isEnabled: false});
116
- expect(uiControls.consult.switch).toEqual({isVisible: false, isEnabled: false});
976
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
977
+
978
+ expect(uiControls.activeLeg).toBe('consult');
979
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
980
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
117
981
  });
118
982
 
119
- it('hides transfer for the consulted agent during consult', () => {
120
- const consultedTaskData = createConsultTaskData();
121
- const consultedContext = createVoiceContext({
122
- consultInitiator: false,
123
- taskData: {
124
- ...consultedTaskData,
125
- isConsulted: true,
126
- } as any,
983
+ it('enables transferConference after AgentConsulting follows AgentConsultCreated for initiator', () => {
984
+ const baseContext = createVoiceContext();
985
+ const createdTaskData = createConferenceConsultInitiatedInitiatorTaskData();
986
+ const createdContext = createVoiceContext({
987
+ consultInitiator: true,
988
+ consultFromConference: true,
989
+ consultDestinationAgentJoined: false,
990
+ consultCallHeld: false,
991
+ taskData: createdTaskData,
992
+ uiControlConfig: {
993
+ ...baseContext.uiControlConfig,
994
+ agentId: 'agent-1',
995
+ },
127
996
  });
997
+ const createdControls = computeUIControls(TaskState.CONSULTING, createdContext, createdTaskData);
128
998
 
129
- const uiControls = computeUIControls(
999
+ expect(createdControls.consult.transferConference).toEqual({isVisible: true, isEnabled: false});
1000
+
1001
+ const consultingTaskData = createConferenceConsultingInitiatorTaskData();
1002
+ const consultingContext = createVoiceContext({
1003
+ ...createdContext,
1004
+ taskData: consultingTaskData,
1005
+ consultDestinationAgentJoined: true,
1006
+ });
1007
+ const consultingControls = computeUIControls(
130
1008
  TaskState.CONSULTING,
131
- consultedContext,
132
- consultedContext.taskData
1009
+ consultingContext,
1010
+ consultingTaskData
133
1011
  );
134
1012
 
135
- expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
1013
+ expect(consultingControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
136
1014
  });
137
1015
 
138
- it('collapses stale consult leg controls during wrapup', () => {
139
- const context = createVoiceContext();
1016
+ it('hides exitConference on main leg for consult initiator before destination joins', () => {
1017
+ const taskData = createConferenceConsultInitiatedInitiatorTaskData();
1018
+ const baseContext = createVoiceContext();
1019
+ const context = createVoiceContext({
1020
+ consultInitiator: true,
1021
+ consultFromConference: true,
1022
+ consultDestinationAgentJoined: false,
1023
+ consultCallHeld: false,
1024
+ taskData,
1025
+ uiControlConfig: {
1026
+ ...baseContext.uiControlConfig,
1027
+ agentId: 'agent-1',
1028
+ },
1029
+ });
140
1030
 
141
- const uiControls = computeUIControls(TaskState.WRAPPING_UP, context, context.taskData);
1031
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
142
1032
 
143
- expect(uiControls.activeLeg).toBe('main');
144
- expect(uiControls.main.wrapup).toEqual({isVisible: true, isEnabled: true});
145
- expect(uiControls.consult).toEqual(getDefaultUIControls().consult);
1033
+ expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
1034
+ });
1035
+
1036
+ it('hides exitConference on main leg while consulting and destination has not joined', () => {
1037
+ const taskData = createConferenceConsultInitiatedInitiatorTaskData();
1038
+ const baseContext = createVoiceContext();
1039
+ const context = createVoiceContext({
1040
+ consultInitiator: true,
1041
+ consultFromConference: true,
1042
+ consultDestinationAgentJoined: false,
1043
+ consultCallHeld: false,
1044
+ taskData,
1045
+ uiControlConfig: {
1046
+ ...baseContext.uiControlConfig,
1047
+ agentId: 'agent-1',
1048
+ },
1049
+ });
1050
+
1051
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
1052
+
1053
+ expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
1054
+ });
1055
+
1056
+ it('hides exitConference on main leg for pending self consult even with stale initiator flags', () => {
1057
+ const taskData = createConferenceConsultInitiatedInitiatorTaskData();
1058
+ const baseContext = createVoiceContext();
1059
+ const context = createVoiceContext({
1060
+ consultInitiator: false,
1061
+ consultFromConference: false,
1062
+ consultDestinationAgentJoined: false,
1063
+ consultCallHeld: false,
1064
+ taskData,
1065
+ uiControlConfig: {
1066
+ ...baseContext.uiControlConfig,
1067
+ agentId: 'agent-1',
1068
+ },
1069
+ });
1070
+
1071
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
1072
+
1073
+ expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
1074
+ });
1075
+
1076
+ });
1077
+
1078
+ describe('uiControlsComputer outdial accept/decline controls', () => {
1079
+ function createOutdialContext(voiceVariant: 'webrtc' | 'pstn' = 'webrtc'): TaskContext {
1080
+ const taskData = createTaskData({
1081
+ interaction: {
1082
+ outboundType: 'OUTDIAL',
1083
+ state: 'new',
1084
+ isTerminated: false,
1085
+ } as any,
1086
+ });
1087
+ return {
1088
+ taskData,
1089
+ consultInitiator: false,
1090
+ exitingConference: false,
1091
+ consultFromConference: false,
1092
+ transferConferenceRequested: false,
1093
+ consultDestinationType: null,
1094
+ consultDestinationAgentId: null,
1095
+ consultDestinationAgentJoined: false,
1096
+ consultCallHeld: false,
1097
+ recordingControlsAvailable: false,
1098
+ recordingInProgress: false,
1099
+ uiControlConfig: {
1100
+ isEndTaskEnabled: true,
1101
+ isEndConsultEnabled: true,
1102
+ channelType: TASK_CHANNEL_TYPE.VOICE,
1103
+ isRecordingEnabled: false,
1104
+ agentId: 'agent-1',
1105
+ voiceVariant,
1106
+ },
1107
+ uiControls: getDefaultUIControls(),
1108
+ };
1109
+ }
1110
+
1111
+ it('accept is visible but disabled for WebRTC outdial in OFFERED state', () => {
1112
+ const context = createOutdialContext('webrtc');
1113
+ const uiControls = computeUIControls(TaskState.OFFERED, context, context.taskData);
1114
+
1115
+ expect(uiControls.main.accept).toEqual({isVisible: true, isEnabled: false});
1116
+ });
1117
+
1118
+ it('decline is visible but disabled for WebRTC outdial in OFFERED state', () => {
1119
+ const context = createOutdialContext('webrtc');
1120
+ const uiControls = computeUIControls(TaskState.OFFERED, context, context.taskData);
1121
+
1122
+ expect(uiControls.main.decline).toEqual({isVisible: true, isEnabled: false});
146
1123
  });
147
1124
  });
148
1125