@webex/contact-center 3.12.0-task-refactor.5 → 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 (30) hide show
  1. package/dist/services/task/TaskUtils.js +8 -6
  2. package/dist/services/task/TaskUtils.js.map +1 -1
  3. package/dist/services/task/state-machine/TaskStateMachine.js +71 -13
  4. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  5. package/dist/services/task/state-machine/actions.js +85 -13
  6. package/dist/services/task/state-machine/actions.js.map +1 -1
  7. package/dist/services/task/state-machine/guards.js +35 -0
  8. package/dist/services/task/state-machine/guards.js.map +1 -1
  9. package/dist/services/task/state-machine/uiControlsComputer.js +69 -7
  10. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  11. package/dist/services/task/voice/Voice.js +1 -1
  12. package/dist/services/task/voice/Voice.js.map +1 -1
  13. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +60 -8
  14. package/dist/types/services/task/state-machine/guards.d.ts +5 -0
  15. package/dist/webex.js +1 -1
  16. package/package.json +1 -1
  17. package/src/services/task/TaskUtils.ts +8 -6
  18. package/src/services/task/state-machine/TaskStateMachine.ts +94 -15
  19. package/src/services/task/state-machine/actions.ts +148 -24
  20. package/src/services/task/state-machine/guards.ts +46 -0
  21. package/src/services/task/state-machine/uiControlsComputer.ts +150 -9
  22. package/src/services/task/voice/Voice.ts +4 -1
  23. package/test/unit/spec/services/WebCallingService.ts +7 -1
  24. package/test/unit/spec/services/task/TaskUtils.ts +16 -0
  25. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +512 -0
  26. package/test/unit/spec/services/task/state-machine/guards.ts +88 -0
  27. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +929 -0
  28. package/test/unit/spec/services/task/voice/Voice.ts +20 -0
  29. package/umd/contact-center.min.js +2 -2
  30. 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,6 +552,76 @@ function createVoiceContext(overrides: Partial<TaskContext> = {}): TaskContext {
71
552
  };
72
553
  }
73
554
 
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
+
74
625
  describe('uiControlsComputer consult initiator controls', () => {
75
626
  it('returns separate main and consult controls when consult leg is active', () => {
76
627
  const context = createVoiceContext();
@@ -86,6 +637,7 @@ describe('uiControlsComputer consult initiator controls', () => {
86
637
  expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
87
638
 
88
639
  expect(uiControls.consult.hold).toEqual({isVisible: false, isEnabled: false});
640
+ expect(uiControls.consult.mute).toEqual({isVisible: false, isEnabled: false});
89
641
  expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: true});
90
642
  expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: true});
91
643
  expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
@@ -109,6 +661,7 @@ describe('uiControlsComputer consult initiator controls', () => {
109
661
  expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: false});
110
662
 
111
663
  expect(uiControls.consult.hold).toEqual({isVisible: false, isEnabled: false});
664
+ expect(uiControls.consult.mute).toEqual({isVisible: false, isEnabled: false});
112
665
  expect(uiControls.consult.transfer).toEqual({isVisible: true, isEnabled: false});
113
666
  expect(uiControls.consult.conference).toEqual({isVisible: true, isEnabled: false});
114
667
  expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
@@ -135,6 +688,46 @@ describe('uiControlsComputer consult initiator controls', () => {
135
688
  expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
136
689
  });
137
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
+
138
731
  it('collapses stale consult leg controls during wrapup', () => {
139
732
  const context = createVoiceContext();
140
733
 
@@ -144,6 +737,342 @@ describe('uiControlsComputer consult initiator controls', () => {
144
737
  expect(uiControls.main.wrapup).toEqual({isVisible: true, isEnabled: true});
145
738
  expect(uiControls.consult).toEqual(getDefaultUIControls().consult);
146
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);
772
+
773
+ expect(uiControls.main.end).toEqual({isVisible: true, isEnabled: true});
774
+ expect(uiControls.main.exitConference).toEqual({isVisible: true, isEnabled: true});
775
+ });
776
+
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
+ });
788
+
789
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
790
+
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});
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});
895
+ });
896
+
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();
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,
945
+ consultCallHeld: true,
946
+ taskData,
947
+ uiControlConfig: {
948
+ ...baseContext.uiControlConfig,
949
+ agentId: 'agent-1',
950
+ },
951
+ });
952
+
953
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
954
+
955
+ expect(uiControls.activeLeg).toBe('main');
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
+ });
960
+
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
+ });
975
+
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});
981
+ });
982
+
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
+ },
996
+ });
997
+ const createdControls = computeUIControls(TaskState.CONSULTING, createdContext, createdTaskData);
998
+
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(
1008
+ TaskState.CONSULTING,
1009
+ consultingContext,
1010
+ consultingTaskData
1011
+ );
1012
+
1013
+ expect(consultingControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
1014
+ });
1015
+
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
+ });
1030
+
1031
+ const uiControls = computeUIControls(TaskState.CONFERENCING, context, taskData);
1032
+
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
+
147
1076
  });
148
1077
 
149
1078
  describe('uiControlsComputer outdial accept/decline controls', () => {