@webex/contact-center 3.12.0-task-refactor.7 → 3.12.0-task-refactor.9

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 (58) hide show
  1. package/dist/cc.js +3 -4
  2. package/dist/cc.js.map +1 -1
  3. package/dist/constants.js +1 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/metrics/constants.js +2 -0
  6. package/dist/metrics/constants.js.map +1 -1
  7. package/dist/services/ApiAiAssistant.js +74 -3
  8. package/dist/services/ApiAiAssistant.js.map +1 -1
  9. package/dist/services/config/types.js +9 -1
  10. package/dist/services/config/types.js.map +1 -1
  11. package/dist/services/task/Task.js +32 -0
  12. package/dist/services/task/Task.js.map +1 -1
  13. package/dist/services/task/TaskManager.js +7 -2
  14. package/dist/services/task/TaskManager.js.map +1 -1
  15. package/dist/services/task/TaskUtils.js +3 -1
  16. package/dist/services/task/TaskUtils.js.map +1 -1
  17. package/dist/services/task/state-machine/TaskStateMachine.js +76 -0
  18. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  19. package/dist/services/task/state-machine/actions.js +113 -23
  20. package/dist/services/task/state-machine/actions.js.map +1 -1
  21. package/dist/services/task/state-machine/uiControlsComputer.js +99 -21
  22. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  23. package/dist/types/constants.d.ts +1 -0
  24. package/dist/types/metrics/constants.d.ts +2 -0
  25. package/dist/types/services/ApiAiAssistant.d.ts +10 -2
  26. package/dist/types/services/config/types.d.ts +16 -0
  27. package/dist/types/services/task/Task.d.ts +10 -0
  28. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +110 -0
  29. package/dist/types/services/task/state-machine/actions.d.ts +2 -0
  30. package/dist/types/types.d.ts +24 -0
  31. package/dist/types.js +15 -0
  32. package/dist/types.js.map +1 -1
  33. package/dist/webex.js +1 -1
  34. package/package.json +1 -1
  35. package/src/cc.ts +6 -4
  36. package/src/constants.ts +1 -0
  37. package/src/metrics/constants.ts +2 -0
  38. package/src/services/ApiAiAssistant.ts +102 -2
  39. package/src/services/config/types.ts +8 -0
  40. package/src/services/task/Task.ts +34 -0
  41. package/src/services/task/TaskManager.ts +7 -2
  42. package/src/services/task/TaskUtils.ts +5 -3
  43. package/src/services/task/ai-docs/AGENTS.md +7 -0
  44. package/src/services/task/ai-docs/ARCHITECTURE.md +12 -0
  45. package/src/services/task/state-machine/TaskStateMachine.ts +104 -0
  46. package/src/services/task/state-machine/actions.ts +151 -25
  47. package/src/services/task/state-machine/uiControlsComputer.ts +173 -30
  48. package/src/types.ts +25 -0
  49. package/test/unit/spec/cc.ts +2 -0
  50. package/test/unit/spec/services/ApiAiAssistant.ts +105 -17
  51. package/test/unit/spec/services/task/Task.ts +61 -0
  52. package/test/unit/spec/services/task/TaskManager.ts +42 -0
  53. package/test/unit/spec/services/task/TaskUtils.ts +65 -0
  54. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +676 -0
  55. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +597 -0
  56. package/test/unit/spec/services/task/voice/WebRTC.ts +99 -106
  57. package/umd/contact-center.min.js +2 -2
  58. package/umd/contact-center.min.js.map +1 -1
@@ -4,6 +4,7 @@ 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
 
@@ -958,6 +959,80 @@ describe('uiControlsComputer consult initiator controls', () => {
958
959
  expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
959
960
  });
960
961
 
962
+ it('enables consult leg controls for conference DN AgentConsulting after destination joined', () => {
963
+ const taskData = createTaskData({
964
+ agentId: 'agent-1',
965
+ consultingAgentId: 'agent-1',
966
+ consultMediaResourceId: 'consult-media-1',
967
+ isConsulted: false,
968
+ destinationType: 'DN',
969
+ type: 'AgentConsulting' as any,
970
+ interaction: {
971
+ state: 'conference',
972
+ interactionId: 'interaction-1',
973
+ mainInteractionId: 'interaction-1',
974
+ owner: 'agent-1',
975
+ participants: {
976
+ 'agent-1': {
977
+ id: 'agent-1',
978
+ pType: 'Agent',
979
+ hasLeft: false,
980
+ consultState: 'consulting',
981
+ isConsulted: false,
982
+ },
983
+ 'agent-2': {
984
+ id: 'agent-2',
985
+ pType: 'Agent',
986
+ hasLeft: false,
987
+ consultState: 'conferencing',
988
+ },
989
+ 'dn-dest': {
990
+ id: 'dn-dest',
991
+ pType: 'DN',
992
+ hasLeft: false,
993
+ hasJoined: true,
994
+ },
995
+ 'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
996
+ } as any,
997
+ media: {
998
+ 'interaction-1': {
999
+ mediaResourceId: 'interaction-1',
1000
+ mType: 'mainCall',
1001
+ participants: ['agent-1', 'agent-2', 'customer-1'],
1002
+ isHold: false,
1003
+ },
1004
+ 'consult-media-1': {
1005
+ mediaResourceId: 'consult-media-1',
1006
+ mType: 'consult',
1007
+ participants: ['agent-1', 'dn-dest'],
1008
+ isHold: false,
1009
+ },
1010
+ } as any,
1011
+ } as any,
1012
+ });
1013
+ const baseContext = createVoiceContext();
1014
+ const context = createVoiceContext({
1015
+ consultInitiator: true,
1016
+ consultFromConference: true,
1017
+ consultDestinationAgentJoined: true,
1018
+ consultDestinationType: 'entryPoint',
1019
+ consultCallHeld: false,
1020
+ taskData,
1021
+ uiControlConfig: {
1022
+ ...baseContext.uiControlConfig,
1023
+ agentId: 'agent-1',
1024
+ },
1025
+ });
1026
+
1027
+ const uiControls = computeUIControls(TaskState.CONSULTING, context, taskData);
1028
+
1029
+ expect(uiControls.consult.switch).toEqual({isVisible: true, isEnabled: true});
1030
+ expect(uiControls.consult.transfer).toEqual({isVisible: false, isEnabled: false});
1031
+ expect(uiControls.consult.transferConference).toEqual({isVisible: true, isEnabled: true});
1032
+ expect(uiControls.consult.mergeToConference).toEqual({isVisible: true, isEnabled: true});
1033
+ expect(uiControls.consult.endConsult).toEqual({isVisible: true, isEnabled: true});
1034
+ });
1035
+
961
1036
  it('keeps transferConference visible on consult leg for initiator even when state is conferencing', () => {
962
1037
  const taskData = createConferenceConsultingInitiatorTaskData();
963
1038
  const baseContext = createVoiceContext();
@@ -1137,6 +1212,528 @@ describe('uiControlsComputer consult initiator controls', () => {
1137
1212
  expect(uiControls.main.exitConference).toEqual({isVisible: false, isEnabled: false});
1138
1213
  });
1139
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
+
1140
1737
  });
1141
1738
 
1142
1739
  describe('uiControlsComputer outdial accept/decline controls', () => {