@webex/contact-center 3.12.0-llmrefactor.1 → 3.12.0-llmrefactor.11
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.
- package/.sdd/manifest.json +7 -5
- package/ai-docs/ARCHITECTURE.md +1 -1
- package/ai-docs/CONTRACTS.md +8 -3
- package/ai-docs/SECURITY.md +1 -1
- package/ai-docs/contact-center-spec.md +22 -4
- package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
- package/dist/cc.js +544 -12
- package/dist/cc.js.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +10 -2
- package/dist/constants.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/metrics/MetricsManager.js +16 -0
- package/dist/metrics/MetricsManager.js.map +1 -1
- package/dist/metrics/behavioral-events.js +104 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +18 -0
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/AddressBook.js +18 -15
- package/dist/services/AddressBook.js.map +1 -1
- package/dist/services/AnswerCallOnWebexService.js +202 -0
- package/dist/services/AnswerCallOnWebexService.js.map +1 -0
- package/dist/services/EntryPoint.js +46 -68
- package/dist/services/EntryPoint.js.map +1 -1
- package/dist/services/Queue.js +27 -22
- package/dist/services/Queue.js.map +1 -1
- package/dist/services/WebexCrossClientService.js +222 -0
- package/dist/services/WebexCrossClientService.js.map +1 -0
- package/dist/services/WxAppTelephonyMercurySync.js +108 -0
- package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
- package/dist/services/config/Util.js +3 -0
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/constants.js +10 -6
- package/dist/services/config/constants.js.map +1 -1
- package/dist/services/config/types.js +4 -0
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/core/Err.js.map +1 -1
- package/dist/services/core/WebexRequest.js +6 -2
- package/dist/services/core/WebexRequest.js.map +1 -1
- package/dist/services/core/aqm-reqs.js +28 -14
- package/dist/services/core/aqm-reqs.js.map +1 -1
- package/dist/services/core/types.js.map +1 -1
- package/dist/services/task/Task.js +73 -7
- package/dist/services/task/Task.js.map +1 -1
- package/dist/services/task/TaskFactory.js +8 -4
- package/dist/services/task/TaskFactory.js.map +1 -1
- package/dist/services/task/TaskManager.js +490 -21
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/WebexCallingUtils.js +70 -0
- package/dist/services/task/WebexCallingUtils.js.map +1 -0
- package/dist/services/task/constants.js +4 -1
- package/dist/services/task/constants.js.map +1 -1
- package/dist/services/task/contact.js +29 -0
- package/dist/services/task/contact.js.map +1 -1
- package/dist/services/task/digital/Digital.js +3 -2
- package/dist/services/task/digital/Digital.js.map +1 -1
- package/dist/services/task/state-machine/TaskStateMachine.js +162 -53
- package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
- package/dist/services/task/state-machine/actions.js +6 -1
- package/dist/services/task/state-machine/actions.js.map +1 -1
- package/dist/services/task/state-machine/guards.js +7 -10
- package/dist/services/task/state-machine/guards.js.map +1 -1
- package/dist/services/task/state-machine/types.js.map +1 -1
- package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
- package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
- package/dist/services/task/types.js +16 -1
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +456 -15
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/services/task/voice/WebRTC.js +4 -1
- package/dist/services/task/voice/WebRTC.js.map +1 -1
- package/dist/services/task/voice/wxAppVoiceMethods.js +354 -0
- package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
- package/dist/services/wxAppDiagnosticLogging.js +122 -0
- package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
- package/dist/services/wxAppTelephonyUtils.js +19 -0
- package/dist/services/wxAppTelephonyUtils.js.map +1 -0
- package/dist/types/cc.d.ts +78 -4
- package/dist/types/config.d.ts +7 -0
- package/dist/types/constants.d.ts +8 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/types/metrics/MetricsManager.d.ts +6 -0
- package/dist/types/metrics/constants.d.ts +17 -0
- package/dist/types/services/AddressBook.d.ts +2 -1
- package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -0
- package/dist/types/services/EntryPoint.d.ts +7 -5
- package/dist/types/services/Queue.d.ts +5 -3
- package/dist/types/services/WebexCrossClientService.d.ts +32 -0
- package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
- package/dist/types/services/config/constants.d.ts +8 -5
- package/dist/types/services/config/types.d.ts +17 -4
- package/dist/types/services/core/Err.d.ts +2 -0
- package/dist/types/services/core/WebexRequest.d.ts +1 -0
- package/dist/types/services/core/types.d.ts +2 -0
- package/dist/types/services/task/Task.d.ts +20 -2
- package/dist/types/services/task/TaskFactory.d.ts +2 -1
- package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
- package/dist/types/services/task/constants.d.ts +3 -0
- package/dist/types/services/task/contact.d.ts +4 -0
- package/dist/types/services/task/digital/Digital.d.ts +2 -2
- package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +304 -56
- package/dist/types/services/task/state-machine/guards.d.ts +6 -5
- package/dist/types/services/task/state-machine/types.d.ts +9 -1
- package/dist/types/services/task/types.d.ts +81 -4
- package/dist/types/services/task/voice/Voice.d.ts +52 -9
- package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
- package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +55 -0
- package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
- package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
- package/dist/types/types.d.ts +14 -8
- package/dist/types/utils/PageCache.d.ts +20 -3
- package/dist/types.js +7 -2
- package/dist/types.js.map +1 -1
- package/dist/utils/PageCache.js +19 -5
- package/dist/utils/PageCache.js.map +1 -1
- package/dist/webex.js +1 -1
- package/package.json +8 -8
- package/src/cc.ts +657 -12
- package/src/config.ts +7 -0
- package/src/constants.ts +8 -0
- package/src/index.ts +2 -0
- package/src/metrics/MetricsManager.ts +16 -0
- package/src/metrics/ai-docs/metrics-spec.md +78 -10
- package/src/metrics/behavioral-events.ts +106 -0
- package/src/metrics/constants.ts +20 -0
- package/src/services/AddressBook.ts +17 -6
- package/src/services/AnswerCallOnWebexService.ts +239 -0
- package/src/services/EntryPoint.ts +59 -60
- package/src/services/Queue.ts +29 -12
- package/src/services/WebexCrossClientService.ts +293 -0
- package/src/services/WxAppTelephonyMercurySync.ts +142 -0
- package/src/services/agent/ai-docs/agent-spec.md +1 -0
- package/src/services/ai-docs/AGENTS.md +10 -10
- package/src/services/ai-docs/services-spec.md +6 -1
- package/src/services/config/Util.ts +3 -0
- package/src/services/config/ai-docs/AGENTS.md +1 -1
- package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
- package/src/services/config/ai-docs/config-spec.md +6 -0
- package/src/services/config/constants.ts +10 -6
- package/src/services/config/types.ts +16 -4
- package/src/services/core/Err.ts +1 -0
- package/src/services/core/WebexRequest.ts +3 -1
- package/src/services/core/ai-docs/core-spec.md +5 -1
- package/src/services/core/aqm-reqs.ts +30 -15
- package/src/services/core/types.ts +2 -0
- package/src/services/task/Task.ts +79 -8
- package/src/services/task/TaskFactory.ts +8 -3
- package/src/services/task/TaskManager.ts +716 -17
- package/src/services/task/WebexCallingUtils.ts +136 -0
- package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
- package/src/services/task/ai-docs/task-spec.md +156 -3
- package/src/services/task/constants.ts +3 -0
- package/src/services/task/contact.ts +30 -0
- package/src/services/task/digital/Digital.ts +4 -2
- package/src/services/task/state-machine/TaskStateMachine.ts +235 -81
- package/src/services/task/state-machine/actions.ts +5 -2
- package/src/services/task/state-machine/ai-docs/AGENTS.md +6 -4
- package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +35 -16
- package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +103 -33
- package/src/services/task/state-machine/guards.ts +7 -10
- package/src/services/task/state-machine/types.ts +16 -1
- package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
- package/src/services/task/types.ts +114 -4
- package/src/services/task/voice/Voice.ts +595 -14
- package/src/services/task/voice/WebRTC.ts +5 -1
- package/src/services/task/voice/wxAppVoiceMethods.ts +516 -0
- package/src/services/wxAppDiagnosticLogging.ts +174 -0
- package/src/services/wxAppTelephonyUtils.ts +14 -0
- package/src/types.ts +33 -9
- package/src/utils/AGENTS.md +21 -10
- package/src/utils/PageCache.ts +38 -5
- package/src/utils/ai-docs/utils-spec.md +21 -11
- package/test/unit/spec/cc.ts +1522 -19
- package/test/unit/spec/metrics/MetricsManager.ts +25 -0
- package/test/unit/spec/metrics/behavioral-events.ts +60 -0
- package/test/unit/spec/services/AddressBook.ts +37 -6
- package/test/unit/spec/services/AnswerCallOnWebexService.ts +264 -0
- package/test/unit/spec/services/EntryPoint.ts +87 -40
- package/test/unit/spec/services/Queue.ts +123 -12
- package/test/unit/spec/services/WebexCrossClientService.ts +467 -0
- package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +136 -0
- package/test/unit/spec/services/config/Util.ts +85 -0
- package/test/unit/spec/services/core/WebexRequest.ts +3 -1
- package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
- package/test/unit/spec/services/task/Task.ts +274 -6
- package/test/unit/spec/services/task/TaskFactory.ts +39 -2
- package/test/unit/spec/services/task/TaskManager.ts +1717 -9
- package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
- package/test/unit/spec/services/task/contact.ts +33 -0
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +1485 -109
- package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
- package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
- package/test/unit/spec/services/task/voice/Voice.ts +1412 -0
- package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +603 -0
- package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -7,7 +7,7 @@ import TaskManager from '../../../../../src/services/task/TaskManager';
|
|
|
7
7
|
import * as contact from '../../../../../src/services/task/contact';
|
|
8
8
|
import Task from '../../../../../src/services/task/Task';
|
|
9
9
|
import {TASK_EVENTS} from '../../../../../src/services/task/types';
|
|
10
|
-
import {TaskEvent} from '../../../../../src/services/task/state-machine';
|
|
10
|
+
import {TaskEvent, TaskState} from '../../../../../src/services/task/state-machine';
|
|
11
11
|
import WebRTC from '../../../../../src/services/task/voice/WebRTC';
|
|
12
12
|
import {Profile} from '../../../../../src/services/config/types';
|
|
13
13
|
import WebCallingService from '../../../../../src/services/WebCallingService';
|
|
@@ -51,6 +51,7 @@ describe('TaskManager', () => {
|
|
|
51
51
|
[TaskEvent.TASK_OFFERED]: TASK_EVENTS.TASK_OFFER_CONTACT,
|
|
52
52
|
[TaskEvent.OFFER_CONSULT]: TASK_EVENTS.TASK_OFFER_CONSULT,
|
|
53
53
|
[TaskEvent.HYDRATE]: TASK_EVENTS.TASK_HYDRATE,
|
|
54
|
+
[TaskEvent.CONTACT_OWNER_CHANGED]: TASK_EVENTS.TASK_HYDRATE,
|
|
54
55
|
[TaskEvent.ASSIGN]: TASK_EVENTS.TASK_ASSIGNED,
|
|
55
56
|
[TaskEvent.HOLD_SUCCESS]: TASK_EVENTS.TASK_HOLD,
|
|
56
57
|
[TaskEvent.UNHOLD_SUCCESS]: TASK_EVENTS.TASK_RESUME,
|
|
@@ -86,7 +87,9 @@ describe('TaskManager', () => {
|
|
|
86
87
|
) {
|
|
87
88
|
task.emit(mappedEvent, event.reason ?? event.taskData?.reason);
|
|
88
89
|
} else if (event.type === TaskEvent.OUTBOUND_FAILED) {
|
|
89
|
-
|
|
90
|
+
if (!event.taskData?.suppressOutdialFailedPopup) {
|
|
91
|
+
task.emit(mappedEvent, event.reason);
|
|
92
|
+
}
|
|
90
93
|
} else {
|
|
91
94
|
task.emit(mappedEvent, task);
|
|
92
95
|
}
|
|
@@ -1468,6 +1471,368 @@ describe('TaskManager', () => {
|
|
|
1468
1471
|
expect(removeTaskSpy).toHaveBeenCalled();
|
|
1469
1472
|
});
|
|
1470
1473
|
|
|
1474
|
+
it('should map non-wxApp agent-terminated OUTDIAL ContactEnded to CONTACT_ENDED (not OUTBOUND_FAILED)', () => {
|
|
1475
|
+
const task = taskManager.getTask(taskId);
|
|
1476
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1477
|
+
const outdialFailedSpy = jest.fn();
|
|
1478
|
+
task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
|
|
1479
|
+
|
|
1480
|
+
const payload = {
|
|
1481
|
+
data: {
|
|
1482
|
+
type: CC_EVENTS.CONTACT_ENDED,
|
|
1483
|
+
interactionId: taskId,
|
|
1484
|
+
terminatingParty: 'Agent',
|
|
1485
|
+
interaction: {
|
|
1486
|
+
outboundType: 'OUTDIAL',
|
|
1487
|
+
state: 'wrapUp',
|
|
1488
|
+
mediaType: 'telephony',
|
|
1489
|
+
},
|
|
1490
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1491
|
+
},
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1495
|
+
|
|
1496
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONTACT_ENDED);
|
|
1497
|
+
expect(outdialFailedSpy).not.toHaveBeenCalled();
|
|
1498
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
it('should map non-wxApp agent-terminated OUTDIAL AgentOutboundFailed to OUTBOUND_FAILED with popup suppressed', () => {
|
|
1502
|
+
const task = taskManager.getTask(taskId);
|
|
1503
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1504
|
+
const outdialFailedSpy = jest.fn();
|
|
1505
|
+
task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
|
|
1506
|
+
|
|
1507
|
+
const payload = {
|
|
1508
|
+
data: {
|
|
1509
|
+
type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
|
|
1510
|
+
interactionId: taskId,
|
|
1511
|
+
terminatingParty: 'Agent',
|
|
1512
|
+
reason: 'AGENT_ENDS',
|
|
1513
|
+
interaction: {
|
|
1514
|
+
outboundType: 'OUTDIAL',
|
|
1515
|
+
state: 'wrapUp',
|
|
1516
|
+
mediaType: 'telephony',
|
|
1517
|
+
},
|
|
1518
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1519
|
+
},
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1523
|
+
|
|
1524
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1525
|
+
sendStateMachineEventSpy,
|
|
1526
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1527
|
+
);
|
|
1528
|
+
expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
|
|
1529
|
+
expect(stateMachineEvent?.taskData?.suppressOutdialFailedPopup).toBe(true);
|
|
1530
|
+
expect(outdialFailedSpy).not.toHaveBeenCalled();
|
|
1531
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
it('should map wxApp agent-terminated OUTDIAL AgentOutboundFailed to OUTBOUND_FAILED with AGENT_ENDS', () => {
|
|
1535
|
+
const task = taskManager.getTask(taskId);
|
|
1536
|
+
task.uiControlConfig = {enableWxBetterTogether: true};
|
|
1537
|
+
task.data = {
|
|
1538
|
+
...task.data,
|
|
1539
|
+
agentId: 'test-agent-id',
|
|
1540
|
+
interaction: {
|
|
1541
|
+
...task.data.interaction,
|
|
1542
|
+
outboundType: 'OUTDIAL',
|
|
1543
|
+
participants: {
|
|
1544
|
+
'test-agent-id': {
|
|
1545
|
+
id: 'test-agent-id',
|
|
1546
|
+
deviceType: 'wxApp',
|
|
1547
|
+
deviceId: 'device-1',
|
|
1548
|
+
deviceCallId: 'call-1',
|
|
1549
|
+
},
|
|
1550
|
+
},
|
|
1551
|
+
},
|
|
1552
|
+
};
|
|
1553
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1554
|
+
const outdialFailedSpy = jest.fn();
|
|
1555
|
+
task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
|
|
1556
|
+
|
|
1557
|
+
const payload = {
|
|
1558
|
+
data: {
|
|
1559
|
+
type: CC_EVENTS.AGENT_OUTBOUND_FAILED,
|
|
1560
|
+
interactionId: taskId,
|
|
1561
|
+
terminatingParty: 'Agent',
|
|
1562
|
+
reason: 'AGENT_ENDS',
|
|
1563
|
+
interaction: {
|
|
1564
|
+
outboundType: 'OUTDIAL',
|
|
1565
|
+
state: 'wrapUp',
|
|
1566
|
+
mediaType: 'telephony',
|
|
1567
|
+
participants: {
|
|
1568
|
+
'test-agent-id': {
|
|
1569
|
+
id: 'test-agent-id',
|
|
1570
|
+
deviceType: 'wxApp',
|
|
1571
|
+
deviceId: 'device-1',
|
|
1572
|
+
deviceCallId: 'call-1',
|
|
1573
|
+
},
|
|
1574
|
+
},
|
|
1575
|
+
},
|
|
1576
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1577
|
+
},
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1581
|
+
|
|
1582
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1583
|
+
sendStateMachineEventSpy,
|
|
1584
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1585
|
+
);
|
|
1586
|
+
expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
|
|
1587
|
+
expect(outdialFailedSpy).toHaveBeenCalledWith('AGENT_ENDS');
|
|
1588
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
it('should map wxApp agent-terminated OUTDIAL ContactEnded to OUTBOUND_FAILED with AGENT_ENDS', () => {
|
|
1592
|
+
const task = taskManager.getTask(taskId);
|
|
1593
|
+
task.uiControlConfig = {enableWxBetterTogether: true};
|
|
1594
|
+
task.data = {
|
|
1595
|
+
...task.data,
|
|
1596
|
+
agentId: 'test-agent-id',
|
|
1597
|
+
interaction: {
|
|
1598
|
+
...task.data.interaction,
|
|
1599
|
+
outboundType: 'OUTDIAL',
|
|
1600
|
+
participants: {
|
|
1601
|
+
'test-agent-id': {
|
|
1602
|
+
id: 'test-agent-id',
|
|
1603
|
+
deviceType: 'wxApp',
|
|
1604
|
+
deviceId: 'device-1',
|
|
1605
|
+
deviceCallId: 'call-1',
|
|
1606
|
+
},
|
|
1607
|
+
},
|
|
1608
|
+
},
|
|
1609
|
+
};
|
|
1610
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1611
|
+
const outdialFailedSpy = jest.fn();
|
|
1612
|
+
task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
|
|
1613
|
+
|
|
1614
|
+
const payload = {
|
|
1615
|
+
data: {
|
|
1616
|
+
type: CC_EVENTS.CONTACT_ENDED,
|
|
1617
|
+
interactionId: taskId,
|
|
1618
|
+
terminatingParty: 'Agent',
|
|
1619
|
+
interaction: {
|
|
1620
|
+
outboundType: 'OUTDIAL',
|
|
1621
|
+
state: 'wrapUp',
|
|
1622
|
+
mediaType: 'telephony',
|
|
1623
|
+
participants: {
|
|
1624
|
+
'test-agent-id': {
|
|
1625
|
+
id: 'test-agent-id',
|
|
1626
|
+
deviceType: 'wxApp',
|
|
1627
|
+
deviceId: 'device-1',
|
|
1628
|
+
deviceCallId: 'call-1',
|
|
1629
|
+
},
|
|
1630
|
+
},
|
|
1631
|
+
},
|
|
1632
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1633
|
+
},
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1637
|
+
|
|
1638
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1639
|
+
sendStateMachineEventSpy,
|
|
1640
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1641
|
+
);
|
|
1642
|
+
expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
|
|
1643
|
+
expect(stateMachineEvent?.taskData?.wrapUpRequired).toBe(true);
|
|
1644
|
+
expect(outdialFailedSpy).toHaveBeenCalledWith('AGENT_ENDS');
|
|
1645
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
it('should map offer-stage OUTDIAL decline ContactEnded to CONTACT_ENDED (not wrapup)', () => {
|
|
1649
|
+
const task = taskManager.getTask(taskId);
|
|
1650
|
+
task.stateMachineService = {
|
|
1651
|
+
getSnapshot: () => ({value: 'OFFERED'}),
|
|
1652
|
+
};
|
|
1653
|
+
task.uiControlConfig = {enableWxBetterTogether: true, wxAppAnswerPending: false};
|
|
1654
|
+
task.data = {
|
|
1655
|
+
...task.data,
|
|
1656
|
+
agentId: 'test-agent-id',
|
|
1657
|
+
interaction: {
|
|
1658
|
+
...task.data.interaction,
|
|
1659
|
+
outboundType: 'OUTDIAL',
|
|
1660
|
+
participants: {
|
|
1661
|
+
'test-agent-id': {
|
|
1662
|
+
id: 'test-agent-id',
|
|
1663
|
+
deviceType: 'wxApp',
|
|
1664
|
+
deviceId: 'device-1',
|
|
1665
|
+
deviceCallId: 'call-1',
|
|
1666
|
+
},
|
|
1667
|
+
},
|
|
1668
|
+
},
|
|
1669
|
+
};
|
|
1670
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1671
|
+
const outdialFailedSpy = jest.fn();
|
|
1672
|
+
task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
|
|
1673
|
+
|
|
1674
|
+
const payload = {
|
|
1675
|
+
data: {
|
|
1676
|
+
type: CC_EVENTS.CONTACT_ENDED,
|
|
1677
|
+
interactionId: taskId,
|
|
1678
|
+
terminatingParty: 'Agent',
|
|
1679
|
+
interaction: {
|
|
1680
|
+
outboundType: 'OUTDIAL',
|
|
1681
|
+
state: 'wrapUp',
|
|
1682
|
+
mediaType: 'telephony',
|
|
1683
|
+
},
|
|
1684
|
+
agentsPendingWrapUp: [],
|
|
1685
|
+
},
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1688
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1689
|
+
|
|
1690
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONTACT_ENDED);
|
|
1691
|
+
expect(outdialFailedSpy).not.toHaveBeenCalled();
|
|
1692
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1693
|
+
});
|
|
1694
|
+
|
|
1695
|
+
it('should map post-accept OUTDIAL ContactEnded to OUTBOUND_FAILED when still OFFERED with wxAppAnswerPending', () => {
|
|
1696
|
+
const task = taskManager.getTask(taskId);
|
|
1697
|
+
task.stateMachineService = {
|
|
1698
|
+
getSnapshot: () => ({value: 'OFFERED'}),
|
|
1699
|
+
};
|
|
1700
|
+
task.uiControlConfig = {enableWxBetterTogether: true, wxAppAnswerPending: true};
|
|
1701
|
+
task.data = {
|
|
1702
|
+
...task.data,
|
|
1703
|
+
agentId: 'test-agent-id',
|
|
1704
|
+
interaction: {
|
|
1705
|
+
...task.data.interaction,
|
|
1706
|
+
outboundType: 'OUTDIAL',
|
|
1707
|
+
participants: {
|
|
1708
|
+
'test-agent-id': {
|
|
1709
|
+
id: 'test-agent-id',
|
|
1710
|
+
deviceType: 'wxApp',
|
|
1711
|
+
deviceId: 'device-1',
|
|
1712
|
+
deviceCallId: 'call-1',
|
|
1713
|
+
},
|
|
1714
|
+
},
|
|
1715
|
+
},
|
|
1716
|
+
};
|
|
1717
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1718
|
+
const outdialFailedSpy = jest.fn();
|
|
1719
|
+
task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
|
|
1720
|
+
|
|
1721
|
+
const payload = {
|
|
1722
|
+
data: {
|
|
1723
|
+
type: CC_EVENTS.CONTACT_ENDED,
|
|
1724
|
+
interactionId: taskId,
|
|
1725
|
+
terminatingParty: 'Agent',
|
|
1726
|
+
interaction: {
|
|
1727
|
+
outboundType: 'OUTDIAL',
|
|
1728
|
+
state: 'offered',
|
|
1729
|
+
mediaType: 'telephony',
|
|
1730
|
+
participants: {
|
|
1731
|
+
'test-agent-id': {
|
|
1732
|
+
id: 'test-agent-id',
|
|
1733
|
+
deviceType: 'wxApp',
|
|
1734
|
+
deviceId: 'device-1',
|
|
1735
|
+
deviceCallId: 'call-1',
|
|
1736
|
+
},
|
|
1737
|
+
},
|
|
1738
|
+
},
|
|
1739
|
+
},
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1743
|
+
|
|
1744
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1745
|
+
sendStateMachineEventSpy,
|
|
1746
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1747
|
+
);
|
|
1748
|
+
expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
|
|
1749
|
+
expect(outdialFailedSpy).toHaveBeenCalledWith('AGENT_ENDS');
|
|
1750
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
it('should map post-accept agent-terminated OUTDIAL ContactEnded to OUTBOUND_FAILED when CONNECTED', () => {
|
|
1754
|
+
const task = taskManager.getTask(taskId);
|
|
1755
|
+
task.stateMachineService = {
|
|
1756
|
+
getSnapshot: () => ({value: 'CONNECTED'}),
|
|
1757
|
+
};
|
|
1758
|
+
task.uiControlConfig = {enableWxBetterTogether: true};
|
|
1759
|
+
task.data = {
|
|
1760
|
+
...task.data,
|
|
1761
|
+
agentId: 'test-agent-id',
|
|
1762
|
+
interaction: {
|
|
1763
|
+
...task.data.interaction,
|
|
1764
|
+
outboundType: 'OUTDIAL',
|
|
1765
|
+
participants: {
|
|
1766
|
+
'test-agent-id': {
|
|
1767
|
+
id: 'test-agent-id',
|
|
1768
|
+
deviceType: 'wxApp',
|
|
1769
|
+
deviceId: 'device-1',
|
|
1770
|
+
deviceCallId: 'call-1',
|
|
1771
|
+
},
|
|
1772
|
+
},
|
|
1773
|
+
},
|
|
1774
|
+
};
|
|
1775
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1776
|
+
const outdialFailedSpy = jest.fn();
|
|
1777
|
+
task.on(TASK_EVENTS.TASK_OUTDIAL_FAILED, outdialFailedSpy);
|
|
1778
|
+
|
|
1779
|
+
const payload = {
|
|
1780
|
+
data: {
|
|
1781
|
+
type: CC_EVENTS.CONTACT_ENDED,
|
|
1782
|
+
interactionId: taskId,
|
|
1783
|
+
terminatingParty: 'Agent',
|
|
1784
|
+
interaction: {
|
|
1785
|
+
outboundType: 'OUTDIAL',
|
|
1786
|
+
state: 'wrapUp',
|
|
1787
|
+
mediaType: 'telephony',
|
|
1788
|
+
participants: {
|
|
1789
|
+
'test-agent-id': {
|
|
1790
|
+
id: 'test-agent-id',
|
|
1791
|
+
deviceType: 'wxApp',
|
|
1792
|
+
deviceId: 'device-1',
|
|
1793
|
+
deviceCallId: 'call-1',
|
|
1794
|
+
},
|
|
1795
|
+
},
|
|
1796
|
+
},
|
|
1797
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1798
|
+
},
|
|
1799
|
+
};
|
|
1800
|
+
|
|
1801
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1802
|
+
|
|
1803
|
+
const stateMachineEvent = expectLastStateMachineEvent(
|
|
1804
|
+
sendStateMachineEventSpy,
|
|
1805
|
+
TaskEvent.OUTBOUND_FAILED
|
|
1806
|
+
);
|
|
1807
|
+
expect(stateMachineEvent?.reason).toBe('AGENT_ENDS');
|
|
1808
|
+
expect(outdialFailedSpy).toHaveBeenCalledWith('AGENT_ENDS');
|
|
1809
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
it('should map customer-terminated OUTDIAL ContactEnded to CONTACT_ENDED (no AGENT_ENDS remap)', () => {
|
|
1813
|
+
const task = taskManager.getTask(taskId);
|
|
1814
|
+
const sendStateMachineEventSpy = jest.spyOn(task, 'sendStateMachineEvent');
|
|
1815
|
+
|
|
1816
|
+
const payload = {
|
|
1817
|
+
data: {
|
|
1818
|
+
type: CC_EVENTS.CONTACT_ENDED,
|
|
1819
|
+
interactionId: taskId,
|
|
1820
|
+
terminatingParty: 'Customer',
|
|
1821
|
+
interaction: {
|
|
1822
|
+
outboundType: 'OUTDIAL',
|
|
1823
|
+
state: 'new',
|
|
1824
|
+
mediaType: 'telephony',
|
|
1825
|
+
},
|
|
1826
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
1827
|
+
},
|
|
1828
|
+
};
|
|
1829
|
+
|
|
1830
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
1831
|
+
|
|
1832
|
+
expectLastStateMachineEvent(sendStateMachineEventSpy, TaskEvent.CONTACT_ENDED);
|
|
1833
|
+
sendStateMachineEventSpy.mockRestore();
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1471
1836
|
it('should handle CONTACT_ENDED gracefully when task is undefined', () => {
|
|
1472
1837
|
const payload = {
|
|
1473
1838
|
data: {
|
|
@@ -2252,6 +2617,22 @@ describe('TaskManager', () => {
|
|
|
2252
2617
|
sendStateMachineEventSpy.mockRestore();
|
|
2253
2618
|
});
|
|
2254
2619
|
|
|
2620
|
+
it('stamps wrapUpRequired on AGENT_WRAPUP when the payload omits it', () => {
|
|
2621
|
+
const payload = {
|
|
2622
|
+
data: {
|
|
2623
|
+
type: CC_EVENTS.AGENT_WRAPUP,
|
|
2624
|
+
interactionId: taskId,
|
|
2625
|
+
},
|
|
2626
|
+
};
|
|
2627
|
+
const task = taskManager.getTask(taskId);
|
|
2628
|
+
webSocketManagerMock.emit('message', JSON.stringify(payload));
|
|
2629
|
+
const stateMachineEvent = task.sendStateMachineEvent.mock.calls.at(-1)?.[0];
|
|
2630
|
+
|
|
2631
|
+
expect(stateMachineEvent?.type).toBe(TaskEvent.TASK_WRAPUP);
|
|
2632
|
+
expect(stateMachineEvent?.taskData.wrapUpRequired).toBe(true);
|
|
2633
|
+
expect(task.data.wrapUpRequired).toBe(true);
|
|
2634
|
+
});
|
|
2635
|
+
|
|
2255
2636
|
it('should not attempt cleanup twice when AGENT_CONTACT_UNASSIGNED is followed by AGENT_WRAPUP', () => {
|
|
2256
2637
|
webSocketManagerMock.emit('message', JSON.stringify(initalPayload));
|
|
2257
2638
|
const task = taskManager.getTask(taskId);
|
|
@@ -2359,14 +2740,654 @@ describe('TaskManager', () => {
|
|
|
2359
2740
|
});
|
|
2360
2741
|
});
|
|
2361
2742
|
|
|
2362
|
-
describe('
|
|
2363
|
-
|
|
2743
|
+
describe('Contact owner propagation', () => {
|
|
2744
|
+
const currentAgentId = 'test-agent-id';
|
|
2745
|
+
const previousOwnerId = 'previous-owner-id';
|
|
2746
|
+
const promotedOwnerId = 'promoted-owner-id';
|
|
2364
2747
|
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2748
|
+
const createConferenceTaskData = (
|
|
2749
|
+
interactionId: string,
|
|
2750
|
+
owner: string,
|
|
2751
|
+
mainInteractionId = taskId
|
|
2752
|
+
) => ({
|
|
2753
|
+
...taskDataMock,
|
|
2754
|
+
agentId: currentAgentId,
|
|
2755
|
+
interactionId,
|
|
2756
|
+
owner,
|
|
2757
|
+
interaction: {
|
|
2758
|
+
mediaType: 'telephony',
|
|
2759
|
+
interactionId,
|
|
2760
|
+
mainInteractionId,
|
|
2761
|
+
owner,
|
|
2762
|
+
participants: {
|
|
2763
|
+
[currentAgentId]: {
|
|
2764
|
+
id: currentAgentId,
|
|
2765
|
+
pType: 'Agent',
|
|
2766
|
+
hasJoined: true,
|
|
2767
|
+
hasLeft: false,
|
|
2768
|
+
},
|
|
2769
|
+
[previousOwnerId]: {
|
|
2770
|
+
id: previousOwnerId,
|
|
2771
|
+
pType: 'Agent',
|
|
2772
|
+
hasJoined: true,
|
|
2773
|
+
hasLeft: false,
|
|
2774
|
+
},
|
|
2775
|
+
[promotedOwnerId]: {
|
|
2776
|
+
id: promotedOwnerId,
|
|
2777
|
+
pType: 'Agent',
|
|
2778
|
+
hasJoined: true,
|
|
2779
|
+
hasLeft: false,
|
|
2780
|
+
},
|
|
2781
|
+
},
|
|
2782
|
+
media: {
|
|
2783
|
+
[mainInteractionId]: {
|
|
2784
|
+
mediaResourceId: mainInteractionId,
|
|
2785
|
+
mediaType: 'telephony',
|
|
2786
|
+
mType: 'mainCall',
|
|
2787
|
+
participants: [currentAgentId, previousOwnerId, promotedOwnerId],
|
|
2788
|
+
},
|
|
2789
|
+
},
|
|
2790
|
+
},
|
|
2791
|
+
});
|
|
2792
|
+
|
|
2793
|
+
const installTask = (data) => {
|
|
2794
|
+
const task = createMockTask(data);
|
|
2795
|
+
taskManager.taskCollection = {[data.interactionId]: task};
|
|
2796
|
+
(taskManager as any).setupTaskListeners(task);
|
|
2797
|
+
|
|
2798
|
+
return task;
|
|
2799
|
+
};
|
|
2800
|
+
|
|
2801
|
+
const createPromotedAgentOwnerChangeData = (
|
|
2802
|
+
interactionId = 'promoted-child-interaction-id',
|
|
2803
|
+
mainInteractionId = taskId
|
|
2804
|
+
) => {
|
|
2805
|
+
const data = createConferenceTaskData(interactionId, currentAgentId, mainInteractionId);
|
|
2806
|
+
|
|
2807
|
+
data.interaction.state = 'conference';
|
|
2808
|
+
|
|
2809
|
+
return {...data, type: CC_EVENTS.CONTACT_OWNER_CHANGED};
|
|
2810
|
+
};
|
|
2811
|
+
|
|
2812
|
+
const clearTasksAndFactoryHistory = () => {
|
|
2813
|
+
taskManager.taskCollection = {};
|
|
2814
|
+
(TaskFactory.createTask as jest.Mock).mockClear();
|
|
2815
|
+
};
|
|
2816
|
+
|
|
2817
|
+
it('emits one hydrate when ContactUpdated changes interaction.owner', () => {
|
|
2818
|
+
const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
2819
|
+
const hydrateHandler = jest.fn();
|
|
2820
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
2821
|
+
|
|
2822
|
+
webSocketManagerMock.emit(
|
|
2823
|
+
'message',
|
|
2824
|
+
JSON.stringify({
|
|
2825
|
+
data: {
|
|
2826
|
+
...createConferenceTaskData(taskId, promotedOwnerId),
|
|
2827
|
+
type: CC_EVENTS.CONTACT_UPDATED,
|
|
2828
|
+
},
|
|
2829
|
+
})
|
|
2830
|
+
);
|
|
2831
|
+
|
|
2832
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
2833
|
+
expect(task.data.interaction.owner).toBe(promotedOwnerId);
|
|
2834
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
2835
|
+
expect(hydrateHandler).toHaveBeenCalledWith(task);
|
|
2836
|
+
});
|
|
2837
|
+
|
|
2838
|
+
it.each([
|
|
2839
|
+
['the same owner', previousOwnerId],
|
|
2840
|
+
['no owner', undefined],
|
|
2841
|
+
['a blank owner', ' '],
|
|
2842
|
+
])('keeps ContactUpdated data-only when it carries %s', (_description, owner) => {
|
|
2843
|
+
const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
2844
|
+
const hydrateHandler = jest.fn();
|
|
2845
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
2846
|
+
const interaction = createConferenceTaskData(taskId, previousOwnerId).interaction;
|
|
2847
|
+
if (owner === undefined) {
|
|
2848
|
+
delete interaction.owner;
|
|
2849
|
+
} else {
|
|
2850
|
+
interaction.owner = owner;
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
webSocketManagerMock.emit(
|
|
2854
|
+
'message',
|
|
2855
|
+
JSON.stringify({
|
|
2856
|
+
data: {
|
|
2857
|
+
...createConferenceTaskData(taskId, previousOwnerId),
|
|
2858
|
+
type: CC_EVENTS.CONTACT_UPDATED,
|
|
2859
|
+
interaction,
|
|
2860
|
+
},
|
|
2861
|
+
})
|
|
2862
|
+
);
|
|
2863
|
+
|
|
2864
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_UPDATED);
|
|
2865
|
+
expect(hydrateHandler).not.toHaveBeenCalled();
|
|
2866
|
+
});
|
|
2867
|
+
|
|
2868
|
+
it('correlates child-keyed ContactOwnerChanged and keeps the main task identity', () => {
|
|
2869
|
+
const childTaskId = 'surviving-child-id';
|
|
2870
|
+
const promotedChildId = 'promoted-child-id';
|
|
2871
|
+
const task = installTask(
|
|
2872
|
+
createConferenceTaskData(childTaskId, previousOwnerId, taskId)
|
|
2873
|
+
);
|
|
2874
|
+
const hydrateHandler = jest.fn();
|
|
2875
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
2876
|
+
|
|
2877
|
+
const ownerChangeData = createConferenceTaskData(
|
|
2878
|
+
promotedChildId,
|
|
2879
|
+
promotedOwnerId,
|
|
2880
|
+
taskId
|
|
2881
|
+
);
|
|
2882
|
+
ownerChangeData.interaction.interactionId = taskId;
|
|
2883
|
+
webSocketManagerMock.emit(
|
|
2884
|
+
'message',
|
|
2885
|
+
JSON.stringify({
|
|
2886
|
+
data: {...ownerChangeData, type: CC_EVENTS.CONTACT_OWNER_CHANGED},
|
|
2887
|
+
})
|
|
2888
|
+
);
|
|
2889
|
+
|
|
2890
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
2891
|
+
expect(task.data.interactionId).toBe(taskId);
|
|
2892
|
+
expect(task.data.interaction.owner).toBe(promotedOwnerId);
|
|
2893
|
+
expect(taskManager.taskCollection[childTaskId]).toBeUndefined();
|
|
2894
|
+
expect(taskManager.taskCollection[promotedChildId]).toBeUndefined();
|
|
2895
|
+
expect(taskManager.taskCollection[taskId]).toBe(task);
|
|
2896
|
+
expect(Object.values(taskManager.taskCollection).filter((entry) => entry === task)).toHaveLength(
|
|
2897
|
+
1
|
|
2898
|
+
);
|
|
2899
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
2900
|
+
});
|
|
2901
|
+
|
|
2902
|
+
it('uses the incoming promotion snapshot to correlate a stale child-keyed task', () => {
|
|
2903
|
+
const childTaskId = 'stale-child-interaction-id';
|
|
2904
|
+
const staleTaskData = createConferenceTaskData(childTaskId, previousOwnerId, taskId);
|
|
2905
|
+
staleTaskData.interaction.media = {
|
|
2906
|
+
[childTaskId]: {
|
|
2907
|
+
mediaResourceId: childTaskId,
|
|
2908
|
+
mediaType: 'telephony',
|
|
2909
|
+
mType: 'consult',
|
|
2910
|
+
participants: [currentAgentId, promotedOwnerId],
|
|
2911
|
+
},
|
|
2912
|
+
};
|
|
2913
|
+
const task = installTask(staleTaskData);
|
|
2914
|
+
const hydrateHandler = jest.fn();
|
|
2915
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
2916
|
+
|
|
2917
|
+
webSocketManagerMock.emit(
|
|
2918
|
+
'message',
|
|
2919
|
+
JSON.stringify({data: createPromotedAgentOwnerChangeData()})
|
|
2920
|
+
);
|
|
2921
|
+
|
|
2922
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
2923
|
+
expect(task.data.interaction.owner).toBe(currentAgentId);
|
|
2924
|
+
expect(taskManager.taskCollection[childTaskId]).toBeUndefined();
|
|
2925
|
+
expect(taskManager.taskCollection[taskId]).toBe(task);
|
|
2926
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
2927
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
2928
|
+
});
|
|
2929
|
+
|
|
2930
|
+
it('reuses a task stored under the incoming main-call media key', () => {
|
|
2931
|
+
const mainMediaInteractionId = 'main-media-interaction-id';
|
|
2932
|
+
const promotedChildId = 'promoted-child-without-main-id';
|
|
2933
|
+
const task = installTask(
|
|
2934
|
+
createConferenceTaskData(
|
|
2935
|
+
mainMediaInteractionId,
|
|
2936
|
+
previousOwnerId,
|
|
2937
|
+
mainMediaInteractionId
|
|
2938
|
+
)
|
|
2939
|
+
);
|
|
2940
|
+
const hydrateHandler = jest.fn();
|
|
2941
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
2942
|
+
const ownerChangeData = createPromotedAgentOwnerChangeData(
|
|
2943
|
+
promotedChildId,
|
|
2944
|
+
mainMediaInteractionId
|
|
2945
|
+
);
|
|
2946
|
+
delete ownerChangeData.interaction.mainInteractionId;
|
|
2947
|
+
|
|
2948
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
|
|
2949
|
+
|
|
2950
|
+
expect(task.sendStateMachineEvent).toHaveBeenCalledTimes(1);
|
|
2951
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
2952
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
2953
|
+
expect(taskManager.taskCollection[mainMediaInteractionId]).toBe(task);
|
|
2954
|
+
expect(taskManager.taskCollection[promotedChildId]).toBeUndefined();
|
|
2955
|
+
expect(Object.values(taskManager.taskCollection)).toEqual([task]);
|
|
2956
|
+
expect(task.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
|
|
2957
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
2958
|
+
expect(hydrateHandler).toHaveBeenCalledWith(task);
|
|
2959
|
+
});
|
|
2960
|
+
|
|
2961
|
+
it('prefers the exact task for ContactOwnerChanged over related child tasks', () => {
|
|
2962
|
+
const exactTask = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
2963
|
+
const childTaskId = 'related-child-id';
|
|
2964
|
+
const childTask = createMockTask(
|
|
2965
|
+
createConferenceTaskData(childTaskId, previousOwnerId, taskId)
|
|
2966
|
+
);
|
|
2967
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
2968
|
+
|
|
2969
|
+
webSocketManagerMock.emit(
|
|
2970
|
+
'message',
|
|
2971
|
+
JSON.stringify({
|
|
2972
|
+
data: {
|
|
2973
|
+
...createConferenceTaskData(taskId, promotedOwnerId),
|
|
2974
|
+
type: CC_EVENTS.CONTACT_OWNER_CHANGED,
|
|
2975
|
+
},
|
|
2976
|
+
})
|
|
2977
|
+
);
|
|
2978
|
+
|
|
2979
|
+
expectLastStateMachineEvent(exactTask.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
2980
|
+
expect(childTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
2981
|
+
});
|
|
2982
|
+
|
|
2983
|
+
it('ignores a related ContactOwnerChanged task whose current agent is consult-only', () => {
|
|
2984
|
+
const childTaskId = 'consult-only-child-id';
|
|
2985
|
+
const data = createConferenceTaskData(childTaskId, previousOwnerId, taskId);
|
|
2986
|
+
data.interaction.media[taskId].participants = [previousOwnerId, promotedOwnerId];
|
|
2987
|
+
data.interaction.media[childTaskId] = {
|
|
2988
|
+
mediaResourceId: childTaskId,
|
|
2989
|
+
mediaType: 'telephony',
|
|
2990
|
+
mType: 'consult',
|
|
2991
|
+
participants: [currentAgentId, promotedOwnerId],
|
|
2992
|
+
};
|
|
2993
|
+
const task = installTask(data);
|
|
2994
|
+
|
|
2995
|
+
webSocketManagerMock.emit(
|
|
2996
|
+
'message',
|
|
2997
|
+
JSON.stringify({
|
|
2998
|
+
data: {
|
|
2999
|
+
...createConferenceTaskData('unmatched-child-id', promotedOwnerId, taskId),
|
|
3000
|
+
type: CC_EVENTS.CONTACT_OWNER_CHANGED,
|
|
3001
|
+
},
|
|
3002
|
+
})
|
|
3003
|
+
);
|
|
3004
|
+
|
|
3005
|
+
expect(task.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3006
|
+
expect(taskManager.taskCollection[childTaskId]).toBe(task);
|
|
3007
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3008
|
+
});
|
|
3009
|
+
|
|
3010
|
+
it.each([
|
|
3011
|
+
[
|
|
3012
|
+
'the current agent is marked departed',
|
|
3013
|
+
(data) => {
|
|
3014
|
+
data.interaction.participants[currentAgentId].hasLeft = true;
|
|
3015
|
+
},
|
|
3016
|
+
],
|
|
3017
|
+
[
|
|
3018
|
+
'the current agent participant is missing',
|
|
3019
|
+
(data) => {
|
|
3020
|
+
delete data.interaction.participants[currentAgentId];
|
|
3021
|
+
},
|
|
3022
|
+
],
|
|
3023
|
+
[
|
|
3024
|
+
'the current agent is present only on a consult leg',
|
|
3025
|
+
(data) => {
|
|
3026
|
+
data.interaction.media[taskId].mType = 'consult';
|
|
3027
|
+
},
|
|
3028
|
+
],
|
|
3029
|
+
])('does not recover a promoted task when %s', (_description, mutatePayload) => {
|
|
3030
|
+
clearTasksAndFactoryHistory();
|
|
3031
|
+
const ownerChangeData = createPromotedAgentOwnerChangeData();
|
|
3032
|
+
mutatePayload(ownerChangeData);
|
|
3033
|
+
|
|
3034
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
|
|
3035
|
+
|
|
3036
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3037
|
+
expect(taskManager.getAllTasks()).toEqual({});
|
|
3038
|
+
});
|
|
3039
|
+
|
|
3040
|
+
it('ignores ambiguous active main-call matches for ContactOwnerChanged', () => {
|
|
3041
|
+
const firstTask = createMockTask(
|
|
3042
|
+
createConferenceTaskData('first-child-id', previousOwnerId, taskId)
|
|
3043
|
+
);
|
|
3044
|
+
const secondTask = createMockTask(
|
|
3045
|
+
createConferenceTaskData('second-child-id', previousOwnerId, taskId)
|
|
3046
|
+
);
|
|
3047
|
+
taskManager.taskCollection = {
|
|
3048
|
+
'first-child-id': firstTask,
|
|
3049
|
+
'second-child-id': secondTask,
|
|
3050
|
+
};
|
|
3051
|
+
|
|
3052
|
+
const ownerChangeData = createConferenceTaskData(
|
|
3053
|
+
'unmatched-child-id',
|
|
3054
|
+
promotedOwnerId,
|
|
3055
|
+
taskId
|
|
3056
|
+
);
|
|
3057
|
+
ownerChangeData.interaction.interactionId = 'unmatched-child-id';
|
|
3058
|
+
webSocketManagerMock.emit(
|
|
3059
|
+
'message',
|
|
3060
|
+
JSON.stringify({
|
|
3061
|
+
data: {...ownerChangeData, type: CC_EVENTS.CONTACT_OWNER_CHANGED},
|
|
3062
|
+
})
|
|
3063
|
+
);
|
|
3064
|
+
|
|
3065
|
+
expect(firstTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3066
|
+
expect(secondTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3067
|
+
});
|
|
3068
|
+
|
|
3069
|
+
it('does not recover a new task when multiple stale related tasks become eligible from the payload', () => {
|
|
3070
|
+
const createStaleRelatedTask = (interactionId: string) => {
|
|
3071
|
+
const data = createConferenceTaskData(interactionId, previousOwnerId, taskId);
|
|
3072
|
+
data.interaction.media[taskId].participants = [previousOwnerId, promotedOwnerId];
|
|
3073
|
+
|
|
3074
|
+
return createMockTask(data);
|
|
3075
|
+
};
|
|
3076
|
+
const firstTask = createStaleRelatedTask('first-stale-child-id');
|
|
3077
|
+
const secondTask = createStaleRelatedTask('second-stale-child-id');
|
|
3078
|
+
taskManager.taskCollection = {
|
|
3079
|
+
'first-stale-child-id': firstTask,
|
|
3080
|
+
'second-stale-child-id': secondTask,
|
|
3081
|
+
};
|
|
3082
|
+
(TaskFactory.createTask as jest.Mock).mockClear();
|
|
3083
|
+
|
|
3084
|
+
webSocketManagerMock.emit(
|
|
3085
|
+
'message',
|
|
3086
|
+
JSON.stringify({data: createPromotedAgentOwnerChangeData()})
|
|
3087
|
+
);
|
|
3088
|
+
|
|
3089
|
+
expect(firstTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3090
|
+
expect(secondTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3091
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3092
|
+
expect(Object.keys(taskManager.taskCollection)).toEqual([
|
|
3093
|
+
'first-stale-child-id',
|
|
3094
|
+
'second-stale-child-id',
|
|
3095
|
+
]);
|
|
3096
|
+
});
|
|
3097
|
+
|
|
3098
|
+
it('ignores ContactOwnerChanged when different direct nested IDs match active tasks', () => {
|
|
3099
|
+
const firstTaskId = 'direct-main-id';
|
|
3100
|
+
const secondTaskId = 'direct-parent-id';
|
|
3101
|
+
const firstTask = createMockTask(
|
|
3102
|
+
createConferenceTaskData(firstTaskId, previousOwnerId, firstTaskId)
|
|
3103
|
+
);
|
|
3104
|
+
const secondTask = createMockTask(
|
|
3105
|
+
createConferenceTaskData(secondTaskId, previousOwnerId, secondTaskId)
|
|
3106
|
+
);
|
|
3107
|
+
taskManager.taskCollection = {
|
|
3108
|
+
[firstTaskId]: firstTask,
|
|
3109
|
+
[secondTaskId]: secondTask,
|
|
3110
|
+
};
|
|
3111
|
+
|
|
3112
|
+
const ownerChangeData = createConferenceTaskData(
|
|
3113
|
+
'unmatched-child-id',
|
|
3114
|
+
promotedOwnerId,
|
|
3115
|
+
firstTaskId
|
|
3116
|
+
);
|
|
3117
|
+
ownerChangeData.interaction.interactionId = secondTaskId;
|
|
3118
|
+
webSocketManagerMock.emit(
|
|
3119
|
+
'message',
|
|
3120
|
+
JSON.stringify({
|
|
3121
|
+
data: {...ownerChangeData, type: CC_EVENTS.CONTACT_OWNER_CHANGED},
|
|
3122
|
+
})
|
|
3123
|
+
);
|
|
3124
|
+
|
|
3125
|
+
expect(firstTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3126
|
+
expect(secondTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3127
|
+
});
|
|
3128
|
+
|
|
3129
|
+
it('ignores ContactOwnerChanged without an interaction correlation identifier', () => {
|
|
3130
|
+
const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
3131
|
+
|
|
3132
|
+
webSocketManagerMock.emit(
|
|
3133
|
+
'message',
|
|
3134
|
+
JSON.stringify({
|
|
3135
|
+
data: {
|
|
3136
|
+
type: CC_EVENTS.CONTACT_OWNER_CHANGED,
|
|
3137
|
+
interaction: {owner: promotedOwnerId},
|
|
3138
|
+
},
|
|
3139
|
+
})
|
|
3140
|
+
);
|
|
3141
|
+
|
|
3142
|
+
expect(task.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3143
|
+
});
|
|
3144
|
+
|
|
3145
|
+
it('recovers a missing promoted-agent task under the main interaction and emits one hydrate', () => {
|
|
3146
|
+
clearTasksAndFactoryHistory();
|
|
3147
|
+
const hydrateHandler = jest.fn();
|
|
3148
|
+
const incomingHandler = jest.fn();
|
|
3149
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
3150
|
+
taskManager.on(TASK_EVENTS.TASK_INCOMING, incomingHandler);
|
|
3151
|
+
|
|
3152
|
+
webSocketManagerMock.emit(
|
|
3153
|
+
'message',
|
|
3154
|
+
JSON.stringify({data: createPromotedAgentOwnerChangeData()})
|
|
3155
|
+
);
|
|
3156
|
+
|
|
3157
|
+
expect(TaskFactory.createTask).toHaveBeenCalledTimes(1);
|
|
3158
|
+
expect(TaskFactory.createTask).toHaveBeenCalledWith(
|
|
3159
|
+
contactMock,
|
|
3160
|
+
webCallingService,
|
|
3161
|
+
expect.objectContaining({
|
|
3162
|
+
interactionId: taskId,
|
|
3163
|
+
isAutoAnswering: false,
|
|
3164
|
+
isConsulted: false,
|
|
3165
|
+
isConferenceInProgress: true,
|
|
3166
|
+
wrapUpRequired: false,
|
|
3167
|
+
}),
|
|
3168
|
+
undefined,
|
|
3169
|
+
undefined,
|
|
3170
|
+
currentAgentId,
|
|
3171
|
+
undefined
|
|
3172
|
+
);
|
|
3173
|
+
|
|
3174
|
+
const recoveredTask = (TaskFactory.createTask as jest.Mock).mock.results[0].value;
|
|
3175
|
+
expect(recoveredTask.sendStateMachineEvent).toHaveBeenCalledTimes(2);
|
|
3176
|
+
expect(recoveredTask.sendStateMachineEvent).toHaveBeenNthCalledWith(
|
|
3177
|
+
1,
|
|
3178
|
+
expect.objectContaining({
|
|
3179
|
+
type: TaskEvent.HYDRATE,
|
|
3180
|
+
agentId: currentAgentId,
|
|
3181
|
+
taskData: expect.objectContaining({interactionId: taskId}),
|
|
3182
|
+
})
|
|
3183
|
+
);
|
|
3184
|
+
expect(recoveredTask.sendStateMachineEvent).toHaveBeenNthCalledWith(
|
|
3185
|
+
2,
|
|
3186
|
+
expect.objectContaining({
|
|
3187
|
+
type: TaskEvent.CONTACT_OWNER_CHANGED,
|
|
3188
|
+
taskData: expect.objectContaining({interactionId: taskId}),
|
|
3189
|
+
})
|
|
3190
|
+
);
|
|
3191
|
+
expect(taskManager.taskCollection[taskId]).toBe(recoveredTask);
|
|
3192
|
+
expect(taskManager.taskCollection['promoted-child-interaction-id']).toBeUndefined();
|
|
3193
|
+
expect(Object.values(taskManager.taskCollection)).toEqual([recoveredTask]);
|
|
3194
|
+
expect(recoveredTask.data.interaction.owner).toBe(currentAgentId);
|
|
3195
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
3196
|
+
expect(hydrateHandler).toHaveBeenCalledWith(recoveredTask);
|
|
3197
|
+
expect(incomingHandler).not.toHaveBeenCalled();
|
|
3198
|
+
});
|
|
3199
|
+
|
|
3200
|
+
it.each([
|
|
3201
|
+
[
|
|
3202
|
+
'another agent owns the interaction',
|
|
3203
|
+
(data) => {
|
|
3204
|
+
data.interaction.owner = promotedOwnerId;
|
|
3205
|
+
data.owner = promotedOwnerId;
|
|
3206
|
+
},
|
|
3207
|
+
],
|
|
3208
|
+
[
|
|
3209
|
+
'the payload has no main-call media',
|
|
3210
|
+
(data) => {
|
|
3211
|
+
data.interaction.media = {};
|
|
3212
|
+
},
|
|
3213
|
+
],
|
|
3214
|
+
[
|
|
3215
|
+
'the interaction is explicitly terminated',
|
|
3216
|
+
(data) => {
|
|
3217
|
+
data.interaction.isTerminated = true;
|
|
3218
|
+
},
|
|
3219
|
+
],
|
|
3220
|
+
])('does not create a missing task when %s', (_description, mutatePayload) => {
|
|
3221
|
+
clearTasksAndFactoryHistory();
|
|
3222
|
+
const ownerChangeData = createPromotedAgentOwnerChangeData();
|
|
3223
|
+
mutatePayload(ownerChangeData);
|
|
3224
|
+
|
|
3225
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
|
|
3226
|
+
|
|
3227
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3228
|
+
expect(taskManager.getAllTasks()).toEqual({});
|
|
3229
|
+
});
|
|
3230
|
+
|
|
3231
|
+
it('does not create a missing task for an owner-changing ContactUpdated event', () => {
|
|
3232
|
+
clearTasksAndFactoryHistory();
|
|
3233
|
+
const contactUpdatedData = {
|
|
3234
|
+
...createPromotedAgentOwnerChangeData(),
|
|
3235
|
+
type: CC_EVENTS.CONTACT_UPDATED,
|
|
3236
|
+
};
|
|
3237
|
+
|
|
3238
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: contactUpdatedData}));
|
|
3239
|
+
|
|
3240
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3241
|
+
expect(taskManager.getAllTasks()).toEqual({});
|
|
3242
|
+
});
|
|
3243
|
+
|
|
3244
|
+
it('reuses a recovered task for later AgentContact and ContactMerged events', () => {
|
|
3245
|
+
clearTasksAndFactoryHistory();
|
|
3246
|
+
const hydrateHandler = jest.fn();
|
|
3247
|
+
const mergedHandler = jest.fn();
|
|
3248
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
3249
|
+
taskManager.on(TASK_EVENTS.TASK_MERGED, mergedHandler);
|
|
3250
|
+
|
|
3251
|
+
webSocketManagerMock.emit(
|
|
3252
|
+
'message',
|
|
3253
|
+
JSON.stringify({data: createPromotedAgentOwnerChangeData()})
|
|
3254
|
+
);
|
|
3255
|
+
|
|
3256
|
+
const recoveredTask = taskManager.taskCollection[taskId];
|
|
3257
|
+
expect(recoveredTask).toBeDefined();
|
|
3258
|
+
expect(recoveredTask.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
|
|
3259
|
+
|
|
3260
|
+
webSocketManagerMock.emit(
|
|
3261
|
+
'message',
|
|
3262
|
+
JSON.stringify({
|
|
3263
|
+
data: {
|
|
3264
|
+
...createConferenceTaskData(taskId, currentAgentId),
|
|
3265
|
+
type: CC_EVENTS.AGENT_CONTACT,
|
|
3266
|
+
},
|
|
3267
|
+
})
|
|
3268
|
+
);
|
|
3269
|
+
webSocketManagerMock.emit(
|
|
3270
|
+
'message',
|
|
3271
|
+
JSON.stringify({
|
|
3272
|
+
data: {
|
|
3273
|
+
...createConferenceTaskData(taskId, currentAgentId),
|
|
3274
|
+
type: CC_EVENTS.CONTACT_MERGED,
|
|
3275
|
+
childInteractionId: 'promoted-child-interaction-id',
|
|
3276
|
+
},
|
|
3277
|
+
})
|
|
3278
|
+
);
|
|
3279
|
+
|
|
3280
|
+
expect(TaskFactory.createTask).toHaveBeenCalledTimes(1);
|
|
3281
|
+
expect(taskManager.taskCollection[taskId]).toBe(recoveredTask);
|
|
3282
|
+
expect(taskManager.taskCollection['promoted-child-interaction-id']).toBeUndefined();
|
|
3283
|
+
expect(Object.values(taskManager.taskCollection)).toEqual([recoveredTask]);
|
|
3284
|
+
expect(recoveredTask.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
|
|
3285
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(2);
|
|
3286
|
+
expect(mergedHandler).toHaveBeenCalledTimes(1);
|
|
3287
|
+
expect(mergedHandler).toHaveBeenCalledWith(recoveredTask);
|
|
3288
|
+
});
|
|
3289
|
+
|
|
3290
|
+
it('does not let a late ParticipantLeftConference restore a departed owner', () => {
|
|
3291
|
+
const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
3292
|
+
|
|
3293
|
+
webSocketManagerMock.emit(
|
|
3294
|
+
'message',
|
|
3295
|
+
JSON.stringify({
|
|
3296
|
+
data: {
|
|
3297
|
+
...createConferenceTaskData(taskId, promotedOwnerId),
|
|
3298
|
+
type: CC_EVENTS.CONTACT_UPDATED,
|
|
3299
|
+
},
|
|
3300
|
+
})
|
|
3301
|
+
);
|
|
3302
|
+
|
|
3303
|
+
const participantLeftData = createConferenceTaskData(taskId, previousOwnerId);
|
|
3304
|
+
participantLeftData.participantId = previousOwnerId;
|
|
3305
|
+
participantLeftData.interaction.participants[previousOwnerId].hasLeft = true;
|
|
3306
|
+
participantLeftData.interaction.media[taskId].participants = [
|
|
3307
|
+
currentAgentId,
|
|
3308
|
+
promotedOwnerId,
|
|
3309
|
+
];
|
|
3310
|
+
webSocketManagerMock.emit(
|
|
3311
|
+
'message',
|
|
3312
|
+
JSON.stringify({
|
|
3313
|
+
data: {...participantLeftData, type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE},
|
|
3314
|
+
})
|
|
3315
|
+
);
|
|
3316
|
+
|
|
3317
|
+
const participantLeaveEvent = expectLastStateMachineEvent(
|
|
3318
|
+
task.sendStateMachineEvent,
|
|
3319
|
+
TaskEvent.PARTICIPANT_LEAVE
|
|
3320
|
+
);
|
|
3321
|
+
expect(participantLeaveEvent.taskData.interaction.owner).toBe(promotedOwnerId);
|
|
3322
|
+
expect(task.data.interaction.owner).toBe(promotedOwnerId);
|
|
3323
|
+
});
|
|
3324
|
+
|
|
3325
|
+
it('does not infer that an incoming owner departed from a partial main-call roster', () => {
|
|
3326
|
+
const task = installTask(createConferenceTaskData(taskId, promotedOwnerId));
|
|
3327
|
+
const partialParticipantLeftData = createConferenceTaskData(taskId, previousOwnerId);
|
|
3328
|
+
partialParticipantLeftData.participantId = 'another-agent-id';
|
|
3329
|
+
partialParticipantLeftData.interaction.media[taskId].participants = [
|
|
3330
|
+
currentAgentId,
|
|
3331
|
+
promotedOwnerId,
|
|
3332
|
+
];
|
|
3333
|
+
|
|
3334
|
+
webSocketManagerMock.emit(
|
|
3335
|
+
'message',
|
|
3336
|
+
JSON.stringify({
|
|
3337
|
+
data: {
|
|
3338
|
+
...partialParticipantLeftData,
|
|
3339
|
+
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3340
|
+
},
|
|
3341
|
+
})
|
|
3342
|
+
);
|
|
3343
|
+
|
|
3344
|
+
const participantLeaveEvent = expectLastStateMachineEvent(
|
|
3345
|
+
task.sendStateMachineEvent,
|
|
3346
|
+
TaskEvent.PARTICIPANT_LEAVE
|
|
3347
|
+
);
|
|
3348
|
+
expect(participantLeaveEvent.taskData.interaction.owner).toBe(previousOwnerId);
|
|
3349
|
+
expect(task.data.interaction.owner).toBe(previousOwnerId);
|
|
3350
|
+
});
|
|
3351
|
+
|
|
3352
|
+
it('applies the promoted owner when ParticipantLeftConference arrives first', () => {
|
|
3353
|
+
const task = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
3354
|
+
const participantLeftData = createConferenceTaskData(taskId, previousOwnerId);
|
|
3355
|
+
participantLeftData.participantId = previousOwnerId;
|
|
3356
|
+
participantLeftData.interaction.participants[previousOwnerId].hasLeft = true;
|
|
3357
|
+
participantLeftData.interaction.media[taskId].participants = [
|
|
3358
|
+
currentAgentId,
|
|
3359
|
+
promotedOwnerId,
|
|
3360
|
+
];
|
|
3361
|
+
|
|
3362
|
+
webSocketManagerMock.emit(
|
|
3363
|
+
'message',
|
|
3364
|
+
JSON.stringify({
|
|
3365
|
+
data: {...participantLeftData, type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE},
|
|
3366
|
+
})
|
|
3367
|
+
);
|
|
3368
|
+
webSocketManagerMock.emit(
|
|
3369
|
+
'message',
|
|
3370
|
+
JSON.stringify({
|
|
3371
|
+
data: {
|
|
3372
|
+
...createConferenceTaskData(taskId, promotedOwnerId),
|
|
3373
|
+
type: CC_EVENTS.CONTACT_UPDATED,
|
|
3374
|
+
},
|
|
3375
|
+
})
|
|
3376
|
+
);
|
|
3377
|
+
|
|
3378
|
+
expect(task.data.interaction.owner).toBe(promotedOwnerId);
|
|
3379
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
3380
|
+
});
|
|
3381
|
+
});
|
|
3382
|
+
|
|
3383
|
+
describe('Conference event handling', () => {
|
|
3384
|
+
let task;
|
|
3385
|
+
|
|
3386
|
+
beforeEach(() => {
|
|
3387
|
+
task = {
|
|
3388
|
+
data: {interactionId: taskId},
|
|
3389
|
+
emit: jest.fn(),
|
|
3390
|
+
updateTaskData: jest.fn(),
|
|
2370
3391
|
sendStateMachineEvent: jest.fn(),
|
|
2371
3392
|
};
|
|
2372
3393
|
taskManager.taskCollection[taskId] = task as any;
|
|
@@ -2412,6 +3433,254 @@ describe('TaskManager', () => {
|
|
|
2412
3433
|
expect(call.type).toBe(TaskEvent.PARTICIPANT_LEAVE);
|
|
2413
3434
|
});
|
|
2414
3435
|
|
|
3436
|
+
it('routes participant-left from a main interaction to a child-keyed task', () => {
|
|
3437
|
+
const childTaskId = 'child-interaction-id';
|
|
3438
|
+
const childTask = createMockTask({
|
|
3439
|
+
...taskDataMock,
|
|
3440
|
+
interactionId: childTaskId,
|
|
3441
|
+
isConsulted: true,
|
|
3442
|
+
interaction: {
|
|
3443
|
+
mediaType: 'telephony',
|
|
3444
|
+
interactionId: childTaskId,
|
|
3445
|
+
parentInteractionId: taskId,
|
|
3446
|
+
},
|
|
3447
|
+
});
|
|
3448
|
+
delete taskManager.taskCollection[taskId];
|
|
3449
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
3450
|
+
|
|
3451
|
+
webSocketManagerMock.emit(
|
|
3452
|
+
'message',
|
|
3453
|
+
JSON.stringify({
|
|
3454
|
+
data: {
|
|
3455
|
+
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3456
|
+
interactionId: taskId,
|
|
3457
|
+
participantId: 'test-agent-id',
|
|
3458
|
+
interaction: {
|
|
3459
|
+
mediaType: 'telephony',
|
|
3460
|
+
interactionId: taskId,
|
|
3461
|
+
mainInteractionId: taskId,
|
|
3462
|
+
},
|
|
3463
|
+
},
|
|
3464
|
+
})
|
|
3465
|
+
);
|
|
3466
|
+
|
|
3467
|
+
expect(childTask.sendStateMachineEvent).toHaveBeenCalledWith(
|
|
3468
|
+
expect.objectContaining({
|
|
3469
|
+
type: TaskEvent.PARTICIPANT_LEAVE,
|
|
3470
|
+
participantId: 'test-agent-id',
|
|
3471
|
+
})
|
|
3472
|
+
);
|
|
3473
|
+
expect(taskManager.taskCollection[childTaskId]).toBeUndefined();
|
|
3474
|
+
expect(taskManager.taskCollection[taskId]).toBe(childTask);
|
|
3475
|
+
expect(
|
|
3476
|
+
Object.values(taskManager.taskCollection).filter((entry) => entry === childTask)
|
|
3477
|
+
).toHaveLength(1);
|
|
3478
|
+
});
|
|
3479
|
+
|
|
3480
|
+
it('routes consult-end from a main interaction to a child-keyed consulted task', () => {
|
|
3481
|
+
const childTaskId = 'consult-child-interaction-id';
|
|
3482
|
+
const childTask = createMockTask({
|
|
3483
|
+
...taskDataMock,
|
|
3484
|
+
interactionId: childTaskId,
|
|
3485
|
+
isConsulted: true,
|
|
3486
|
+
interaction: {
|
|
3487
|
+
mediaType: 'telephony',
|
|
3488
|
+
interactionId: childTaskId,
|
|
3489
|
+
callProcessingDetails: {parentInteractionId: taskId},
|
|
3490
|
+
},
|
|
3491
|
+
});
|
|
3492
|
+
delete taskManager.taskCollection[taskId];
|
|
3493
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
3494
|
+
|
|
3495
|
+
webSocketManagerMock.emit(
|
|
3496
|
+
'message',
|
|
3497
|
+
JSON.stringify({
|
|
3498
|
+
data: {
|
|
3499
|
+
type: CC_EVENTS.AGENT_CONSULT_ENDED,
|
|
3500
|
+
interactionId: taskId,
|
|
3501
|
+
interaction: {
|
|
3502
|
+
mediaType: 'telephony',
|
|
3503
|
+
interactionId: taskId,
|
|
3504
|
+
mainInteractionId: taskId,
|
|
3505
|
+
},
|
|
3506
|
+
},
|
|
3507
|
+
})
|
|
3508
|
+
);
|
|
3509
|
+
|
|
3510
|
+
expect(childTask.sendStateMachineEvent).toHaveBeenCalledWith(
|
|
3511
|
+
expect.objectContaining({type: TaskEvent.CONSULT_END})
|
|
3512
|
+
);
|
|
3513
|
+
});
|
|
3514
|
+
|
|
3515
|
+
it('deduplicates aliases of the same child task during related-interaction lookup', () => {
|
|
3516
|
+
const childTaskId = 'aliased-child-interaction-id';
|
|
3517
|
+
const childTask = createMockTask({
|
|
3518
|
+
...taskDataMock,
|
|
3519
|
+
interactionId: childTaskId,
|
|
3520
|
+
interaction: {
|
|
3521
|
+
mediaType: 'telephony',
|
|
3522
|
+
interactionId: childTaskId,
|
|
3523
|
+
mainInteractionId: taskId,
|
|
3524
|
+
},
|
|
3525
|
+
});
|
|
3526
|
+
delete taskManager.taskCollection[taskId];
|
|
3527
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
3528
|
+
taskManager.taskCollection['child-task-alias'] = childTask;
|
|
3529
|
+
|
|
3530
|
+
webSocketManagerMock.emit(
|
|
3531
|
+
'message',
|
|
3532
|
+
JSON.stringify({
|
|
3533
|
+
data: {
|
|
3534
|
+
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3535
|
+
interactionId: taskId,
|
|
3536
|
+
participantId: 'test-agent-id',
|
|
3537
|
+
},
|
|
3538
|
+
})
|
|
3539
|
+
);
|
|
3540
|
+
|
|
3541
|
+
expect(childTask.sendStateMachineEvent).toHaveBeenCalledTimes(1);
|
|
3542
|
+
expect(
|
|
3543
|
+
Object.values(taskManager.taskCollection).filter((entry) => entry === childTask)
|
|
3544
|
+
).toHaveLength(1);
|
|
3545
|
+
});
|
|
3546
|
+
|
|
3547
|
+
it('prefers an exact task match over a child task with the same main interaction', () => {
|
|
3548
|
+
const childTaskId = 'child-interaction-id';
|
|
3549
|
+
const childTask = createMockTask({
|
|
3550
|
+
...taskDataMock,
|
|
3551
|
+
interactionId: childTaskId,
|
|
3552
|
+
interaction: {
|
|
3553
|
+
mediaType: 'telephony',
|
|
3554
|
+
interactionId: childTaskId,
|
|
3555
|
+
mainInteractionId: taskId,
|
|
3556
|
+
},
|
|
3557
|
+
});
|
|
3558
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
3559
|
+
|
|
3560
|
+
webSocketManagerMock.emit(
|
|
3561
|
+
'message',
|
|
3562
|
+
JSON.stringify({
|
|
3563
|
+
data: {
|
|
3564
|
+
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3565
|
+
interactionId: taskId,
|
|
3566
|
+
participantId: 'another-agent',
|
|
3567
|
+
},
|
|
3568
|
+
})
|
|
3569
|
+
);
|
|
3570
|
+
|
|
3571
|
+
expect(task.sendStateMachineEvent).toHaveBeenCalled();
|
|
3572
|
+
expect(childTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3573
|
+
});
|
|
3574
|
+
|
|
3575
|
+
it('does not route a main-interaction event when multiple unique child tasks match', () => {
|
|
3576
|
+
delete taskManager.taskCollection[taskId];
|
|
3577
|
+
const createChildTask = (interactionId: string) =>
|
|
3578
|
+
createMockTask({
|
|
3579
|
+
...taskDataMock,
|
|
3580
|
+
interactionId,
|
|
3581
|
+
interaction: {
|
|
3582
|
+
mediaType: 'telephony',
|
|
3583
|
+
interactionId,
|
|
3584
|
+
mainInteractionId: taskId,
|
|
3585
|
+
},
|
|
3586
|
+
});
|
|
3587
|
+
const firstChildTask = createChildTask('child-1');
|
|
3588
|
+
const secondChildTask = createChildTask('child-2');
|
|
3589
|
+
taskManager.taskCollection['child-1'] = firstChildTask;
|
|
3590
|
+
taskManager.taskCollection['child-2'] = secondChildTask;
|
|
3591
|
+
|
|
3592
|
+
webSocketManagerMock.emit(
|
|
3593
|
+
'message',
|
|
3594
|
+
JSON.stringify({
|
|
3595
|
+
data: {
|
|
3596
|
+
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3597
|
+
interactionId: taskId,
|
|
3598
|
+
},
|
|
3599
|
+
})
|
|
3600
|
+
);
|
|
3601
|
+
|
|
3602
|
+
expect(firstChildTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3603
|
+
expect(secondChildTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3604
|
+
});
|
|
3605
|
+
|
|
3606
|
+
it('does not route a lifecycle event without a correlation identifier', () => {
|
|
3607
|
+
webSocketManagerMock.emit(
|
|
3608
|
+
'message',
|
|
3609
|
+
JSON.stringify({
|
|
3610
|
+
data: {
|
|
3611
|
+
type: CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3612
|
+
participantId: 'another-agent',
|
|
3613
|
+
},
|
|
3614
|
+
})
|
|
3615
|
+
);
|
|
3616
|
+
|
|
3617
|
+
expect(task.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3618
|
+
});
|
|
3619
|
+
|
|
3620
|
+
it('removes every collection alias for a task during terminal cleanup', () => {
|
|
3621
|
+
taskManager.taskCollection['task-alias'] = task;
|
|
3622
|
+
|
|
3623
|
+
(taskManager as any).removeTaskFromCollection(task);
|
|
3624
|
+
|
|
3625
|
+
expect(taskManager.taskCollection[taskId]).toBeUndefined();
|
|
3626
|
+
expect(taskManager.taskCollection['task-alias']).toBeUndefined();
|
|
3627
|
+
});
|
|
3628
|
+
|
|
3629
|
+
it('replaces an EP-DN child task with a hydrated main task on CONTACT_MERGED', () => {
|
|
3630
|
+
const childTaskId = 'ep-dn-child-interaction-id';
|
|
3631
|
+
const childTask = createMockTask({
|
|
3632
|
+
...taskDataMock,
|
|
3633
|
+
interactionId: childTaskId,
|
|
3634
|
+
interaction: {
|
|
3635
|
+
mediaType: 'telephony',
|
|
3636
|
+
interactionId: childTaskId,
|
|
3637
|
+
parentInteractionId: taskId,
|
|
3638
|
+
},
|
|
3639
|
+
});
|
|
3640
|
+
const mergedHandler = jest.fn();
|
|
3641
|
+
delete taskManager.taskCollection[taskId];
|
|
3642
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
3643
|
+
taskManager.on(TASK_EVENTS.TASK_MERGED, mergedHandler);
|
|
3644
|
+
|
|
3645
|
+
webSocketManagerMock.emit(
|
|
3646
|
+
'message',
|
|
3647
|
+
JSON.stringify({
|
|
3648
|
+
data: {
|
|
3649
|
+
type: CC_EVENTS.CONTACT_MERGED,
|
|
3650
|
+
interactionId: taskId,
|
|
3651
|
+
childInteractionId: childTaskId,
|
|
3652
|
+
interaction: {
|
|
3653
|
+
mediaType: 'telephony',
|
|
3654
|
+
interactionId: taskId,
|
|
3655
|
+
mainInteractionId: taskId,
|
|
3656
|
+
state: 'conference',
|
|
3657
|
+
owner: 'test-agent-id',
|
|
3658
|
+
participants: {
|
|
3659
|
+
'test-agent-id': {id: 'test-agent-id', pType: 'Agent', hasLeft: false},
|
|
3660
|
+
},
|
|
3661
|
+
media: {
|
|
3662
|
+
[taskId]: {
|
|
3663
|
+
mediaResourceId: taskId,
|
|
3664
|
+
mediaType: 'telephony',
|
|
3665
|
+
mType: 'mainCall',
|
|
3666
|
+
participants: ['test-agent-id'],
|
|
3667
|
+
},
|
|
3668
|
+
},
|
|
3669
|
+
},
|
|
3670
|
+
},
|
|
3671
|
+
})
|
|
3672
|
+
);
|
|
3673
|
+
|
|
3674
|
+
const mainTask = taskManager.taskCollection[taskId];
|
|
3675
|
+
expect(taskManager.taskCollection[childTaskId]).toBeUndefined();
|
|
3676
|
+
expect(mainTask).toBeDefined();
|
|
3677
|
+
expect(mainTask).not.toBe(childTask);
|
|
3678
|
+
expect(mainTask.sendStateMachineEvent).toHaveBeenCalledWith(
|
|
3679
|
+
expect.objectContaining({type: TaskEvent.HYDRATE})
|
|
3680
|
+
);
|
|
3681
|
+
expect(mergedHandler).toHaveBeenCalledWith(mainTask);
|
|
3682
|
+
});
|
|
3683
|
+
|
|
2415
3684
|
it('handles AGENT_CONSULT_CONFERENCING event without errors', () => {
|
|
2416
3685
|
const payload = {
|
|
2417
3686
|
data: {type: CC_EVENTS.AGENT_CONSULT_CONFERENCING, interactionId: taskId},
|
|
@@ -2463,6 +3732,405 @@ describe('TaskManager', () => {
|
|
|
2463
3732
|
});
|
|
2464
3733
|
});
|
|
2465
3734
|
|
|
3735
|
+
const stampWrapUp = (
|
|
3736
|
+
eventType: CC_EVENTS,
|
|
3737
|
+
payload: Record<string, unknown>,
|
|
3738
|
+
agentId = 'test-agent-id',
|
|
3739
|
+
task?: unknown
|
|
3740
|
+
) => (TaskManager as any).stampWrapUpRequiredForEvent(eventType, payload, agentId, task);
|
|
3741
|
+
|
|
3742
|
+
it('maps conference wrap-up events without restamping wrapUpRequired', () => {
|
|
3743
|
+
const unstamped = {...taskDataMock, wrapUpRequired: false};
|
|
3744
|
+
const conferenceEnded = (TaskManager as any).mapEventToTaskStateMachineEvent(
|
|
3745
|
+
CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED,
|
|
3746
|
+
unstamped,
|
|
3747
|
+
'test-agent-id'
|
|
3748
|
+
);
|
|
3749
|
+
const participantLeft = (TaskManager as any).mapEventToTaskStateMachineEvent(
|
|
3750
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3751
|
+
{...unstamped, participantId: 'test-agent-id'},
|
|
3752
|
+
'test-agent-id'
|
|
3753
|
+
);
|
|
3754
|
+
const transferred = (TaskManager as any).mapEventToTaskStateMachineEvent(
|
|
3755
|
+
CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED,
|
|
3756
|
+
unstamped,
|
|
3757
|
+
'test-agent-id'
|
|
3758
|
+
);
|
|
3759
|
+
|
|
3760
|
+
expect(conferenceEnded.taskData).toBe(unstamped);
|
|
3761
|
+
expect(participantLeft.taskData.wrapUpRequired).toBe(false);
|
|
3762
|
+
expect(participantLeft.participantId).toBe('test-agent-id');
|
|
3763
|
+
expect(transferred.taskData).toBe(unstamped);
|
|
3764
|
+
});
|
|
3765
|
+
|
|
3766
|
+
it('stamps wrapUpRequired on AGENT_CONSULT_CONFERENCE_ENDED only from explicit signals', () => {
|
|
3767
|
+
const wrapping = stampWrapUp(CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED, {
|
|
3768
|
+
...taskDataMock,
|
|
3769
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
3770
|
+
});
|
|
3771
|
+
const remaining = stampWrapUp(CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED, {
|
|
3772
|
+
...taskDataMock,
|
|
3773
|
+
owner: 'test-agent-id',
|
|
3774
|
+
isConsulted: false,
|
|
3775
|
+
});
|
|
3776
|
+
|
|
3777
|
+
expect(wrapping.wrapUpRequired).toBe(true);
|
|
3778
|
+
expect(remaining.wrapUpRequired).toBe(false);
|
|
3779
|
+
});
|
|
3780
|
+
|
|
3781
|
+
it('stamps wrapUpRequired on PARTICIPANT_LEFT_CONFERENCE only when this agent left', () => {
|
|
3782
|
+
const selfLeft = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
3783
|
+
...taskDataMock,
|
|
3784
|
+
participantId: 'test-agent-id',
|
|
3785
|
+
interaction: {
|
|
3786
|
+
...taskDataMock.interaction,
|
|
3787
|
+
owner: 'test-agent-id',
|
|
3788
|
+
participants: {},
|
|
3789
|
+
},
|
|
3790
|
+
});
|
|
3791
|
+
const otherLeft = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
3792
|
+
...taskDataMock,
|
|
3793
|
+
participantId: 'other-agent-id',
|
|
3794
|
+
interaction: {
|
|
3795
|
+
...taskDataMock.interaction,
|
|
3796
|
+
owner: 'test-agent-id',
|
|
3797
|
+
participants: {
|
|
3798
|
+
'test-agent-id': {id: 'test-agent-id', pType: 'Agent', hasLeft: false},
|
|
3799
|
+
},
|
|
3800
|
+
},
|
|
3801
|
+
});
|
|
3802
|
+
|
|
3803
|
+
expect(selfLeft.wrapUpRequired).toBe(true);
|
|
3804
|
+
expect(otherLeft.wrapUpRequired).toBe(false);
|
|
3805
|
+
});
|
|
3806
|
+
|
|
3807
|
+
it('stamps wrapUpRequired true when this agent left and pending is empty or absent', () => {
|
|
3808
|
+
const emptyPending = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
3809
|
+
...taskDataMock,
|
|
3810
|
+
participantId: 'test-agent-id',
|
|
3811
|
+
agentsPendingWrapUp: [],
|
|
3812
|
+
interaction: {
|
|
3813
|
+
...taskDataMock.interaction,
|
|
3814
|
+
participants: {},
|
|
3815
|
+
},
|
|
3816
|
+
});
|
|
3817
|
+
const absentPending = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
3818
|
+
...taskDataMock,
|
|
3819
|
+
participantId: 'test-agent-id',
|
|
3820
|
+
interaction: {
|
|
3821
|
+
...taskDataMock.interaction,
|
|
3822
|
+
participants: {},
|
|
3823
|
+
},
|
|
3824
|
+
});
|
|
3825
|
+
|
|
3826
|
+
expect(emptyPending.wrapUpRequired).toBe(true);
|
|
3827
|
+
expect(absentPending.wrapUpRequired).toBe(true);
|
|
3828
|
+
});
|
|
3829
|
+
|
|
3830
|
+
it('does not stamp wrapUpRequired when this agent left but pending excludes self', () => {
|
|
3831
|
+
const leftExcluded = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
3832
|
+
...taskDataMock,
|
|
3833
|
+
participantId: 'test-agent-id',
|
|
3834
|
+
wrapUpRequired: true,
|
|
3835
|
+
agentsPendingWrapUp: ['other-agent-id'],
|
|
3836
|
+
interaction: {
|
|
3837
|
+
...taskDataMock.interaction,
|
|
3838
|
+
owner: 'test-agent-id',
|
|
3839
|
+
participants: {},
|
|
3840
|
+
},
|
|
3841
|
+
});
|
|
3842
|
+
|
|
3843
|
+
expect(leftExcluded.wrapUpRequired).toBe(false);
|
|
3844
|
+
});
|
|
3845
|
+
|
|
3846
|
+
it('does not infer self-leave from a partial participant map when another participant is named', () => {
|
|
3847
|
+
const remaining = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
3848
|
+
...taskDataMock,
|
|
3849
|
+
participantId: 'other-agent-id',
|
|
3850
|
+
agentsPendingWrapUp: [],
|
|
3851
|
+
interaction: {
|
|
3852
|
+
...taskDataMock.interaction,
|
|
3853
|
+
owner: 'test-agent-id',
|
|
3854
|
+
participants: {
|
|
3855
|
+
'other-agent-id': {id: 'other-agent-id', pType: 'Agent', hasLeft: true},
|
|
3856
|
+
},
|
|
3857
|
+
},
|
|
3858
|
+
});
|
|
3859
|
+
|
|
3860
|
+
expect(remaining.wrapUpRequired).toBe(false);
|
|
3861
|
+
|
|
3862
|
+
class TimerTask extends Task {
|
|
3863
|
+
public accept() {
|
|
3864
|
+
return Promise.resolve({} as any);
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
const remainingTask = new TimerTask(
|
|
3868
|
+
{},
|
|
3869
|
+
{...taskDataMock, wrapUpRequired: false},
|
|
3870
|
+
{channelType: 'voice', isEndTaskEnabled: true, isEndConsultEnabled: true},
|
|
3871
|
+
{
|
|
3872
|
+
wrapUpProps: {
|
|
3873
|
+
autoWrapup: true,
|
|
3874
|
+
autoWrapupInterval: 1000,
|
|
3875
|
+
wrapUpReasonList: [{id: 'code-1', name: 'Default', isDefault: true, isSystem: false}],
|
|
3876
|
+
},
|
|
3877
|
+
},
|
|
3878
|
+
'test-agent-id'
|
|
3879
|
+
);
|
|
3880
|
+
try {
|
|
3881
|
+
remainingTask.updateTaskData(remaining);
|
|
3882
|
+
expect(remainingTask.autoWrapup).toBeUndefined();
|
|
3883
|
+
} finally {
|
|
3884
|
+
remainingTask.cancelAutoWrapupTimer();
|
|
3885
|
+
}
|
|
3886
|
+
});
|
|
3887
|
+
|
|
3888
|
+
it('does not stamp wrapUpRequired when a consulted non-owner leaves with empty pending', () => {
|
|
3889
|
+
const consulted = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
3890
|
+
...taskDataMock,
|
|
3891
|
+
participantId: 'test-agent-id',
|
|
3892
|
+
isConsulted: true,
|
|
3893
|
+
agentsPendingWrapUp: [],
|
|
3894
|
+
interaction: {
|
|
3895
|
+
...taskDataMock.interaction,
|
|
3896
|
+
owner: 'other-agent-id',
|
|
3897
|
+
participants: {},
|
|
3898
|
+
},
|
|
3899
|
+
});
|
|
3900
|
+
const consultedFromTask = stampWrapUp(
|
|
3901
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3902
|
+
{
|
|
3903
|
+
...taskDataMock,
|
|
3904
|
+
participantId: 'test-agent-id',
|
|
3905
|
+
agentsPendingWrapUp: [],
|
|
3906
|
+
interaction: {
|
|
3907
|
+
...taskDataMock.interaction,
|
|
3908
|
+
participants: {},
|
|
3909
|
+
},
|
|
3910
|
+
},
|
|
3911
|
+
'test-agent-id',
|
|
3912
|
+
{
|
|
3913
|
+
data: {
|
|
3914
|
+
isConsulted: true,
|
|
3915
|
+
interaction: {owner: 'other-agent-id'},
|
|
3916
|
+
},
|
|
3917
|
+
}
|
|
3918
|
+
);
|
|
3919
|
+
|
|
3920
|
+
expect(consulted.wrapUpRequired).toBe(false);
|
|
3921
|
+
expect(consultedFromTask.wrapUpRequired).toBe(false);
|
|
3922
|
+
});
|
|
3923
|
+
|
|
3924
|
+
it('stamps wrapUpRequired when EP-DN omits participantId and previous task included self', () => {
|
|
3925
|
+
const epdn = stampWrapUp(
|
|
3926
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3927
|
+
{
|
|
3928
|
+
...taskDataMock,
|
|
3929
|
+
agentsPendingWrapUp: [],
|
|
3930
|
+
interaction: {
|
|
3931
|
+
...taskDataMock.interaction,
|
|
3932
|
+
owner: 'test-agent-id',
|
|
3933
|
+
participants: {},
|
|
3934
|
+
},
|
|
3935
|
+
},
|
|
3936
|
+
'test-agent-id',
|
|
3937
|
+
{
|
|
3938
|
+
data: {
|
|
3939
|
+
interaction: {
|
|
3940
|
+
participants: {
|
|
3941
|
+
'test-agent-id': {id: 'test-agent-id', pType: 'Agent', hasLeft: false},
|
|
3942
|
+
},
|
|
3943
|
+
},
|
|
3944
|
+
},
|
|
3945
|
+
}
|
|
3946
|
+
);
|
|
3947
|
+
|
|
3948
|
+
expect(epdn.wrapUpRequired).toBe(true);
|
|
3949
|
+
});
|
|
3950
|
+
|
|
3951
|
+
it('does not stamp wrapUpRequired on PARTICIPANT_LEFT_CONFERENCE when the actor ignores PARTICIPANT_LEAVE', () => {
|
|
3952
|
+
const selfLeave = {
|
|
3953
|
+
...taskDataMock,
|
|
3954
|
+
participantId: 'test-agent-id',
|
|
3955
|
+
agentsPendingWrapUp: [],
|
|
3956
|
+
interaction: {
|
|
3957
|
+
...taskDataMock.interaction,
|
|
3958
|
+
owner: 'test-agent-id',
|
|
3959
|
+
participants: {},
|
|
3960
|
+
},
|
|
3961
|
+
};
|
|
3962
|
+
const snapshotTask = (state: TaskState) => ({
|
|
3963
|
+
stateMachineService: {getSnapshot: () => ({value: state})},
|
|
3964
|
+
});
|
|
3965
|
+
|
|
3966
|
+
const holdInitiating = stampWrapUp(
|
|
3967
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3968
|
+
selfLeave,
|
|
3969
|
+
'test-agent-id',
|
|
3970
|
+
snapshotTask(TaskState.HOLD_INITIATING)
|
|
3971
|
+
);
|
|
3972
|
+
const connected = stampWrapUp(
|
|
3973
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3974
|
+
selfLeave,
|
|
3975
|
+
'test-agent-id',
|
|
3976
|
+
snapshotTask(TaskState.CONNECTED)
|
|
3977
|
+
);
|
|
3978
|
+
const confInitiating = stampWrapUp(
|
|
3979
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3980
|
+
selfLeave,
|
|
3981
|
+
'test-agent-id',
|
|
3982
|
+
snapshotTask(TaskState.CONF_INITIATING)
|
|
3983
|
+
);
|
|
3984
|
+
const conferencing = stampWrapUp(
|
|
3985
|
+
CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE,
|
|
3986
|
+
selfLeave,
|
|
3987
|
+
'test-agent-id',
|
|
3988
|
+
snapshotTask(TaskState.CONFERENCING)
|
|
3989
|
+
);
|
|
3990
|
+
|
|
3991
|
+
expect(holdInitiating).toBe(selfLeave);
|
|
3992
|
+
expect(holdInitiating.wrapUpRequired).toBeUndefined();
|
|
3993
|
+
expect(connected.wrapUpRequired).toBeUndefined();
|
|
3994
|
+
expect(confInitiating.wrapUpRequired).toBeUndefined();
|
|
3995
|
+
expect(conferencing.wrapUpRequired).toBe(true);
|
|
3996
|
+
|
|
3997
|
+
class TimerTask extends Task {
|
|
3998
|
+
public accept() {
|
|
3999
|
+
return Promise.resolve({} as any);
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
const holdTask = new TimerTask(
|
|
4003
|
+
{},
|
|
4004
|
+
{...taskDataMock, wrapUpRequired: false},
|
|
4005
|
+
{channelType: 'voice', isEndTaskEnabled: true, isEndConsultEnabled: true},
|
|
4006
|
+
{
|
|
4007
|
+
wrapUpProps: {
|
|
4008
|
+
autoWrapup: true,
|
|
4009
|
+
autoWrapupInterval: 1000,
|
|
4010
|
+
wrapUpReasonList: [{id: 'code-1', name: 'Default', isDefault: true, isSystem: false}],
|
|
4011
|
+
},
|
|
4012
|
+
},
|
|
4013
|
+
'test-agent-id'
|
|
4014
|
+
);
|
|
4015
|
+
try {
|
|
4016
|
+
holdTask.updateTaskData(holdInitiating);
|
|
4017
|
+
expect(holdTask.autoWrapup).toBeUndefined();
|
|
4018
|
+
} finally {
|
|
4019
|
+
holdTask.cancelAutoWrapupTimer();
|
|
4020
|
+
}
|
|
4021
|
+
});
|
|
4022
|
+
|
|
4023
|
+
it('stamps wrapUpRequired on AGENT_CONFERENCE_TRANSFERRED for the initiator only', () => {
|
|
4024
|
+
const initiator = stampWrapUp(CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED, {
|
|
4025
|
+
...taskDataMock,
|
|
4026
|
+
consultingAgentId: 'test-agent-id',
|
|
4027
|
+
});
|
|
4028
|
+
const observer = stampWrapUp(CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED, {
|
|
4029
|
+
...taskDataMock,
|
|
4030
|
+
consultingAgentId: 'other-agent-id',
|
|
4031
|
+
owner: 'test-agent-id',
|
|
4032
|
+
});
|
|
4033
|
+
|
|
4034
|
+
expect(initiator.wrapUpRequired).toBe(true);
|
|
4035
|
+
expect(observer.wrapUpRequired).toBe(false);
|
|
4036
|
+
});
|
|
4037
|
+
|
|
4038
|
+
it('does not let transfer initiator override a nonempty pending list that excludes self', () => {
|
|
4039
|
+
const consultingInitiator = stampWrapUp(CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED, {
|
|
4040
|
+
...taskDataMock,
|
|
4041
|
+
wrapUpRequired: true,
|
|
4042
|
+
agentsPendingWrapUp: ['other-agent-id'],
|
|
4043
|
+
consultingAgentId: 'test-agent-id',
|
|
4044
|
+
});
|
|
4045
|
+
const flaggedInitiator = stampWrapUp(
|
|
4046
|
+
CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED,
|
|
4047
|
+
{
|
|
4048
|
+
...taskDataMock,
|
|
4049
|
+
wrapUpRequired: true,
|
|
4050
|
+
agentsPendingWrapUp: ['other-agent-id'],
|
|
4051
|
+
},
|
|
4052
|
+
'test-agent-id',
|
|
4053
|
+
{
|
|
4054
|
+
stateMachineService: {
|
|
4055
|
+
getSnapshot: () => ({context: {transferConferenceRequested: true}}),
|
|
4056
|
+
},
|
|
4057
|
+
}
|
|
4058
|
+
);
|
|
4059
|
+
|
|
4060
|
+
expect(consultingInitiator.wrapUpRequired).toBe(false);
|
|
4061
|
+
expect(flaggedInitiator.wrapUpRequired).toBe(false);
|
|
4062
|
+
});
|
|
4063
|
+
|
|
4064
|
+
it('honors transferConferenceRequested when pending is empty and consultingAgentId is omitted', () => {
|
|
4065
|
+
const flagged = stampWrapUp(
|
|
4066
|
+
CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED,
|
|
4067
|
+
{...taskDataMock},
|
|
4068
|
+
'test-agent-id',
|
|
4069
|
+
{
|
|
4070
|
+
stateMachineService: {
|
|
4071
|
+
getSnapshot: () => ({context: {transferConferenceRequested: true}}),
|
|
4072
|
+
},
|
|
4073
|
+
}
|
|
4074
|
+
);
|
|
4075
|
+
const unflagged = stampWrapUp(CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED, {...taskDataMock});
|
|
4076
|
+
|
|
4077
|
+
expect(flagged.wrapUpRequired).toBe(true);
|
|
4078
|
+
expect(unflagged.wrapUpRequired).toBe(false);
|
|
4079
|
+
});
|
|
4080
|
+
|
|
4081
|
+
it('does not stamp wrapUpRequired when a nonempty pending list excludes this agent', () => {
|
|
4082
|
+
const conflictingPayload = {
|
|
4083
|
+
...taskDataMock,
|
|
4084
|
+
wrapUpRequired: true,
|
|
4085
|
+
agentsPendingWrapUp: ['other-agent-id'],
|
|
4086
|
+
};
|
|
4087
|
+
|
|
4088
|
+
const conferenceEnded = stampWrapUp(
|
|
4089
|
+
CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED,
|
|
4090
|
+
conflictingPayload
|
|
4091
|
+
);
|
|
4092
|
+
const conferenceTransferred = stampWrapUp(CC_EVENTS.AGENT_CONFERENCE_TRANSFERRED, {
|
|
4093
|
+
...conflictingPayload,
|
|
4094
|
+
consultingAgentId: 'other-agent-id',
|
|
4095
|
+
});
|
|
4096
|
+
const remainingOnLeave = stampWrapUp(CC_EVENTS.PARTICIPANT_LEFT_CONFERENCE, {
|
|
4097
|
+
...conflictingPayload,
|
|
4098
|
+
participantId: 'other-agent-id',
|
|
4099
|
+
interaction: {
|
|
4100
|
+
...taskDataMock.interaction,
|
|
4101
|
+
owner: 'test-agent-id',
|
|
4102
|
+
participants: {
|
|
4103
|
+
'test-agent-id': {id: 'test-agent-id', pType: 'Agent', hasLeft: false},
|
|
4104
|
+
},
|
|
4105
|
+
},
|
|
4106
|
+
});
|
|
4107
|
+
|
|
4108
|
+
expect(conferenceEnded.wrapUpRequired).toBe(false);
|
|
4109
|
+
expect(conferenceTransferred.wrapUpRequired).toBe(false);
|
|
4110
|
+
expect(remainingOnLeave.wrapUpRequired).toBe(false);
|
|
4111
|
+
});
|
|
4112
|
+
|
|
4113
|
+
it('stamps wrapUpRequired when pending includes this agent or pending is empty', () => {
|
|
4114
|
+
const pendingIncludesSelf = stampWrapUp(CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED, {
|
|
4115
|
+
...taskDataMock,
|
|
4116
|
+
wrapUpRequired: true,
|
|
4117
|
+
agentsPendingWrapUp: ['test-agent-id'],
|
|
4118
|
+
});
|
|
4119
|
+
const emptyPendingFallback = stampWrapUp(CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED, {
|
|
4120
|
+
...taskDataMock,
|
|
4121
|
+
wrapUpRequired: true,
|
|
4122
|
+
agentsPendingWrapUp: [],
|
|
4123
|
+
});
|
|
4124
|
+
const missingPendingFallback = stampWrapUp(CC_EVENTS.AGENT_CONSULT_CONFERENCE_ENDED, {
|
|
4125
|
+
...taskDataMock,
|
|
4126
|
+
wrapUpRequired: true,
|
|
4127
|
+
});
|
|
4128
|
+
|
|
4129
|
+
expect(pendingIncludesSelf.wrapUpRequired).toBe(true);
|
|
4130
|
+
expect(emptyPendingFallback.wrapUpRequired).toBe(true);
|
|
4131
|
+
expect(missingPendingFallback.wrapUpRequired).toBe(true);
|
|
4132
|
+
});
|
|
4133
|
+
|
|
2466
4134
|
it('sends mapped events to the task state machine service', () => {
|
|
2467
4135
|
const payload = {...taskDataMock, type: CC_EVENTS.AGENT_CONTACT_ASSIGNED};
|
|
2468
4136
|
const task = taskManager.getTask(taskId);
|
|
@@ -2478,4 +4146,44 @@ describe('TaskManager', () => {
|
|
|
2478
4146
|
sendStateMachineEventSpy.mockRestore();
|
|
2479
4147
|
});
|
|
2480
4148
|
});
|
|
4149
|
+
|
|
4150
|
+
describe('applyEnableWxBetterTogether', () => {
|
|
4151
|
+
it('updates config flags and propagates to active tasks', () => {
|
|
4152
|
+
const taskOne = {setEnableWxBetterTogether: jest.fn()};
|
|
4153
|
+
const taskTwo = {setEnableWxBetterTogether: jest.fn()};
|
|
4154
|
+
|
|
4155
|
+
taskManager.setConfigFlags({
|
|
4156
|
+
isEndTaskEnabled: true,
|
|
4157
|
+
isEndConsultEnabled: true,
|
|
4158
|
+
enableWxBetterTogether: true,
|
|
4159
|
+
});
|
|
4160
|
+
taskManager['taskCollection'] = {
|
|
4161
|
+
[taskId]: taskOne,
|
|
4162
|
+
'task-2': taskTwo,
|
|
4163
|
+
};
|
|
4164
|
+
|
|
4165
|
+
taskManager.applyEnableWxBetterTogether(false);
|
|
4166
|
+
|
|
4167
|
+
expect(taskManager['configFlags']?.enableWxBetterTogether).toBe(false);
|
|
4168
|
+
expect(taskOne.setEnableWxBetterTogether).toHaveBeenCalledWith(false);
|
|
4169
|
+
expect(taskTwo.setEnableWxBetterTogether).toHaveBeenCalledWith(false);
|
|
4170
|
+
});
|
|
4171
|
+
});
|
|
4172
|
+
|
|
4173
|
+
describe('applyWxAppMuteStateFromSync', () => {
|
|
4174
|
+
it('propagates mute state to all tasks', () => {
|
|
4175
|
+
const taskOne = {applyWxAppMuteStateFromSync: jest.fn()};
|
|
4176
|
+
const taskTwo = {applyWxAppMuteStateFromSync: jest.fn()};
|
|
4177
|
+
|
|
4178
|
+
taskManager['taskCollection'] = {
|
|
4179
|
+
[taskId]: taskOne,
|
|
4180
|
+
'task-2': taskTwo,
|
|
4181
|
+
};
|
|
4182
|
+
|
|
4183
|
+
taskManager.applyWxAppMuteStateFromSync('call-1', true);
|
|
4184
|
+
|
|
4185
|
+
expect(taskOne.applyWxAppMuteStateFromSync).toHaveBeenCalledWith('call-1', true);
|
|
4186
|
+
expect(taskTwo.applyWxAppMuteStateFromSync).toHaveBeenCalledWith('call-1', true);
|
|
4187
|
+
});
|
|
4188
|
+
});
|
|
2481
4189
|
});
|