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

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 (26) hide show
  1. package/dist/services/task/Task.js +32 -0
  2. package/dist/services/task/Task.js.map +1 -1
  3. package/dist/services/task/TaskUtils.js +3 -1
  4. package/dist/services/task/TaskUtils.js.map +1 -1
  5. package/dist/services/task/state-machine/TaskStateMachine.js +77 -1
  6. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  7. package/dist/services/task/state-machine/actions.js +113 -23
  8. package/dist/services/task/state-machine/actions.js.map +1 -1
  9. package/dist/services/task/state-machine/uiControlsComputer.js +101 -20
  10. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  11. package/dist/types/services/task/Task.d.ts +10 -0
  12. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +110 -0
  13. package/dist/types/services/task/state-machine/actions.d.ts +2 -0
  14. package/dist/webex.js +1 -1
  15. package/package.json +1 -1
  16. package/src/services/task/Task.ts +34 -0
  17. package/src/services/task/TaskUtils.ts +5 -3
  18. package/src/services/task/state-machine/TaskStateMachine.ts +105 -1
  19. package/src/services/task/state-machine/actions.ts +151 -25
  20. package/src/services/task/state-machine/uiControlsComputer.ts +185 -29
  21. package/test/unit/spec/services/task/Task.ts +61 -0
  22. package/test/unit/spec/services/task/TaskUtils.ts +65 -0
  23. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +676 -0
  24. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +662 -1
  25. package/umd/contact-center.min.js +2 -2
  26. package/umd/contact-center.min.js.map +1 -1
@@ -1,9 +1,10 @@
1
- import {TASK_CHANNEL_TYPE} from '../../../../../../src/services/task/types';
1
+ import {TASK_CHANNEL_TYPE, VOICE_VARIANT} from '../../../../../../src/services/task/types';
2
2
  import {TaskState} from '../../../../../../src/services/task/state-machine/constants';
3
3
  import {
4
4
  computeUIControls,
5
5
  getDefaultUIControls,
6
6
  } from '../../../../../../src/services/task/state-machine/uiControlsComputer';
7
+ import {getTaskStateForUiControls} from '../../../../../../src/services/task/state-machine/actions';
7
8
  import {TaskContext} from '../../../../../../src/services/task/state-machine/types';
8
9
  import {createTaskData} from '../taskTestUtils';
9
10
 
@@ -669,6 +670,70 @@ describe('uiControlsComputer consult initiator controls', () => {
669
670
  expect(uiControls.consult.switch).toEqual({isVisible: false, isEnabled: false});
670
671
  });
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
+
672
737
  it('hides transfer for the consulted agent during consult', () => {
673
738
  const consultedTaskData = createConsultTaskData();
674
739
  const consultedContext = createVoiceContext({
@@ -894,6 +959,80 @@ describe('uiControlsComputer consult initiator controls', () => {
894
959
  expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
895
960
  });
896
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
+
897
1036
  it('keeps transferConference visible on consult leg for initiator even when state is conferencing', () => {
898
1037
  const taskData = createConferenceConsultingInitiatorTaskData();
899
1038
  const baseContext = createVoiceContext();
@@ -1073,6 +1212,528 @@ describe('uiControlsComputer consult initiator controls', () => {
1073
1212
  expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
1074
1213
  });
1075
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
+
1076
1737
  });
1077
1738
 
1078
1739
  describe('uiControlsComputer outdial accept/decline controls', () => {