@webex/contact-center 3.12.0-llmrefactor.2 → 3.12.0-llmrefactor.3

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 (187) hide show
  1. package/.sdd/manifest.json +5 -4
  2. package/ai-docs/ARCHITECTURE.md +1 -1
  3. package/ai-docs/CONTRACTS.md +5 -1
  4. package/ai-docs/SECURITY.md +1 -1
  5. package/ai-docs/contact-center-spec.md +22 -4
  6. package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
  7. package/dist/cc.js +440 -10
  8. package/dist/cc.js.map +1 -1
  9. package/dist/config.js +7 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +10 -2
  12. package/dist/constants.js.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/metrics/behavioral-events.js +13 -0
  15. package/dist/metrics/behavioral-events.js.map +1 -1
  16. package/dist/metrics/constants.js +2 -0
  17. package/dist/metrics/constants.js.map +1 -1
  18. package/dist/services/AddressBook.js +18 -15
  19. package/dist/services/AddressBook.js.map +1 -1
  20. package/dist/services/AnswerCallOnWebexService.js +174 -0
  21. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  22. package/dist/services/EntryPoint.js +46 -68
  23. package/dist/services/EntryPoint.js.map +1 -1
  24. package/dist/services/Queue.js +27 -22
  25. package/dist/services/Queue.js.map +1 -1
  26. package/dist/services/WebexCrossClientService.js +171 -0
  27. package/dist/services/WebexCrossClientService.js.map +1 -0
  28. package/dist/services/WxAppTelephonyMercurySync.js +93 -0
  29. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  30. package/dist/services/config/Util.js +3 -0
  31. package/dist/services/config/Util.js.map +1 -1
  32. package/dist/services/config/constants.js +10 -6
  33. package/dist/services/config/constants.js.map +1 -1
  34. package/dist/services/config/types.js +4 -0
  35. package/dist/services/config/types.js.map +1 -1
  36. package/dist/services/core/Err.js.map +1 -1
  37. package/dist/services/core/WebexRequest.js +6 -2
  38. package/dist/services/core/WebexRequest.js.map +1 -1
  39. package/dist/services/core/aqm-reqs.js +28 -14
  40. package/dist/services/core/aqm-reqs.js.map +1 -1
  41. package/dist/services/core/types.js.map +1 -1
  42. package/dist/services/task/Task.js +73 -7
  43. package/dist/services/task/Task.js.map +1 -1
  44. package/dist/services/task/TaskFactory.js +8 -4
  45. package/dist/services/task/TaskFactory.js.map +1 -1
  46. package/dist/services/task/TaskManager.js +176 -20
  47. package/dist/services/task/TaskManager.js.map +1 -1
  48. package/dist/services/task/WebexCallingUtils.js +70 -0
  49. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  50. package/dist/services/task/constants.js +4 -1
  51. package/dist/services/task/constants.js.map +1 -1
  52. package/dist/services/task/contact.js +29 -0
  53. package/dist/services/task/contact.js.map +1 -1
  54. package/dist/services/task/digital/Digital.js +3 -2
  55. package/dist/services/task/digital/Digital.js.map +1 -1
  56. package/dist/services/task/state-machine/TaskStateMachine.js +135 -37
  57. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  58. package/dist/services/task/state-machine/actions.js +6 -1
  59. package/dist/services/task/state-machine/actions.js.map +1 -1
  60. package/dist/services/task/state-machine/guards.js +38 -11
  61. package/dist/services/task/state-machine/guards.js.map +1 -1
  62. package/dist/services/task/state-machine/types.js.map +1 -1
  63. package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
  64. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  65. package/dist/services/task/types.js +16 -1
  66. package/dist/services/task/types.js.map +1 -1
  67. package/dist/services/task/voice/Voice.js +350 -14
  68. package/dist/services/task/voice/Voice.js.map +1 -1
  69. package/dist/services/task/voice/WebRTC.js +4 -1
  70. package/dist/services/task/voice/WebRTC.js.map +1 -1
  71. package/dist/services/task/voice/wxAppVoiceMethods.js +201 -0
  72. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  73. package/dist/services/wxAppTelephonyUtils.js +19 -0
  74. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  75. package/dist/types/cc.d.ts +77 -4
  76. package/dist/types/config.d.ts +7 -0
  77. package/dist/types/constants.d.ts +8 -0
  78. package/dist/types/index.d.ts +3 -1
  79. package/dist/types/metrics/constants.d.ts +2 -0
  80. package/dist/types/services/AddressBook.d.ts +2 -1
  81. package/dist/types/services/AnswerCallOnWebexService.d.ts +37 -0
  82. package/dist/types/services/EntryPoint.d.ts +7 -5
  83. package/dist/types/services/Queue.d.ts +5 -3
  84. package/dist/types/services/WebexCrossClientService.d.ts +28 -0
  85. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  86. package/dist/types/services/config/constants.d.ts +8 -5
  87. package/dist/types/services/config/types.d.ts +17 -4
  88. package/dist/types/services/core/Err.d.ts +2 -0
  89. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  90. package/dist/types/services/core/types.d.ts +2 -0
  91. package/dist/types/services/task/Task.d.ts +20 -2
  92. package/dist/types/services/task/TaskFactory.d.ts +2 -1
  93. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  94. package/dist/types/services/task/constants.d.ts +3 -0
  95. package/dist/types/services/task/contact.d.ts +4 -0
  96. package/dist/types/services/task/digital/Digital.d.ts +2 -2
  97. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +260 -56
  98. package/dist/types/services/task/state-machine/guards.d.ts +6 -5
  99. package/dist/types/services/task/state-machine/types.d.ts +9 -1
  100. package/dist/types/services/task/types.d.ts +81 -4
  101. package/dist/types/services/task/voice/Voice.d.ts +48 -9
  102. package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
  103. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +52 -0
  104. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  105. package/dist/types/types.d.ts +14 -8
  106. package/dist/types/utils/PageCache.d.ts +20 -3
  107. package/dist/types.js +7 -2
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/PageCache.js +19 -5
  110. package/dist/utils/PageCache.js.map +1 -1
  111. package/dist/webex.js +1 -1
  112. package/package.json +1 -1
  113. package/src/cc.ts +506 -9
  114. package/src/config.ts +7 -0
  115. package/src/constants.ts +8 -0
  116. package/src/index.ts +2 -0
  117. package/src/metrics/ai-docs/metrics-spec.md +9 -3
  118. package/src/metrics/behavioral-events.ts +14 -0
  119. package/src/metrics/constants.ts +2 -0
  120. package/src/services/AddressBook.ts +17 -6
  121. package/src/services/AnswerCallOnWebexService.ts +206 -0
  122. package/src/services/EntryPoint.ts +59 -60
  123. package/src/services/Queue.ts +29 -12
  124. package/src/services/WebexCrossClientService.ts +212 -0
  125. package/src/services/WxAppTelephonyMercurySync.ts +115 -0
  126. package/src/services/ai-docs/AGENTS.md +10 -10
  127. package/src/services/ai-docs/services-spec.md +6 -1
  128. package/src/services/config/Util.ts +3 -0
  129. package/src/services/config/ai-docs/AGENTS.md +1 -1
  130. package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
  131. package/src/services/config/ai-docs/config-spec.md +6 -0
  132. package/src/services/config/constants.ts +10 -6
  133. package/src/services/config/types.ts +16 -4
  134. package/src/services/core/Err.ts +1 -0
  135. package/src/services/core/WebexRequest.ts +3 -1
  136. package/src/services/core/ai-docs/core-spec.md +5 -1
  137. package/src/services/core/aqm-reqs.ts +30 -15
  138. package/src/services/core/types.ts +2 -0
  139. package/src/services/task/Task.ts +79 -8
  140. package/src/services/task/TaskFactory.ts +8 -3
  141. package/src/services/task/TaskManager.ts +254 -15
  142. package/src/services/task/WebexCallingUtils.ts +136 -0
  143. package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
  144. package/src/services/task/ai-docs/task-spec.md +117 -2
  145. package/src/services/task/constants.ts +3 -0
  146. package/src/services/task/contact.ts +30 -0
  147. package/src/services/task/digital/Digital.ts +4 -2
  148. package/src/services/task/state-machine/TaskStateMachine.ts +210 -63
  149. package/src/services/task/state-machine/actions.ts +5 -2
  150. package/src/services/task/state-machine/ai-docs/AGENTS.md +8 -4
  151. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +17 -8
  152. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +46 -22
  153. package/src/services/task/state-machine/guards.ts +64 -11
  154. package/src/services/task/state-machine/types.ts +16 -1
  155. package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
  156. package/src/services/task/types.ts +114 -4
  157. package/src/services/task/voice/Voice.ts +462 -12
  158. package/src/services/task/voice/WebRTC.ts +5 -1
  159. package/src/services/task/voice/wxAppVoiceMethods.ts +307 -0
  160. package/src/services/wxAppTelephonyUtils.ts +14 -0
  161. package/src/types.ts +33 -9
  162. package/src/utils/AGENTS.md +21 -10
  163. package/src/utils/PageCache.ts +38 -5
  164. package/src/utils/ai-docs/utils-spec.md +21 -11
  165. package/test/unit/spec/cc.ts +1274 -0
  166. package/test/unit/spec/metrics/behavioral-events.ts +18 -0
  167. package/test/unit/spec/services/AddressBook.ts +37 -6
  168. package/test/unit/spec/services/AnswerCallOnWebexService.ts +223 -0
  169. package/test/unit/spec/services/EntryPoint.ts +87 -40
  170. package/test/unit/spec/services/Queue.ts +123 -12
  171. package/test/unit/spec/services/WebexCrossClientService.ts +261 -0
  172. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +113 -0
  173. package/test/unit/spec/services/config/Util.ts +85 -0
  174. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  175. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  176. package/test/unit/spec/services/task/Task.ts +200 -0
  177. package/test/unit/spec/services/task/TaskFactory.ts +39 -2
  178. package/test/unit/spec/services/task/TaskManager.ts +653 -1
  179. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  180. package/test/unit/spec/services/task/contact.ts +33 -0
  181. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +854 -72
  182. package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
  183. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
  184. package/test/unit/spec/services/task/voice/Voice.ts +874 -0
  185. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +459 -0
  186. package/umd/contact-center.min.js +2 -2
  187. package/umd/contact-center.min.js.map +1 -1
@@ -86,7 +86,9 @@ describe('TaskManager', () => {
86
86
  ) {
87
87
  task.emit(mappedEvent, event.reason ?? event.taskData?.reason);
88
88
  } else if (event.type === TaskEvent.OUTBOUND_FAILED) {
89
- task.emit(mappedEvent, event.reason);
89
+ if (!event.taskData?.suppressOutdialFailedPopup) {
90
+ task.emit(mappedEvent, event.reason);
91
+ }
90
92
  } else {
91
93
  task.emit(mappedEvent, task);
92
94
  }
@@ -1468,6 +1470,368 @@ describe('TaskManager', () => {
1468
1470
  expect(removeTaskSpy).toHaveBeenCalled();
1469
1471
  });
1470
1472
 
1473
+ it('should map non-wxApp agent-terminated OUTDIAL ContactEnded to CONTACT_ENDED (not OUTBOUND_FAILED)', () => {
1474
+ const task = taskManager.getTask(taskId);
1475
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1476
+ const outdialFailedSpy = jest.fn();
1477
+ task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
1478
+
1479
+ const payload = {
1480
+ data: {
1481
+ type: CC_EVENTS.CONTACT_ENDED,
1482
+ interactionId: taskId,
1483
+ terminatingParty: 'Agent',
1484
+ interaction: {
1485
+ outboundType: 'OUTDIAL',
1486
+ state: 'wrapUp',
1487
+ mediaType: 'telephony',
1488
+ },
1489
+ agentsPendingWrapUp: ['test-agent-id'],
1490
+ },
1491
+ };
1492
+
1493
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1494
+
1495
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONTACT_ENDED);
1496
+ expect(outdialFailedSpy).not.toHaveBeenCalled();
1497
+ sendStateMachineEventSpy.mockRestore();
1498
+ });
1499
+
1500
+ it('should map non-wxApp agent-terminated OUTDIAL AgentOutboundFailed to OUTBOUND_FAILED with popup suppressed', () => {
1501
+ const task = taskManager.getTask(taskId);
1502
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1503
+ const outdialFailedSpy = jest.fn();
1504
+ task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
1505
+
1506
+ const payload = {
1507
+ data: {
1508
+ type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
1509
+ interactionId: taskId,
1510
+ terminatingParty: 'Agent',
1511
+ reason: 'AGENT_ENDS',
1512
+ interaction: {
1513
+ outboundType: 'OUTDIAL',
1514
+ state: 'wrapUp',
1515
+ mediaType: 'telephony',
1516
+ },
1517
+ agentsPendingWrapUp: ['test-agent-id'],
1518
+ },
1519
+ };
1520
+
1521
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1522
+
1523
+ const stateMachineEvent = expectLastStateMachineEvent(
1524
+ sendStateMachineEventSpy,
1525
+ TaskEvent.OUTBOUND_FAILED
1526
+ );
1527
+ expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
1528
+ expect(stateMachineEvent?.taskData?.suppressOutdialFailedPopup).toBe(true);
1529
+ expect(outdialFailedSpy).not.toHaveBeenCalled();
1530
+ sendStateMachineEventSpy.mockRestore();
1531
+ });
1532
+
1533
+ it('should map wxApp agent-terminated OUTDIAL AgentOutboundFailed to OUTBOUND_FAILED with AGENT_ENDS', () => {
1534
+ const task = taskManager.getTask(taskId);
1535
+ task.uiControlConfig = {enableWxBetterTogether: true};
1536
+ task.data = {
1537
+ ...task.data,
1538
+ agentId: 'test-agent-id',
1539
+ interaction: {
1540
+ ...task.data.interaction,
1541
+ outboundType: 'OUTDIAL',
1542
+ participants: {
1543
+ 'test-agent-id': {
1544
+ id: 'test-agent-id',
1545
+ deviceType: 'wxApp',
1546
+ deviceId: 'device-1',
1547
+ deviceCallId: 'call-1',
1548
+ },
1549
+ },
1550
+ },
1551
+ };
1552
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1553
+ const outdialFailedSpy = jest.fn();
1554
+ task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
1555
+
1556
+ const payload = {
1557
+ data: {
1558
+ type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
1559
+ interactionId: taskId,
1560
+ terminatingParty: 'Agent',
1561
+ reason: 'AGENT_ENDS',
1562
+ interaction: {
1563
+ outboundType: 'OUTDIAL',
1564
+ state: 'wrapUp',
1565
+ mediaType: 'telephony',
1566
+ participants: {
1567
+ 'test-agent-id': {
1568
+ id: 'test-agent-id',
1569
+ deviceType: 'wxApp',
1570
+ deviceId: 'device-1',
1571
+ deviceCallId: 'call-1',
1572
+ },
1573
+ },
1574
+ },
1575
+ agentsPendingWrapUp: ['test-agent-id'],
1576
+ },
1577
+ };
1578
+
1579
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1580
+
1581
+ const stateMachineEvent = expectLastStateMachineEvent(
1582
+ sendStateMachineEventSpy,
1583
+ TaskEvent.OUTBOUND_FAILED
1584
+ );
1585
+ expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
1586
+ expect(outdialFailedSpy).toHaveBeenCalledWith('AGENT_ENDS');
1587
+ sendStateMachineEventSpy.mockRestore();
1588
+ });
1589
+
1590
+ it('should map wxApp agent-terminated OUTDIAL ContactEnded to OUTBOUND_FAILED with AGENT_ENDS', () => {
1591
+ const task = taskManager.getTask(taskId);
1592
+ task.uiControlConfig = {enableWxBetterTogether: true};
1593
+ task.data = {
1594
+ ...task.data,
1595
+ agentId: 'test-agent-id',
1596
+ interaction: {
1597
+ ...task.data.interaction,
1598
+ outboundType: 'OUTDIAL',
1599
+ participants: {
1600
+ 'test-agent-id': {
1601
+ id: 'test-agent-id',
1602
+ deviceType: 'wxApp',
1603
+ deviceId: 'device-1',
1604
+ deviceCallId: 'call-1',
1605
+ },
1606
+ },
1607
+ },
1608
+ };
1609
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1610
+ const outdialFailedSpy = jest.fn();
1611
+ task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
1612
+
1613
+ const payload = {
1614
+ data: {
1615
+ type: CC_EVENTS.CONTACT_ENDED,
1616
+ interactionId: taskId,
1617
+ terminatingParty: 'Agent',
1618
+ interaction: {
1619
+ outboundType: 'OUTDIAL',
1620
+ state: 'wrapUp',
1621
+ mediaType: 'telephony',
1622
+ participants: {
1623
+ 'test-agent-id': {
1624
+ id: 'test-agent-id',
1625
+ deviceType: 'wxApp',
1626
+ deviceId: 'device-1',
1627
+ deviceCallId: 'call-1',
1628
+ },
1629
+ },
1630
+ },
1631
+ agentsPendingWrapUp: ['test-agent-id'],
1632
+ },
1633
+ };
1634
+
1635
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1636
+
1637
+ const stateMachineEvent = expectLastStateMachineEvent(
1638
+ sendStateMachineEventSpy,
1639
+ TaskEvent.OUTBOUND_FAILED
1640
+ );
1641
+ expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
1642
+ expect(stateMachineEvent?.taskData?.wrapUpRequired).toBe(true);
1643
+ expect(outdialFailedSpy).toHaveBeenCalledWith('AGENT_ENDS');
1644
+ sendStateMachineEventSpy.mockRestore();
1645
+ });
1646
+
1647
+ it('should map offer-stage OUTDIAL decline ContactEnded to CONTACT_ENDED (not wrapup)', () => {
1648
+ const task = taskManager.getTask(taskId);
1649
+ task.stateMachineService = {
1650
+ getSnapshot: () => ({value: 'OFFERED'}),
1651
+ };
1652
+ task.uiControlConfig = {enableWxBetterTogether: true, wxAppAnswerPending: false};
1653
+ task.data = {
1654
+ ...task.data,
1655
+ agentId: 'test-agent-id',
1656
+ interaction: {
1657
+ ...task.data.interaction,
1658
+ outboundType: 'OUTDIAL',
1659
+ participants: {
1660
+ 'test-agent-id': {
1661
+ id: 'test-agent-id',
1662
+ deviceType: 'wxApp',
1663
+ deviceId: 'device-1',
1664
+ deviceCallId: 'call-1',
1665
+ },
1666
+ },
1667
+ },
1668
+ };
1669
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1670
+ const outdialFailedSpy = jest.fn();
1671
+ task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
1672
+
1673
+ const payload = {
1674
+ data: {
1675
+ type: CC_EVENTS.CONTACT_ENDED,
1676
+ interactionId: taskId,
1677
+ terminatingParty: 'Agent',
1678
+ interaction: {
1679
+ outboundType: 'OUTDIAL',
1680
+ state: 'wrapUp',
1681
+ mediaType: 'telephony',
1682
+ },
1683
+ agentsPendingWrapUp: [],
1684
+ },
1685
+ };
1686
+
1687
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1688
+
1689
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONTACT_ENDED);
1690
+ expect(outdialFailedSpy).not.toHaveBeenCalled();
1691
+ sendStateMachineEventSpy.mockRestore();
1692
+ });
1693
+
1694
+ it('should map post-accept OUTDIAL ContactEnded to OUTBOUND_FAILED when still OFFERED with wxAppAnswerPending', () => {
1695
+ const task = taskManager.getTask(taskId);
1696
+ task.stateMachineService = {
1697
+ getSnapshot: () => ({value: 'OFFERED'}),
1698
+ };
1699
+ task.uiControlConfig = {enableWxBetterTogether: true, wxAppAnswerPending: true};
1700
+ task.data = {
1701
+ ...task.data,
1702
+ agentId: 'test-agent-id',
1703
+ interaction: {
1704
+ ...task.data.interaction,
1705
+ outboundType: 'OUTDIAL',
1706
+ participants: {
1707
+ 'test-agent-id': {
1708
+ id: 'test-agent-id',
1709
+ deviceType: 'wxApp',
1710
+ deviceId: 'device-1',
1711
+ deviceCallId: 'call-1',
1712
+ },
1713
+ },
1714
+ },
1715
+ };
1716
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1717
+ const outdialFailedSpy = jest.fn();
1718
+ task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
1719
+
1720
+ const payload = {
1721
+ data: {
1722
+ type: CC_EVENTS.CONTACT_ENDED,
1723
+ interactionId: taskId,
1724
+ terminatingParty: 'Agent',
1725
+ interaction: {
1726
+ outboundType: 'OUTDIAL',
1727
+ state: 'offered',
1728
+ mediaType: 'telephony',
1729
+ participants: {
1730
+ 'test-agent-id': {
1731
+ id: 'test-agent-id',
1732
+ deviceType: 'wxApp',
1733
+ deviceId: 'device-1',
1734
+ deviceCallId: 'call-1',
1735
+ },
1736
+ },
1737
+ },
1738
+ },
1739
+ };
1740
+
1741
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1742
+
1743
+ const stateMachineEvent = expectLastStateMachineEvent(
1744
+ sendStateMachineEventSpy,
1745
+ TaskEvent.OUTBOUND_FAILED
1746
+ );
1747
+ expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
1748
+ expect(outdialFailedSpy).toHaveBeenCalledWith('AGENT_ENDS');
1749
+ sendStateMachineEventSpy.mockRestore();
1750
+ });
1751
+
1752
+ it('should map post-accept agent-terminated OUTDIAL ContactEnded to OUTBOUND_FAILED when CONNECTED', () => {
1753
+ const task = taskManager.getTask(taskId);
1754
+ task.stateMachineService = {
1755
+ getSnapshot: () => ({value: 'CONNECTED'}),
1756
+ };
1757
+ task.uiControlConfig = {enableWxBetterTogether: true};
1758
+ task.data = {
1759
+ ...task.data,
1760
+ agentId: 'test-agent-id',
1761
+ interaction: {
1762
+ ...task.data.interaction,
1763
+ outboundType: 'OUTDIAL',
1764
+ participants: {
1765
+ 'test-agent-id': {
1766
+ id: 'test-agent-id',
1767
+ deviceType: 'wxApp',
1768
+ deviceId: 'device-1',
1769
+ deviceCallId: 'call-1',
1770
+ },
1771
+ },
1772
+ },
1773
+ };
1774
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1775
+ const outdialFailedSpy = jest.fn();
1776
+ task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
1777
+
1778
+ const payload = {
1779
+ data: {
1780
+ type: CC_EVENTS.CONTACT_ENDED,
1781
+ interactionId: taskId,
1782
+ terminatingParty: 'Agent',
1783
+ interaction: {
1784
+ outboundType: 'OUTDIAL',
1785
+ state: 'wrapUp',
1786
+ mediaType: 'telephony',
1787
+ participants: {
1788
+ 'test-agent-id': {
1789
+ id: 'test-agent-id',
1790
+ deviceType: 'wxApp',
1791
+ deviceId: 'device-1',
1792
+ deviceCallId: 'call-1',
1793
+ },
1794
+ },
1795
+ },
1796
+ agentsPendingWrapUp: ['test-agent-id'],
1797
+ },
1798
+ };
1799
+
1800
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1801
+
1802
+ const stateMachineEvent = expectLastStateMachineEvent(
1803
+ sendStateMachineEventSpy,
1804
+ TaskEvent.OUTBOUND_FAILED
1805
+ );
1806
+ expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
1807
+ expect(outdialFailedSpy).toHaveBeenCalledWith('AGENT_ENDS');
1808
+ sendStateMachineEventSpy.mockRestore();
1809
+ });
1810
+
1811
+ it('should map customer-terminated OUTDIAL ContactEnded to CONTACT_ENDED (no AGENT_ENDS remap)', () => {
1812
+ const task = taskManager.getTask(taskId);
1813
+ const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
1814
+
1815
+ const payload = {
1816
+ data: {
1817
+ type: CC_EVENTS.CONTACT_ENDED,
1818
+ interactionId: taskId,
1819
+ terminatingParty: 'Customer',
1820
+ interaction: {
1821
+ outboundType: 'OUTDIAL',
1822
+ state: 'new',
1823
+ mediaType: 'telephony',
1824
+ },
1825
+ agentsPendingWrapUp: ['test-agent-id'],
1826
+ },
1827
+ };
1828
+
1829
+ webSocketManagerMock.emit('message', JSON.stringify(payload));
1830
+
1831
+ expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONTACT_ENDED);
1832
+ sendStateMachineEventSpy.mockRestore();
1833
+ });
1834
+
1471
1835
  it('should handle CONTACT_ENDED gracefully when task is undefined', () => {
1472
1836
  const payload = {
1473
1837
  data: {
@@ -2412,6 +2776,254 @@ describe('TaskManager', () => {
2412
2776
  expect(call.type).toBe(TaskEvent.PARTICIPANT_LEAVE);
2413
2777
  });
2414
2778
 
2779
+ it('routes participant-left from a main interaction to a child-keyed task', () => {
2780
+ const childTaskId = 'child-interaction-id';
2781
+ const childTask = createMockTask({
2782
+ ...taskDataMock,
2783
+ interactionId: childTaskId,
2784
+ isConsulted: true,
2785
+ interaction: {
2786
+ mediaType: 'telephony',
2787
+ interactionId: childTaskId,
2788
+ parentInteractionId: taskId,
2789
+ },
2790
+ });
2791
+ delete taskManager.taskCollection[taskId];
2792
+ taskManager.taskCollection[childTaskId] = childTask;
2793
+
2794
+ webSocketManagerMock.emit(
2795
+ 'message',
2796
+ JSON.stringify({
2797
+ data: {
2798
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2799
+ interactionId: taskId,
2800
+ participantId: 'test-agent-id',
2801
+ interaction: {
2802
+ mediaType: 'telephony',
2803
+ interactionId: taskId,
2804
+ mainInteractionId: taskId,
2805
+ },
2806
+ },
2807
+ })
2808
+ );
2809
+
2810
+ expect(childTask.sendStateMachineEvent).toHaveBeenCalledWith(
2811
+ expect.objectContaining({
2812
+ type: TaskEvent.PARTICIPANT_LEAVE,
2813
+ participantId: 'test-agent-id',
2814
+ })
2815
+ );
2816
+ expect(taskManager.taskCollection[childTaskId]).toBeUndefined();
2817
+ expect(taskManager.taskCollection[taskId]).toBe(childTask);
2818
+ expect(
2819
+ Object.values(taskManager.taskCollection).filter((entry) => entry === childTask)
2820
+ ).toHaveLength(1);
2821
+ });
2822
+
2823
+ it('routes consult-end from a main interaction to a child-keyed consulted task', () => {
2824
+ const childTaskId = 'consult-child-interaction-id';
2825
+ const childTask = createMockTask({
2826
+ ...taskDataMock,
2827
+ interactionId: childTaskId,
2828
+ isConsulted: true,
2829
+ interaction: {
2830
+ mediaType: 'telephony',
2831
+ interactionId: childTaskId,
2832
+ callProcessingDetails: {parentInteractionId: taskId},
2833
+ },
2834
+ });
2835
+ delete taskManager.taskCollection[taskId];
2836
+ taskManager.taskCollection[childTaskId] = childTask;
2837
+
2838
+ webSocketManagerMock.emit(
2839
+ 'message',
2840
+ JSON.stringify({
2841
+ data: {
2842
+ type: CC_EVENTS.AGENT_CONSULT_ENDED,
2843
+ interactionId: taskId,
2844
+ interaction: {
2845
+ mediaType: 'telephony',
2846
+ interactionId: taskId,
2847
+ mainInteractionId: taskId,
2848
+ },
2849
+ },
2850
+ })
2851
+ );
2852
+
2853
+ expect(childTask.sendStateMachineEvent).toHaveBeenCalledWith(
2854
+ expect.objectContaining({type: TaskEvent.CONSULT_END})
2855
+ );
2856
+ });
2857
+
2858
+ it('deduplicates aliases of the same child task during related-interaction lookup', () => {
2859
+ const childTaskId = 'aliased-child-interaction-id';
2860
+ const childTask = createMockTask({
2861
+ ...taskDataMock,
2862
+ interactionId: childTaskId,
2863
+ interaction: {
2864
+ mediaType: 'telephony',
2865
+ interactionId: childTaskId,
2866
+ mainInteractionId: taskId,
2867
+ },
2868
+ });
2869
+ delete taskManager.taskCollection[taskId];
2870
+ taskManager.taskCollection[childTaskId] = childTask;
2871
+ taskManager.taskCollection['child-task-alias'] = childTask;
2872
+
2873
+ webSocketManagerMock.emit(
2874
+ 'message',
2875
+ JSON.stringify({
2876
+ data: {
2877
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2878
+ interactionId: taskId,
2879
+ participantId: 'test-agent-id',
2880
+ },
2881
+ })
2882
+ );
2883
+
2884
+ expect(childTask.sendStateMachineEvent).toHaveBeenCalledTimes(1);
2885
+ expect(
2886
+ Object.values(taskManager.taskCollection).filter((entry) => entry === childTask)
2887
+ ).toHaveLength(1);
2888
+ });
2889
+
2890
+ it('prefers an exact task match over a child task with the same main interaction', () => {
2891
+ const childTaskId = 'child-interaction-id';
2892
+ const childTask = createMockTask({
2893
+ ...taskDataMock,
2894
+ interactionId: childTaskId,
2895
+ interaction: {
2896
+ mediaType: 'telephony',
2897
+ interactionId: childTaskId,
2898
+ mainInteractionId: taskId,
2899
+ },
2900
+ });
2901
+ taskManager.taskCollection[childTaskId] = childTask;
2902
+
2903
+ webSocketManagerMock.emit(
2904
+ 'message',
2905
+ JSON.stringify({
2906
+ data: {
2907
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2908
+ interactionId: taskId,
2909
+ participantId: 'another-agent',
2910
+ },
2911
+ })
2912
+ );
2913
+
2914
+ expect(task.sendStateMachineEvent).toHaveBeenCalled();
2915
+ expect(childTask.sendStateMachineEvent).not.toHaveBeenCalled();
2916
+ });
2917
+
2918
+ it('does not route a main-interaction event when multiple unique child tasks match', () => {
2919
+ delete taskManager.taskCollection[taskId];
2920
+ const createChildTask = (interactionId: string) =>
2921
+ createMockTask({
2922
+ ...taskDataMock,
2923
+ interactionId,
2924
+ interaction: {
2925
+ mediaType: 'telephony',
2926
+ interactionId,
2927
+ mainInteractionId: taskId,
2928
+ },
2929
+ });
2930
+ const firstChildTask = createChildTask('child-1');
2931
+ const secondChildTask = createChildTask('child-2');
2932
+ taskManager.taskCollection['child-1'] = firstChildTask;
2933
+ taskManager.taskCollection['child-2'] = secondChildTask;
2934
+
2935
+ webSocketManagerMock.emit(
2936
+ 'message',
2937
+ JSON.stringify({
2938
+ data: {
2939
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2940
+ interactionId: taskId,
2941
+ },
2942
+ })
2943
+ );
2944
+
2945
+ expect(firstChildTask.sendStateMachineEvent).not.toHaveBeenCalled();
2946
+ expect(secondChildTask.sendStateMachineEvent).not.toHaveBeenCalled();
2947
+ });
2948
+
2949
+ it('does not route a lifecycle event without a correlation identifier', () => {
2950
+ webSocketManagerMock.emit(
2951
+ 'message',
2952
+ JSON.stringify({
2953
+ data: {
2954
+ type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
2955
+ participantId: 'another-agent',
2956
+ },
2957
+ })
2958
+ );
2959
+
2960
+ expect(task.sendStateMachineEvent).not.toHaveBeenCalled();
2961
+ });
2962
+
2963
+ it('removes every collection alias for a task during terminal cleanup', () => {
2964
+ taskManager.taskCollection['task-alias'] = task;
2965
+
2966
+ (taskManager as any).removeTaskFromCollection(task);
2967
+
2968
+ expect(taskManager.taskCollection[taskId]).toBeUndefined();
2969
+ expect(taskManager.taskCollection['task-alias']).toBeUndefined();
2970
+ });
2971
+
2972
+ it('replaces an EP-DN child task with a hydrated main task on CONTACT_MERGED', () => {
2973
+ const childTaskId = 'ep-dn-child-interaction-id';
2974
+ const childTask = createMockTask({
2975
+ ...taskDataMock,
2976
+ interactionId: childTaskId,
2977
+ interaction: {
2978
+ mediaType: 'telephony',
2979
+ interactionId: childTaskId,
2980
+ parentInteractionId: taskId,
2981
+ },
2982
+ });
2983
+ const mergedHandler = jest.fn();
2984
+ delete taskManager.taskCollection[taskId];
2985
+ taskManager.taskCollection[childTaskId] = childTask;
2986
+ taskManager.on(TASK_EVENTS.TASK_MERGED, mergedHandler);
2987
+
2988
+ webSocketManagerMock.emit(
2989
+ 'message',
2990
+ JSON.stringify({
2991
+ data: {
2992
+ type: CC_EVENTS.CONTACT_MERGED,
2993
+ interactionId: taskId,
2994
+ childInteractionId: childTaskId,
2995
+ interaction: {
2996
+ mediaType: 'telephony',
2997
+ interactionId: taskId,
2998
+ mainInteractionId: taskId,
2999
+ state: 'conference',
3000
+ owner: 'test-agent-id',
3001
+ participants: {
3002
+ 'test-agent-id': {id: 'test-agent-id', pType: 'Agent', hasLeft: false},
3003
+ },
3004
+ media: {
3005
+ [taskId]: {
3006
+ mediaResourceId: taskId,
3007
+ mediaType: 'telephony',
3008
+ mType: 'mainCall',
3009
+ participants: ['test-agent-id'],
3010
+ },
3011
+ },
3012
+ },
3013
+ },
3014
+ })
3015
+ );
3016
+
3017
+ const mainTask = taskManager.taskCollection[taskId];
3018
+ expect(taskManager.taskCollection[childTaskId]).toBeUndefined();
3019
+ expect(mainTask).toBeDefined();
3020
+ expect(mainTask).not.toBe(childTask);
3021
+ expect(mainTask.sendStateMachineEvent).toHaveBeenCalledWith(
3022
+ expect.objectContaining({type: TaskEvent.HYDRATE})
3023
+ );
3024
+ expect(mergedHandler).toHaveBeenCalledWith(mainTask);
3025
+ });
3026
+
2415
3027
  it('handles AGENT_CONSULT_CONFERENCING event without errors', () => {
2416
3028
  const payload = {
2417
3029
  data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCING, interactionId: taskId},
@@ -2478,4 +3090,44 @@ describe('TaskManager', () => {
2478
3090
  sendStateMachineEventSpy.mockRestore();
2479
3091
  });
2480
3092
  });
3093
+
3094
+ describe('applyEnableWxBetterTogether', () => {
3095
+ it('updates config flags and propagates to active tasks', () => {
3096
+ const taskOne = {setEnableWxBetterTogether: jest.fn()};
3097
+ const taskTwo = {setEnableWxBetterTogether: jest.fn()};
3098
+
3099
+ taskManager.setConfigFlags({
3100
+ isEndTaskEnabled: true,
3101
+ isEndConsultEnabled: true,
3102
+ enableWxBetterTogether: true,
3103
+ });
3104
+ taskManager['taskCollection'] = {
3105
+ [taskId]: taskOne,
3106
+ 'task-2': taskTwo,
3107
+ };
3108
+
3109
+ taskManager.applyEnableWxBetterTogether(false);
3110
+
3111
+ expect(taskManager['configFlags']?.enableWxBetterTogether).toBe(false);
3112
+ expect(taskOne.setEnableWxBetterTogether).toHaveBeenCalledWith(false);
3113
+ expect(taskTwo.setEnableWxBetterTogether).toHaveBeenCalledWith(false);
3114
+ });
3115
+ });
3116
+
3117
+ describe('applyWxAppMuteStateFromSync', () => {
3118
+ it('propagates mute state to all tasks', () => {
3119
+ const taskOne = {applyWxAppMuteStateFromSync: jest.fn()};
3120
+ const taskTwo = {applyWxAppMuteStateFromSync: jest.fn()};
3121
+
3122
+ taskManager['taskCollection'] = {
3123
+ [taskId]: taskOne,
3124
+ 'task-2': taskTwo,
3125
+ };
3126
+
3127
+ taskManager.applyWxAppMuteStateFromSync('call-1', true);
3128
+
3129
+ expect(taskOne.applyWxAppMuteStateFromSync).toHaveBeenCalledWith('call-1', true);
3130
+ expect(taskTwo.applyWxAppMuteStateFromSync).toHaveBeenCalledWith('call-1', true);
3131
+ });
3132
+ });
2481
3133
  });