@webex/contact-center 3.12.0-llmrefactor.1 → 3.12.0-llmrefactor.10
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
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import Voice from '../../../../../../src/services/task/voice/Voice';
|
|
2
|
+
import LoggerProxy from '../../../../../../src/logger-proxy';
|
|
2
3
|
import {
|
|
3
4
|
TaskData,
|
|
4
5
|
CONSULT_TRANSFER_DESTINATION_TYPE,
|
|
6
|
+
TASK_EVENTS,
|
|
7
|
+
VOICE_VARIANT,
|
|
5
8
|
} from '../../../../../../src/services/task/types';
|
|
6
9
|
import {CC_EVENTS} from '../../../../../../src/services/config/types';
|
|
7
10
|
import {TaskEvent, TaskState} from '../../../../../../src/services/task/state-machine';
|
|
8
11
|
import {computeUIControls} from '../../../../../../src/services/task/state-machine/uiControlsComputer';
|
|
9
12
|
import * as Utils from '../../../../../../src/services/core/Utils';
|
|
10
13
|
import {createTaskData} from '../taskTestUtils';
|
|
14
|
+
import MetricsManager from '../../../../../../src/metrics/MetricsManager';
|
|
15
|
+
import {METRIC_EVENT_NAMES} from '../../../../../../src/metrics/constants';
|
|
16
|
+
import * as wxAppDiagnosticLogging from '../../../../../../src/services/wxAppDiagnosticLogging';
|
|
11
17
|
|
|
12
18
|
jest.mock('../../../../../../src/services/core/WebexRequest', () => ({
|
|
13
19
|
__esModule: true,
|
|
@@ -33,7 +39,17 @@ const dummyContact = {
|
|
|
33
39
|
resumeRecording: jest.fn().mockResolvedValue('resumedRecording'),
|
|
34
40
|
consult: jest.fn().mockResolvedValue('consulted'),
|
|
35
41
|
consultConference: jest.fn().mockResolvedValue('conferenceStarted'),
|
|
42
|
+
dropConferenceParticipant: jest.fn().mockResolvedValue({
|
|
43
|
+
type: 'RoutingMessage',
|
|
44
|
+
trackingId: 'drop-tracking-id',
|
|
45
|
+
data: {},
|
|
46
|
+
}),
|
|
36
47
|
consultTransfer: jest.fn().mockResolvedValue('consultTransferred'),
|
|
48
|
+
cancelTask: jest.fn().mockResolvedValue(undefined),
|
|
49
|
+
exitConference: jest.fn().mockResolvedValue({
|
|
50
|
+
trackingId: 'exit-tracking-id',
|
|
51
|
+
data: {},
|
|
52
|
+
}),
|
|
37
53
|
} as any;
|
|
38
54
|
|
|
39
55
|
const createBaseData = (overrides: Partial<TaskData> = {}): TaskData =>
|
|
@@ -94,6 +110,28 @@ describe('Voice Task', () => {
|
|
|
94
110
|
expect(outdialFailedCall).toBeDefined();
|
|
95
111
|
expect(outdialFailedCall![1]).toBe('CUSTOMER_BUSY');
|
|
96
112
|
});
|
|
113
|
+
|
|
114
|
+
it('does not emit task:outdialFailed when suppressOutdialFailedPopup is set', () => {
|
|
115
|
+
const taskData = createBaseData({
|
|
116
|
+
interaction: {
|
|
117
|
+
outboundType: 'OUTDIAL',
|
|
118
|
+
} as any,
|
|
119
|
+
suppressOutdialFailedPopup: true,
|
|
120
|
+
});
|
|
121
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
122
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
123
|
+
|
|
124
|
+
voice.sendStateMachineEvent({
|
|
125
|
+
type: TaskEvent.OUTBOUND_FAILED,
|
|
126
|
+
taskData,
|
|
127
|
+
reason: 'AGENT_ENDS',
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const outdialFailedCall = emitSpy.mock.calls.find(
|
|
131
|
+
(call) => call[0] === 'task:outdialFailed'
|
|
132
|
+
);
|
|
133
|
+
expect(outdialFailedCall).toBeUndefined();
|
|
134
|
+
});
|
|
97
135
|
});
|
|
98
136
|
|
|
99
137
|
it('hides end and endConsult when disabled', () => {
|
|
@@ -560,6 +598,160 @@ describe('Voice Task', () => {
|
|
|
560
598
|
});
|
|
561
599
|
});
|
|
562
600
|
|
|
601
|
+
describe('exitConference()', () => {
|
|
602
|
+
const conferenceTaskData = () =>
|
|
603
|
+
createBaseData({
|
|
604
|
+
agentId: 'agent-1',
|
|
605
|
+
interaction: {
|
|
606
|
+
state: 'conference',
|
|
607
|
+
owner: 'agent-1',
|
|
608
|
+
mainInteractionId: 'int1',
|
|
609
|
+
interactionId: 'int1',
|
|
610
|
+
participants: {
|
|
611
|
+
'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
|
|
612
|
+
'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
|
|
613
|
+
c1: {id: 'c1', pType: 'Customer', type: 'Customer', hasJoined: true, hasLeft: false},
|
|
614
|
+
},
|
|
615
|
+
media: {
|
|
616
|
+
media1: {mediaResourceId: 'media1', isHold: false},
|
|
617
|
+
int1: {
|
|
618
|
+
mediaResourceId: 'int1',
|
|
619
|
+
mType: 'mainCall',
|
|
620
|
+
participants: ['agent-1', 'agent-2', 'c1'],
|
|
621
|
+
isHold: false,
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
} as any,
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
const primeConferencing = (voice: Voice, taskData: TaskData) => {
|
|
628
|
+
primeConnectedState(voice, taskData);
|
|
629
|
+
voice.stateMachineService?.send({type: TaskEvent.CONFERENCE_START, taskData});
|
|
630
|
+
expect(voice.stateMachineService?.getSnapshot().value).toBe(TaskState.CONFERENCING);
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
it('transitions to WRAPPING_UP after exitConference when the actor stays CONNECTED', async () => {
|
|
634
|
+
const taskData = conferenceTaskData();
|
|
635
|
+
const voice = new Voice(dummyContact, taskData, {}, undefined, 'agent-1');
|
|
636
|
+
primeConnectedState(voice, taskData);
|
|
637
|
+
expect(voice.stateMachineService?.getSnapshot().value).toBe(TaskState.CONNECTED);
|
|
638
|
+
|
|
639
|
+
await voice.exitConference();
|
|
640
|
+
|
|
641
|
+
expect(dummyContact.exitConference).toHaveBeenCalled();
|
|
642
|
+
expect(voice.stateMachineService?.getSnapshot().value).toBe(TaskState.WRAPPING_UP);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it('stamps wrapUpRequired on EXIT_CONFERENCE_SUCCESS when this agent should wrap', async () => {
|
|
646
|
+
const taskData = conferenceTaskData();
|
|
647
|
+
const voice = new Voice(dummyContact, taskData, {}, undefined, 'agent-1');
|
|
648
|
+
primeConferencing(voice, taskData);
|
|
649
|
+
const sendSpy = jest.spyOn(voice.stateMachineService as any, 'send');
|
|
650
|
+
|
|
651
|
+
await voice.exitConference();
|
|
652
|
+
|
|
653
|
+
const successEvent = sendSpy.mock.calls
|
|
654
|
+
.map((call) => call[0])
|
|
655
|
+
.find((event) => event?.type === TaskEvent.EXIT_CONFERENCE_SUCCESS);
|
|
656
|
+
|
|
657
|
+
expect(dummyContact.exitConference).toHaveBeenCalled();
|
|
658
|
+
expect(successEvent?.taskData.wrapUpRequired).toBe(true);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('stamps wrapUpRequired false on EXIT_CONFERENCE_SUCCESS when this agent should not wrap', async () => {
|
|
662
|
+
const taskData = createBaseData({
|
|
663
|
+
agentId: 'agent-2',
|
|
664
|
+
isConsulted: true,
|
|
665
|
+
wrapUpRequired: false,
|
|
666
|
+
interaction: {
|
|
667
|
+
state: 'conference',
|
|
668
|
+
owner: 'agent-1',
|
|
669
|
+
mainInteractionId: 'int1',
|
|
670
|
+
interactionId: 'int1',
|
|
671
|
+
participants: {
|
|
672
|
+
'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
|
|
673
|
+
'agent-2': {
|
|
674
|
+
id: 'agent-2',
|
|
675
|
+
pType: 'Agent',
|
|
676
|
+
type: 'Agent',
|
|
677
|
+
hasJoined: true,
|
|
678
|
+
hasLeft: false,
|
|
679
|
+
isWrapUp: false,
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
} as any,
|
|
683
|
+
});
|
|
684
|
+
const voice = new Voice(dummyContact, taskData, {}, undefined, 'agent-2');
|
|
685
|
+
primeConferencing(voice, taskData);
|
|
686
|
+
const sendSpy = jest.spyOn(voice.stateMachineService as any, 'send');
|
|
687
|
+
|
|
688
|
+
await voice.exitConference();
|
|
689
|
+
|
|
690
|
+
const successEvent = sendSpy.mock.calls
|
|
691
|
+
.map((call) => call[0])
|
|
692
|
+
.find((event) => event?.type === TaskEvent.EXIT_CONFERENCE_SUCCESS);
|
|
693
|
+
|
|
694
|
+
expect(successEvent?.taskData.wrapUpRequired).toBe(false);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('stamps wrapUpRequired true on owner-changed exit when isConsulted is omitted', async () => {
|
|
698
|
+
const taskData = createBaseData({
|
|
699
|
+
agentId: 'agent-1',
|
|
700
|
+
wrapUpRequired: false,
|
|
701
|
+
interaction: {
|
|
702
|
+
state: 'conference',
|
|
703
|
+
owner: 'agent-2',
|
|
704
|
+
mainInteractionId: 'int1',
|
|
705
|
+
interactionId: 'int1',
|
|
706
|
+
participants: {
|
|
707
|
+
'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
|
|
708
|
+
'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
|
|
709
|
+
},
|
|
710
|
+
} as any,
|
|
711
|
+
});
|
|
712
|
+
const voice = new Voice(dummyContact, taskData, {}, undefined, 'agent-1');
|
|
713
|
+
primeConferencing(voice, taskData);
|
|
714
|
+
const sendSpy = jest.spyOn(voice.stateMachineService as any, 'send');
|
|
715
|
+
|
|
716
|
+
await voice.exitConference();
|
|
717
|
+
|
|
718
|
+
const successEvent = sendSpy.mock.calls
|
|
719
|
+
.map((call) => call[0])
|
|
720
|
+
.find((event) => event?.type === TaskEvent.EXIT_CONFERENCE_SUCCESS);
|
|
721
|
+
|
|
722
|
+
expect(successEvent?.taskData.wrapUpRequired).toBe(true);
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
it('ignores a stale cached pending list when the exit response omits it', async () => {
|
|
726
|
+
const taskData = createBaseData({
|
|
727
|
+
agentId: 'agent-1',
|
|
728
|
+
wrapUpRequired: false,
|
|
729
|
+
agentsPendingWrapUp: ['agent-2'],
|
|
730
|
+
interaction: {
|
|
731
|
+
state: 'conference',
|
|
732
|
+
owner: 'agent-2',
|
|
733
|
+
mainInteractionId: 'int1',
|
|
734
|
+
interactionId: 'int1',
|
|
735
|
+
participants: {
|
|
736
|
+
'agent-1': {id: 'agent-1', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
|
|
737
|
+
'agent-2': {id: 'agent-2', pType: 'Agent', type: 'Agent', hasJoined: true, hasLeft: false},
|
|
738
|
+
},
|
|
739
|
+
} as any,
|
|
740
|
+
});
|
|
741
|
+
const voice = new Voice(dummyContact, taskData, {}, undefined, 'agent-1');
|
|
742
|
+
primeConferencing(voice, taskData);
|
|
743
|
+
const sendSpy = jest.spyOn(voice.stateMachineService as any, 'send');
|
|
744
|
+
|
|
745
|
+
await voice.exitConference();
|
|
746
|
+
|
|
747
|
+
const successEvent = sendSpy.mock.calls
|
|
748
|
+
.map((call) => call[0])
|
|
749
|
+
.find((event) => event?.type === TaskEvent.EXIT_CONFERENCE_SUCCESS);
|
|
750
|
+
|
|
751
|
+
expect(successEvent?.taskData.wrapUpRequired).toBe(true);
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
|
|
563
755
|
describe('consultConference()', () => {
|
|
564
756
|
afterEach(() => {
|
|
565
757
|
jest.restoreAllMocks();
|
|
@@ -646,4 +838,1224 @@ describe('Voice Task', () => {
|
|
|
646
838
|
});
|
|
647
839
|
});
|
|
648
840
|
});
|
|
841
|
+
|
|
842
|
+
describe('dropConferenceParticipant()', () => {
|
|
843
|
+
afterEach(() => {
|
|
844
|
+
jest.restoreAllMocks();
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it.each([
|
|
848
|
+
['missing payload', undefined],
|
|
849
|
+
['missing participantId', {}],
|
|
850
|
+
['non-string participantId', {participantId: 42}],
|
|
851
|
+
['blank participantId', {participantId: ' '}],
|
|
852
|
+
])('rejects %s before registering a request', async (_label, payload) => {
|
|
853
|
+
const taskData = createBaseData();
|
|
854
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
855
|
+
const timeEventSpy = jest.spyOn(MetricsManager.getInstance(), 'timeEvent');
|
|
856
|
+
|
|
857
|
+
await expect(voice.dropConferenceParticipant(payload as any)).rejects.toThrow(
|
|
858
|
+
'participantId must be a non-empty string'
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
expect(dummyContact.dropConferenceParticipant).not.toHaveBeenCalled();
|
|
862
|
+
expect(timeEventSpy).not.toHaveBeenCalled();
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
it('uses the latest main interaction ID and records PII-safe success telemetry', async () => {
|
|
866
|
+
const participantId = '+1/participant-secret';
|
|
867
|
+
const taskData = createBaseData({
|
|
868
|
+
interaction: {mainInteractionId: 'main-interaction-id'} as any,
|
|
869
|
+
});
|
|
870
|
+
const voice = new Voice(dummyContact, taskData, {}, undefined, 'current-agent-id');
|
|
871
|
+
const metricsManager = MetricsManager.getInstance();
|
|
872
|
+
const timeEventSpy = jest.spyOn(metricsManager, 'timeEvent').mockImplementation();
|
|
873
|
+
const trackEventSpy = jest.spyOn(metricsManager, 'trackEvent').mockImplementation();
|
|
874
|
+
const infoSpy = jest.spyOn(LoggerProxy, 'info').mockImplementation();
|
|
875
|
+
const logSpy = jest.spyOn(LoggerProxy, 'log').mockImplementation();
|
|
876
|
+
|
|
877
|
+
await voice.dropConferenceParticipant({participantId});
|
|
878
|
+
|
|
879
|
+
expect(dummyContact.dropConferenceParticipant).toHaveBeenCalledWith({
|
|
880
|
+
interactionId: 'main-interaction-id',
|
|
881
|
+
participantId,
|
|
882
|
+
});
|
|
883
|
+
expect(timeEventSpy).toHaveBeenCalledWith([
|
|
884
|
+
METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_SUCCESS,
|
|
885
|
+
METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_FAILED,
|
|
886
|
+
]);
|
|
887
|
+
expect(trackEventSpy).toHaveBeenCalledWith(
|
|
888
|
+
METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_SUCCESS,
|
|
889
|
+
expect.objectContaining({
|
|
890
|
+
taskId: 'int1',
|
|
891
|
+
requestInteractionId: 'main-interaction-id',
|
|
892
|
+
agentId: 'current-agent-id',
|
|
893
|
+
}),
|
|
894
|
+
['operational', 'behavioral', 'business']
|
|
895
|
+
);
|
|
896
|
+
|
|
897
|
+
const telemetryAndLogs = JSON.stringify([
|
|
898
|
+
timeEventSpy.mock.calls,
|
|
899
|
+
trackEventSpy.mock.calls,
|
|
900
|
+
infoSpy.mock.calls,
|
|
901
|
+
logSpy.mock.calls,
|
|
902
|
+
]);
|
|
903
|
+
expect(telemetryAndLogs).not.toContain(participantId);
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
it('records PII-safe failure telemetry and rethrows the AQM error', async () => {
|
|
907
|
+
const participantId = '+1/failure-secret';
|
|
908
|
+
const failure = {
|
|
909
|
+
details: {
|
|
910
|
+
type: 'ParticipantDropConferenceFailed',
|
|
911
|
+
trackingId: 'failure-tracking-id',
|
|
912
|
+
data: {reason: participantId},
|
|
913
|
+
},
|
|
914
|
+
};
|
|
915
|
+
dummyContact.dropConferenceParticipant.mockRejectedValueOnce(failure);
|
|
916
|
+
const taskData = createBaseData({
|
|
917
|
+
interaction: {mainInteractionId: 'main-interaction-id'} as any,
|
|
918
|
+
});
|
|
919
|
+
const voice = new Voice(dummyContact, taskData, {}, undefined, 'current-agent-id');
|
|
920
|
+
const metricsManager = MetricsManager.getInstance();
|
|
921
|
+
jest.spyOn(metricsManager, 'timeEvent').mockImplementation();
|
|
922
|
+
const trackEventSpy = jest.spyOn(metricsManager, 'trackEvent').mockImplementation();
|
|
923
|
+
const errorSpy = jest.spyOn(LoggerProxy, 'error').mockImplementation();
|
|
924
|
+
|
|
925
|
+
await expect(voice.dropConferenceParticipant({participantId})).rejects.toBe(failure);
|
|
926
|
+
|
|
927
|
+
expect(trackEventSpy).toHaveBeenCalledWith(
|
|
928
|
+
METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_FAILED,
|
|
929
|
+
expect.objectContaining({
|
|
930
|
+
taskId: 'int1',
|
|
931
|
+
requestInteractionId: 'main-interaction-id',
|
|
932
|
+
agentId: 'current-agent-id',
|
|
933
|
+
}),
|
|
934
|
+
['operational', 'behavioral', 'business']
|
|
935
|
+
);
|
|
936
|
+
expect(errorSpy).toHaveBeenCalledWith('Failed to drop conference participant', {
|
|
937
|
+
module: 'cc',
|
|
938
|
+
method: 'dropConferenceParticipant',
|
|
939
|
+
trackingId: 'failure-tracking-id',
|
|
940
|
+
interactionId: 'main-interaction-id',
|
|
941
|
+
});
|
|
942
|
+
expect(JSON.stringify([trackEventSpy.mock.calls, errorSpy.mock.calls])).not.toContain(
|
|
943
|
+
participantId
|
|
944
|
+
);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
it.each([
|
|
948
|
+
['null', null],
|
|
949
|
+
['undefined', undefined],
|
|
950
|
+
['string', 'primitive failure'],
|
|
951
|
+
['number', 503],
|
|
952
|
+
])('records failure telemetry and preserves a %s rejection', async (_label, failure) => {
|
|
953
|
+
dummyContact.dropConferenceParticipant.mockRejectedValueOnce(failure);
|
|
954
|
+
const voice = new Voice(
|
|
955
|
+
dummyContact,
|
|
956
|
+
createBaseData(),
|
|
957
|
+
{},
|
|
958
|
+
undefined,
|
|
959
|
+
'current-agent-id'
|
|
960
|
+
);
|
|
961
|
+
const metricsManager = MetricsManager.getInstance();
|
|
962
|
+
const failureFieldsSpy = jest.spyOn(
|
|
963
|
+
MetricsManager,
|
|
964
|
+
'getCommonTrackingFieldForAQMResponseFailed'
|
|
965
|
+
);
|
|
966
|
+
jest.spyOn(metricsManager, 'timeEvent').mockImplementation();
|
|
967
|
+
const trackEventSpy = jest.spyOn(metricsManager, 'trackEvent').mockImplementation();
|
|
968
|
+
const errorSpy = jest.spyOn(LoggerProxy, 'error').mockImplementation();
|
|
969
|
+
|
|
970
|
+
await expect(
|
|
971
|
+
voice.dropConferenceParticipant({participantId: 'participant-id'})
|
|
972
|
+
).rejects.toBe(failure);
|
|
973
|
+
|
|
974
|
+
expect(failureFieldsSpy).toHaveBeenCalledWith({});
|
|
975
|
+
expect(trackEventSpy).toHaveBeenCalledWith(
|
|
976
|
+
METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_FAILED,
|
|
977
|
+
expect.objectContaining({
|
|
978
|
+
taskId: 'int1',
|
|
979
|
+
requestInteractionId: 'int1',
|
|
980
|
+
agentId: 'current-agent-id',
|
|
981
|
+
}),
|
|
982
|
+
['operational', 'behavioral', 'business']
|
|
983
|
+
);
|
|
984
|
+
expect(errorSpy).toHaveBeenCalledWith('Failed to drop conference participant', {
|
|
985
|
+
module: 'cc',
|
|
986
|
+
method: 'dropConferenceParticipant',
|
|
987
|
+
trackingId: undefined,
|
|
988
|
+
interactionId: 'int1',
|
|
989
|
+
});
|
|
990
|
+
});
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
describe('setEnableWxBetterTogether', () => {
|
|
994
|
+
it('updates runtime flag, uiControlConfig, and emits ui control updates', () => {
|
|
995
|
+
const voice = new Voice(dummyContact, createBaseData(), {enableWxBetterTogether: true});
|
|
996
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
997
|
+
|
|
998
|
+
voice.setEnableWxBetterTogether(false);
|
|
999
|
+
|
|
1000
|
+
expect(voice['enableWxBetterTogether']).toBe(false);
|
|
1001
|
+
expect(voice['uiControlConfig'].enableWxBetterTogether).toBe(false);
|
|
1002
|
+
expect(emitSpy).toHaveBeenCalledWith(
|
|
1003
|
+
TASK_EVENTS.TASK_UI_CONTROLS_UPDATED,
|
|
1004
|
+
expect.any(Object)
|
|
1005
|
+
);
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
describe('wxApp offer decision logging', () => {
|
|
1010
|
+
const wxAppParticipant = {
|
|
1011
|
+
deviceType: 'wxApp',
|
|
1012
|
+
deviceId: 'device-id-1',
|
|
1013
|
+
deviceCallId: 'call-id-1',
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
const makeWxAppOfferTaskData = () =>
|
|
1017
|
+
createBaseData({
|
|
1018
|
+
agentId: 'agent-1',
|
|
1019
|
+
interaction: {
|
|
1020
|
+
participants: {
|
|
1021
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1022
|
+
},
|
|
1023
|
+
} as any,
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
it('logs evolving accept reasons through the normal wxApp accept flow', () => {
|
|
1027
|
+
const logSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferDecision');
|
|
1028
|
+
const taskData = makeWxAppOfferTaskData();
|
|
1029
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
1030
|
+
|
|
1031
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1032
|
+
|
|
1033
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
1034
|
+
expect.objectContaining({acceptReason: 'wxApp_offer_ready'})
|
|
1035
|
+
);
|
|
1036
|
+
|
|
1037
|
+
logSpy.mockClear();
|
|
1038
|
+
voice['setWxAppAcceptInFlight'](true);
|
|
1039
|
+
|
|
1040
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
1041
|
+
expect.objectContaining({acceptReason: 'wxApp_accept_in_flight'})
|
|
1042
|
+
);
|
|
1043
|
+
|
|
1044
|
+
logSpy.mockClear();
|
|
1045
|
+
voice['setWxAppAcceptInFlight'](false);
|
|
1046
|
+
voice['setWxAppAnswerPending'](true);
|
|
1047
|
+
|
|
1048
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
1049
|
+
expect.objectContaining({acceptReason: 'wxApp_answer_pending'})
|
|
1050
|
+
);
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
it('logs browser_webrtc_offer when voice variant is WebRTC on a non-wxApp inbound offer', () => {
|
|
1054
|
+
const logSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferDecision');
|
|
1055
|
+
const taskData = createBaseData({
|
|
1056
|
+
agentId: 'agent-1',
|
|
1057
|
+
interaction: {
|
|
1058
|
+
participants: {
|
|
1059
|
+
'agent-1': {id: 'agent-1', deviceType: 'phone', deviceId: 'device-id-1'},
|
|
1060
|
+
},
|
|
1061
|
+
} as any,
|
|
1062
|
+
});
|
|
1063
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1064
|
+
enableWxBetterTogether: true,
|
|
1065
|
+
voiceVariant: VOICE_VARIANT.WEBRTC,
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1069
|
+
|
|
1070
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
1071
|
+
expect.objectContaining({acceptReason: 'browser_webrtc_offer'})
|
|
1072
|
+
);
|
|
1073
|
+
});
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
describe('applyWxAppMuteStateFromSync', () => {
|
|
1077
|
+
const wxAppParticipant = {
|
|
1078
|
+
deviceType: 'wxApp',
|
|
1079
|
+
deviceId: 'device-id-1',
|
|
1080
|
+
deviceCallId: 'call-id-1',
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
const makeWxAppTaskData = () =>
|
|
1084
|
+
createBaseData({
|
|
1085
|
+
agentId: 'agent-1',
|
|
1086
|
+
interaction: {
|
|
1087
|
+
participants: {
|
|
1088
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1089
|
+
},
|
|
1090
|
+
} as any,
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
it('emits TASK_WXAPP_MUTE_STATE_UPDATED when callId matches via endsWith', () => {
|
|
1094
|
+
const taskData = makeWxAppTaskData();
|
|
1095
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
1096
|
+
primeConnectedState(voice, taskData);
|
|
1097
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
1098
|
+
|
|
1099
|
+
voice.applyWxAppMuteStateFromSync('prefix:call-id-1', true);
|
|
1100
|
+
|
|
1101
|
+
expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {muted: true});
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it('no-ops when callId does not match active call', () => {
|
|
1105
|
+
const taskData = makeWxAppTaskData();
|
|
1106
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
1107
|
+
primeConnectedState(voice, taskData);
|
|
1108
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
1109
|
+
|
|
1110
|
+
voice.applyWxAppMuteStateFromSync('other-call-id', true);
|
|
1111
|
+
|
|
1112
|
+
expect(emitSpy).not.toHaveBeenCalledWith(
|
|
1113
|
+
TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED,
|
|
1114
|
+
expect.anything()
|
|
1115
|
+
);
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
it('no-ops when enableWxBetterTogether is false', () => {
|
|
1119
|
+
const taskData = makeWxAppTaskData();
|
|
1120
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: false});
|
|
1121
|
+
primeConnectedState(voice, taskData);
|
|
1122
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
1123
|
+
|
|
1124
|
+
voice.applyWxAppMuteStateFromSync('prefix:call-id-1', true);
|
|
1125
|
+
|
|
1126
|
+
expect(emitSpy).not.toHaveBeenCalledWith(
|
|
1127
|
+
TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED,
|
|
1128
|
+
expect.anything()
|
|
1129
|
+
);
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
it('no-ops when muted state is unchanged', () => {
|
|
1133
|
+
const taskData = makeWxAppTaskData();
|
|
1134
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
1135
|
+
primeConnectedState(voice, taskData);
|
|
1136
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
1137
|
+
|
|
1138
|
+
voice.applyWxAppMuteStateFromSync('prefix:call-id-1', true);
|
|
1139
|
+
emitSpy.mockClear();
|
|
1140
|
+
|
|
1141
|
+
voice.applyWxAppMuteStateFromSync('prefix:call-id-1', true);
|
|
1142
|
+
|
|
1143
|
+
expect(emitSpy).not.toHaveBeenCalledWith(
|
|
1144
|
+
TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED,
|
|
1145
|
+
expect.anything()
|
|
1146
|
+
);
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
describe('syncWxAppMuteFromCallDetails', () => {
|
|
1151
|
+
const wxAppParticipant = {
|
|
1152
|
+
deviceType: 'wxApp',
|
|
1153
|
+
deviceId: 'device-id-1',
|
|
1154
|
+
deviceCallId: 'call-id-1',
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
const makeWxAppTaskData = () =>
|
|
1158
|
+
createBaseData({
|
|
1159
|
+
agentId: 'agent-1',
|
|
1160
|
+
interaction: {
|
|
1161
|
+
participants: {
|
|
1162
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1163
|
+
},
|
|
1164
|
+
} as any,
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
it('seeds mute state from telephony GET call details on engaged wxApp call', async () => {
|
|
1168
|
+
const mockSvc = {
|
|
1169
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: true}),
|
|
1170
|
+
};
|
|
1171
|
+
const taskData = makeWxAppTaskData();
|
|
1172
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1173
|
+
enableWxBetterTogether: true,
|
|
1174
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1175
|
+
});
|
|
1176
|
+
primeConnectedState(voice, taskData);
|
|
1177
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
1178
|
+
|
|
1179
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1180
|
+
|
|
1181
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledWith({callId: 'call-id-1'});
|
|
1182
|
+
expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {
|
|
1183
|
+
muted: true,
|
|
1184
|
+
});
|
|
1185
|
+
expect(voice.getWxAppMuted()).toBe(true);
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
it('returns resolved mute state from syncWxAppMuteFromCallDetails', async () => {
|
|
1189
|
+
const mockSvc = {
|
|
1190
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: true}),
|
|
1191
|
+
};
|
|
1192
|
+
const taskData = makeWxAppTaskData();
|
|
1193
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1194
|
+
enableWxBetterTogether: true,
|
|
1195
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1196
|
+
});
|
|
1197
|
+
primeConnectedState(voice, taskData);
|
|
1198
|
+
|
|
1199
|
+
const result = await voice.syncWxAppMuteFromCallDetails();
|
|
1200
|
+
|
|
1201
|
+
expect(result).toBe(true);
|
|
1202
|
+
expect(voice.getWxAppMuted()).toBe(true);
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
it('seeds mute via participant deviceCallId when state machine is still IDLE', async () => {
|
|
1206
|
+
const mockSvc = {
|
|
1207
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: true}),
|
|
1208
|
+
};
|
|
1209
|
+
const taskData = makeWxAppTaskData();
|
|
1210
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1211
|
+
enableWxBetterTogether: true,
|
|
1212
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1213
|
+
});
|
|
1214
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
1215
|
+
|
|
1216
|
+
const result = await voice.syncWxAppMuteFromCallDetails();
|
|
1217
|
+
|
|
1218
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledWith({callId: 'call-id-1'});
|
|
1219
|
+
expect(result).toBe(true);
|
|
1220
|
+
expect(voice.getWxAppMuted()).toBe(true);
|
|
1221
|
+
expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {
|
|
1222
|
+
muted: true,
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
it('no-ops when enableWxBetterTogether is false', async () => {
|
|
1227
|
+
const mockSvc = {getCallDetails: jest.fn()};
|
|
1228
|
+
const taskData = makeWxAppTaskData();
|
|
1229
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1230
|
+
enableWxBetterTogether: false,
|
|
1231
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1232
|
+
});
|
|
1233
|
+
primeConnectedState(voice, taskData);
|
|
1234
|
+
|
|
1235
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1236
|
+
|
|
1237
|
+
expect(mockSvc.getCallDetails).not.toHaveBeenCalled();
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
it('syncs mute state after task assignment once CONNECTED (onTaskAssigned)', async () => {
|
|
1241
|
+
const mockSvc = {
|
|
1242
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: true}),
|
|
1243
|
+
};
|
|
1244
|
+
const taskData = makeWxAppTaskData();
|
|
1245
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1246
|
+
enableWxBetterTogether: true,
|
|
1247
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1251
|
+
expect(voice.getWxAppMuted()).toBe(false);
|
|
1252
|
+
voice.stateMachineService?.send({type: TaskEvent.ASSIGN, taskData});
|
|
1253
|
+
|
|
1254
|
+
await new Promise<void>((resolve) => {
|
|
1255
|
+
setImmediate(resolve);
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledWith({callId: 'call-id-1'});
|
|
1259
|
+
expect(voice.getWxAppMuted()).toBe(true);
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
it('seeds mute on ASSIGN when wxApp offer was accepted on another session', async () => {
|
|
1263
|
+
const mockSvc = {
|
|
1264
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: false}),
|
|
1265
|
+
};
|
|
1266
|
+
const taskData = makeWxAppTaskData();
|
|
1267
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1268
|
+
enableWxBetterTogether: true,
|
|
1269
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1273
|
+
expect(voice['uiControlConfig']?.wxAppAnswerPending).toBeFalsy();
|
|
1274
|
+
voice.stateMachineService?.send({type: TaskEvent.ASSIGN, taskData});
|
|
1275
|
+
|
|
1276
|
+
await new Promise<void>((resolve) => {
|
|
1277
|
+
setImmediate(resolve);
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledWith({callId: 'call-id-1'});
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
it('syncs mute state after task hydrate (onTaskHydrated)', async () => {
|
|
1284
|
+
const mockSvc = {
|
|
1285
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: true}),
|
|
1286
|
+
};
|
|
1287
|
+
const taskData = makeWxAppTaskData();
|
|
1288
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1289
|
+
enableWxBetterTogether: true,
|
|
1290
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1291
|
+
});
|
|
1292
|
+
primeConnectedState(voice, taskData);
|
|
1293
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
1294
|
+
|
|
1295
|
+
voice.onTaskHydrated();
|
|
1296
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1297
|
+
|
|
1298
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledWith({callId: 'call-id-1'});
|
|
1299
|
+
expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {
|
|
1300
|
+
muted: true,
|
|
1301
|
+
});
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
it('preserves wxAppAnswerPending on hydrate while OFFERED and answer is pending', async () => {
|
|
1305
|
+
const mockSvc = {
|
|
1306
|
+
answerCall: jest.fn().mockResolvedValue(undefined),
|
|
1307
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: false}),
|
|
1308
|
+
};
|
|
1309
|
+
const taskData = makeWxAppTaskData();
|
|
1310
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1311
|
+
enableWxBetterTogether: true,
|
|
1312
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1313
|
+
});
|
|
1314
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1315
|
+
|
|
1316
|
+
await voice.accept();
|
|
1317
|
+
expect(voice.uiControlConfig.wxAppAnswerPending).toBe(true);
|
|
1318
|
+
|
|
1319
|
+
voice.onTaskHydrated();
|
|
1320
|
+
|
|
1321
|
+
expect(voice.uiControlConfig.wxAppAnswerPending).toBe(true);
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
it('clears wxAppAnswerPending on hydrate when task is no longer OFFERED', async () => {
|
|
1325
|
+
const mockSvc = {
|
|
1326
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: false}),
|
|
1327
|
+
};
|
|
1328
|
+
const taskData = makeWxAppTaskData();
|
|
1329
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1330
|
+
enableWxBetterTogether: true,
|
|
1331
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1332
|
+
});
|
|
1333
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1334
|
+
voice.uiControlConfig = {...voice.uiControlConfig, wxAppAnswerPending: true};
|
|
1335
|
+
primeConnectedState(voice, taskData);
|
|
1336
|
+
|
|
1337
|
+
voice.onTaskHydrated();
|
|
1338
|
+
|
|
1339
|
+
expect(voice.uiControlConfig.wxAppAnswerPending).toBe(false);
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
it('coalesces parallel syncWxAppMuteFromCallDetails into one getCallDetails', async () => {
|
|
1343
|
+
let resolveGetCallDetails: (value: {muted: boolean}) => void;
|
|
1344
|
+
const getCallDetailsPromise = new Promise<{muted: boolean}>((resolve) => {
|
|
1345
|
+
resolveGetCallDetails = resolve;
|
|
1346
|
+
});
|
|
1347
|
+
const mockSvc = {
|
|
1348
|
+
getCallDetails: jest.fn().mockReturnValue(getCallDetailsPromise),
|
|
1349
|
+
};
|
|
1350
|
+
const taskData = makeWxAppTaskData();
|
|
1351
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1352
|
+
enableWxBetterTogether: true,
|
|
1353
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1354
|
+
});
|
|
1355
|
+
primeConnectedState(voice, taskData);
|
|
1356
|
+
|
|
1357
|
+
const sync1 = voice.syncWxAppMuteFromCallDetails();
|
|
1358
|
+
const sync2 = voice.syncWxAppMuteFromCallDetails();
|
|
1359
|
+
|
|
1360
|
+
resolveGetCallDetails!({muted: true});
|
|
1361
|
+
const [result1, result2] = await Promise.all([sync1, sync2]);
|
|
1362
|
+
|
|
1363
|
+
expect(result1).toBe(true);
|
|
1364
|
+
expect(result2).toBe(true);
|
|
1365
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledTimes(1);
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
it('issues a new getCallDetails on sequential syncWxAppMuteFromCallDetails after the first completes', async () => {
|
|
1369
|
+
const mockSvc = {
|
|
1370
|
+
getCallDetails: jest
|
|
1371
|
+
.fn()
|
|
1372
|
+
.mockResolvedValueOnce({muted: false})
|
|
1373
|
+
.mockResolvedValueOnce({muted: true}),
|
|
1374
|
+
};
|
|
1375
|
+
const taskData = makeWxAppTaskData();
|
|
1376
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1377
|
+
enableWxBetterTogether: true,
|
|
1378
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1379
|
+
});
|
|
1380
|
+
primeConnectedState(voice, taskData);
|
|
1381
|
+
|
|
1382
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1383
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1384
|
+
|
|
1385
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledTimes(2);
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
it('skips mute GET for terminated wxApp outdial with deviceCallId', async () => {
|
|
1389
|
+
const mockSvc = {getCallDetails: jest.fn()};
|
|
1390
|
+
const taskData = createBaseData({
|
|
1391
|
+
agentId: 'agent-1',
|
|
1392
|
+
interaction: {
|
|
1393
|
+
outboundType: 'OUTDIAL',
|
|
1394
|
+
isTerminated: true,
|
|
1395
|
+
participants: {
|
|
1396
|
+
'agent-1': {
|
|
1397
|
+
id: 'agent-1',
|
|
1398
|
+
deviceType: 'wxApp',
|
|
1399
|
+
deviceId: 'device-id-1',
|
|
1400
|
+
deviceCallId: 'callhalf-dead',
|
|
1401
|
+
},
|
|
1402
|
+
},
|
|
1403
|
+
} as any,
|
|
1404
|
+
});
|
|
1405
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1406
|
+
enableWxBetterTogether: true,
|
|
1407
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1408
|
+
});
|
|
1409
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1410
|
+
|
|
1411
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1412
|
+
|
|
1413
|
+
expect(mockSvc.getCallDetails).not.toHaveBeenCalled();
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
it('skips mute GET for pre-accept wxApp OFFERED offer', async () => {
|
|
1417
|
+
const mockSvc = {getCallDetails: jest.fn()};
|
|
1418
|
+
const wxAppParticipant = {
|
|
1419
|
+
deviceType: 'wxApp',
|
|
1420
|
+
deviceId: 'device-id-1',
|
|
1421
|
+
deviceCallId: 'call-id-1',
|
|
1422
|
+
};
|
|
1423
|
+
const taskData = createBaseData({
|
|
1424
|
+
agentId: 'agent-1',
|
|
1425
|
+
interaction: {
|
|
1426
|
+
outboundType: 'OUTDIAL',
|
|
1427
|
+
participants: {
|
|
1428
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1429
|
+
},
|
|
1430
|
+
} as any,
|
|
1431
|
+
});
|
|
1432
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1433
|
+
enableWxBetterTogether: true,
|
|
1434
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1435
|
+
});
|
|
1436
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1437
|
+
|
|
1438
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1439
|
+
|
|
1440
|
+
expect(mockSvc.getCallDetails).not.toHaveBeenCalled();
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
it('still calls mute GET for post-accept OFFERED wxApp offer', async () => {
|
|
1444
|
+
const mockSvc = {
|
|
1445
|
+
answerCall: jest.fn().mockResolvedValue(undefined),
|
|
1446
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: false}),
|
|
1447
|
+
};
|
|
1448
|
+
const wxAppParticipant = {
|
|
1449
|
+
deviceType: 'wxApp',
|
|
1450
|
+
deviceId: 'device-id-1',
|
|
1451
|
+
deviceCallId: 'call-id-1',
|
|
1452
|
+
};
|
|
1453
|
+
const taskData = createBaseData({
|
|
1454
|
+
agentId: 'agent-1',
|
|
1455
|
+
interaction: {
|
|
1456
|
+
outboundType: 'OUTDIAL',
|
|
1457
|
+
participants: {
|
|
1458
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1459
|
+
},
|
|
1460
|
+
} as any,
|
|
1461
|
+
});
|
|
1462
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1463
|
+
enableWxBetterTogether: true,
|
|
1464
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1465
|
+
});
|
|
1466
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1467
|
+
await voice.accept();
|
|
1468
|
+
|
|
1469
|
+
expect(voice.uiControlConfig.wxAppAcceptInFlight).toBe(false);
|
|
1470
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1471
|
+
|
|
1472
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledWith({callId: 'call-id-1'});
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
it('does not log error for expected wxApp call-not-found on mute sync', async () => {
|
|
1476
|
+
const errorSpy = jest.spyOn(LoggerProxy, 'error');
|
|
1477
|
+
const mockSvc = {
|
|
1478
|
+
getCallDetails: jest.fn().mockRejectedValue({status: 400, message: 'Call not found'}),
|
|
1479
|
+
};
|
|
1480
|
+
const wxAppParticipant = {
|
|
1481
|
+
deviceType: 'wxApp',
|
|
1482
|
+
deviceId: 'device-id-1',
|
|
1483
|
+
deviceCallId: 'call-id-1',
|
|
1484
|
+
};
|
|
1485
|
+
const taskData = createBaseData({
|
|
1486
|
+
agentId: 'agent-1',
|
|
1487
|
+
interaction: {
|
|
1488
|
+
participants: {
|
|
1489
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1490
|
+
},
|
|
1491
|
+
} as any,
|
|
1492
|
+
});
|
|
1493
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1494
|
+
enableWxBetterTogether: true,
|
|
1495
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1496
|
+
});
|
|
1497
|
+
primeConnectedState(voice, taskData);
|
|
1498
|
+
|
|
1499
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1500
|
+
|
|
1501
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalled();
|
|
1502
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
1503
|
+
errorSpy.mockRestore();
|
|
1504
|
+
});
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
describe('decline', () => {
|
|
1508
|
+
const wxAppParticipant = {
|
|
1509
|
+
deviceType: 'wxApp',
|
|
1510
|
+
deviceId: 'device-id-1',
|
|
1511
|
+
deviceCallId: 'call-id-1',
|
|
1512
|
+
};
|
|
1513
|
+
|
|
1514
|
+
const makeWxAppOutdialTaskData = () =>
|
|
1515
|
+
createBaseData({
|
|
1516
|
+
agentId: 'agent-1',
|
|
1517
|
+
interaction: {
|
|
1518
|
+
outboundType: 'OUTDIAL',
|
|
1519
|
+
participants: {
|
|
1520
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1521
|
+
},
|
|
1522
|
+
} as any,
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
it('cancels wxApp outdial via contact.cancelTask', async () => {
|
|
1526
|
+
const taskData = makeWxAppOutdialTaskData();
|
|
1527
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
1528
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1529
|
+
|
|
1530
|
+
await voice.decline();
|
|
1531
|
+
|
|
1532
|
+
expect(dummyContact.cancelTask).toHaveBeenCalledWith({interactionId: 'int1'});
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
it('emits TASK_DECLINE and WXAPP_TASK_DECLINE success metrics for wxApp outdial decline', async () => {
|
|
1536
|
+
const taskData = makeWxAppOutdialTaskData();
|
|
1537
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
1538
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1539
|
+
const metricsManager = MetricsManager.getInstance();
|
|
1540
|
+
const trackEventSpy = jest.spyOn(metricsManager, 'trackEvent');
|
|
1541
|
+
|
|
1542
|
+
await voice.decline();
|
|
1543
|
+
|
|
1544
|
+
expect(trackEventSpy).toHaveBeenCalledWith(
|
|
1545
|
+
METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
|
|
1546
|
+
expect.objectContaining({taskId: 'int1'}),
|
|
1547
|
+
['operational', 'behavioral']
|
|
1548
|
+
);
|
|
1549
|
+
expect(trackEventSpy).toHaveBeenCalledWith(
|
|
1550
|
+
METRIC_EVENT_NAMES.TASK_DECLINE_SUCCESS,
|
|
1551
|
+
expect.objectContaining({taskId: 'int1'}),
|
|
1552
|
+
['operational', 'behavioral']
|
|
1553
|
+
);
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
it('emits TASK_DECLINE and WXAPP_TASK_DECLINE failed metrics when outdial cancel fails', async () => {
|
|
1557
|
+
const cancelError = Object.assign(new Error('AQM failed'), {
|
|
1558
|
+
details: {
|
|
1559
|
+
trackingId: 'aqm-track-1',
|
|
1560
|
+
orgId: 'org-1',
|
|
1561
|
+
type: 'Service.aqm.task.cancel',
|
|
1562
|
+
data: {reason: 'TASK_NOT_FOUND', reasonCode: 404, agentId: 'agent-1'},
|
|
1563
|
+
},
|
|
1564
|
+
});
|
|
1565
|
+
dummyContact.cancelTask = jest.fn().mockRejectedValue(cancelError);
|
|
1566
|
+
const taskData = makeWxAppOutdialTaskData();
|
|
1567
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
1568
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1569
|
+
const metricsManager = MetricsManager.getInstance();
|
|
1570
|
+
const trackEventSpy = jest.spyOn(metricsManager, 'trackEvent');
|
|
1571
|
+
|
|
1572
|
+
await expect(voice.decline()).rejects.toThrow('AQM failed');
|
|
1573
|
+
|
|
1574
|
+
expect(trackEventSpy).toHaveBeenCalledWith(
|
|
1575
|
+
METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_FAILED,
|
|
1576
|
+
expect.objectContaining({
|
|
1577
|
+
taskId: 'int1',
|
|
1578
|
+
trackingId: 'aqm-track-1',
|
|
1579
|
+
failureReason: 'TASK_NOT_FOUND',
|
|
1580
|
+
}),
|
|
1581
|
+
['operational', 'behavioral']
|
|
1582
|
+
);
|
|
1583
|
+
expect(trackEventSpy).toHaveBeenCalledWith(
|
|
1584
|
+
METRIC_EVENT_NAMES.TASK_DECLINE_FAILED,
|
|
1585
|
+
expect.objectContaining({
|
|
1586
|
+
taskId: 'int1',
|
|
1587
|
+
trackingId: 'aqm-track-1',
|
|
1588
|
+
failureReason: 'TASK_NOT_FOUND',
|
|
1589
|
+
}),
|
|
1590
|
+
['operational', 'behavioral']
|
|
1591
|
+
);
|
|
1592
|
+
dummyContact.cancelTask = jest.fn().mockResolvedValue(undefined);
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
it('clears wxAppAnswerPending when declining outdial', async () => {
|
|
1596
|
+
const mockSvc = {
|
|
1597
|
+
answerCall: jest.fn().mockResolvedValue(undefined),
|
|
1598
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: false}),
|
|
1599
|
+
};
|
|
1600
|
+
const taskData = makeWxAppOutdialTaskData();
|
|
1601
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1602
|
+
enableWxBetterTogether: true,
|
|
1603
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1604
|
+
});
|
|
1605
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1606
|
+
|
|
1607
|
+
await voice.accept();
|
|
1608
|
+
expect(voice.uiControlConfig.wxAppAnswerPending).toBe(true);
|
|
1609
|
+
expect(voice.uiControlConfig.wxAppAcceptInFlight).toBe(false);
|
|
1610
|
+
|
|
1611
|
+
await voice.decline();
|
|
1612
|
+
|
|
1613
|
+
expect(voice.uiControlConfig.wxAppAnswerPending).toBe(false);
|
|
1614
|
+
expect(dummyContact.cancelTask).toHaveBeenCalledWith({interactionId: 'int1'});
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
it('throws unsupported for non-outdial voice tasks', async () => {
|
|
1618
|
+
const taskData = createBaseData();
|
|
1619
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
1620
|
+
|
|
1621
|
+
await expect(voice.decline()).rejects.toThrow('Unsupported operation: decline');
|
|
1622
|
+
expect(dummyContact.cancelTask).not.toHaveBeenCalled();
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
it('throws unsupported when enableWxBetterTogether is false', async () => {
|
|
1626
|
+
const taskData = makeWxAppOutdialTaskData();
|
|
1627
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: false});
|
|
1628
|
+
|
|
1629
|
+
await expect(voice.decline()).rejects.toThrow('Unsupported operation: decline');
|
|
1630
|
+
expect(dummyContact.cancelTask).not.toHaveBeenCalled();
|
|
1631
|
+
});
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
describe('unified task API routing (WXCC-6026)', () => {
|
|
1635
|
+
const wxAppParticipant = {
|
|
1636
|
+
deviceType: 'wxApp',
|
|
1637
|
+
deviceId: 'device-id-1',
|
|
1638
|
+
deviceCallId: 'call-id-1',
|
|
1639
|
+
};
|
|
1640
|
+
|
|
1641
|
+
it('accept() routes wxApp inbound offer to telephony answerCall', async () => {
|
|
1642
|
+
const mockSvc = {
|
|
1643
|
+
answerCall: jest.fn().mockResolvedValue(undefined),
|
|
1644
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: false}),
|
|
1645
|
+
};
|
|
1646
|
+
const taskData = createBaseData({
|
|
1647
|
+
agentId: 'agent-1',
|
|
1648
|
+
interaction: {
|
|
1649
|
+
participants: {
|
|
1650
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1651
|
+
},
|
|
1652
|
+
} as any,
|
|
1653
|
+
});
|
|
1654
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1655
|
+
enableWxBetterTogether: true,
|
|
1656
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1657
|
+
});
|
|
1658
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1659
|
+
|
|
1660
|
+
await voice.accept();
|
|
1661
|
+
|
|
1662
|
+
expect(mockSvc.answerCall).toHaveBeenCalledWith({
|
|
1663
|
+
callId: 'call-id-1',
|
|
1664
|
+
endpointId: 'device-id-1',
|
|
1665
|
+
lineOwnerId: undefined,
|
|
1666
|
+
});
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
it('decline() routes wxApp inbound offer to telephony rejectCall', async () => {
|
|
1670
|
+
const mockSvc = {
|
|
1671
|
+
rejectCall: jest.fn().mockResolvedValue(undefined),
|
|
1672
|
+
};
|
|
1673
|
+
const taskData = createBaseData({
|
|
1674
|
+
agentId: 'agent-1',
|
|
1675
|
+
interaction: {
|
|
1676
|
+
participants: {
|
|
1677
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1678
|
+
},
|
|
1679
|
+
} as any,
|
|
1680
|
+
});
|
|
1681
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1682
|
+
enableWxBetterTogether: true,
|
|
1683
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1684
|
+
});
|
|
1685
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1686
|
+
|
|
1687
|
+
await voice.decline();
|
|
1688
|
+
|
|
1689
|
+
expect(mockSvc.rejectCall).toHaveBeenCalledWith({
|
|
1690
|
+
callId: 'call-id-1',
|
|
1691
|
+
lineOwnerId: undefined,
|
|
1692
|
+
});
|
|
1693
|
+
expect(dummyContact.cancelTask).not.toHaveBeenCalled();
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
it('toggleMute() routes wxApp engaged call to telephony muteCall', async () => {
|
|
1697
|
+
const mockSvc = {
|
|
1698
|
+
muteCall: jest.fn().mockResolvedValue(undefined),
|
|
1699
|
+
};
|
|
1700
|
+
const taskData = createBaseData({
|
|
1701
|
+
agentId: 'agent-1',
|
|
1702
|
+
interaction: {
|
|
1703
|
+
participants: {
|
|
1704
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1705
|
+
},
|
|
1706
|
+
} as any,
|
|
1707
|
+
});
|
|
1708
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1709
|
+
enableWxBetterTogether: true,
|
|
1710
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1711
|
+
});
|
|
1712
|
+
primeConnectedState(voice, taskData);
|
|
1713
|
+
|
|
1714
|
+
await voice.toggleMute({muted: true});
|
|
1715
|
+
|
|
1716
|
+
expect(mockSvc.muteCall).toHaveBeenCalledWith({callId: 'call-id-1', lineOwnerId: undefined});
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
it('serializes concurrent no-arg toggleMute() so mute then unmute', async () => {
|
|
1720
|
+
let resolveMute: () => void;
|
|
1721
|
+
const muteGate = new Promise<void>((resolve) => {
|
|
1722
|
+
resolveMute = resolve;
|
|
1723
|
+
});
|
|
1724
|
+
const mockSvc = {
|
|
1725
|
+
muteCall: jest.fn().mockImplementation(() => muteGate),
|
|
1726
|
+
unmuteCall: jest.fn().mockResolvedValue(undefined),
|
|
1727
|
+
};
|
|
1728
|
+
const taskData = createBaseData({
|
|
1729
|
+
agentId: 'agent-1',
|
|
1730
|
+
interaction: {
|
|
1731
|
+
participants: {
|
|
1732
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1733
|
+
},
|
|
1734
|
+
} as any,
|
|
1735
|
+
});
|
|
1736
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1737
|
+
enableWxBetterTogether: true,
|
|
1738
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1739
|
+
});
|
|
1740
|
+
primeConnectedState(voice, taskData);
|
|
1741
|
+
expect(voice.getWxAppMuted()).toBe(false);
|
|
1742
|
+
|
|
1743
|
+
const firstToggle = voice.toggleMute();
|
|
1744
|
+
const secondToggle = voice.toggleMute();
|
|
1745
|
+
|
|
1746
|
+
await new Promise<void>((resolve) => {
|
|
1747
|
+
setImmediate(resolve);
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
expect(mockSvc.muteCall).toHaveBeenCalledTimes(1);
|
|
1751
|
+
expect(mockSvc.unmuteCall).not.toHaveBeenCalled();
|
|
1752
|
+
|
|
1753
|
+
resolveMute!();
|
|
1754
|
+
await Promise.all([firstToggle, secondToggle]);
|
|
1755
|
+
|
|
1756
|
+
expect(mockSvc.muteCall).toHaveBeenCalledTimes(1);
|
|
1757
|
+
expect(mockSvc.unmuteCall).toHaveBeenCalledTimes(1);
|
|
1758
|
+
expect(voice.getWxAppMuted()).toBe(false);
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
it('queues three concurrent no-arg toggleMute() calls in order', async () => {
|
|
1762
|
+
let resolveFirstMute: () => void;
|
|
1763
|
+
const firstMuteGate = new Promise<void>((resolve) => {
|
|
1764
|
+
resolveFirstMute = resolve;
|
|
1765
|
+
});
|
|
1766
|
+
const mockSvc = {
|
|
1767
|
+
muteCall: jest
|
|
1768
|
+
.fn()
|
|
1769
|
+
.mockImplementationOnce(() => firstMuteGate)
|
|
1770
|
+
.mockResolvedValue(undefined),
|
|
1771
|
+
unmuteCall: jest.fn().mockResolvedValue(undefined),
|
|
1772
|
+
};
|
|
1773
|
+
const taskData = createBaseData({
|
|
1774
|
+
agentId: 'agent-1',
|
|
1775
|
+
interaction: {
|
|
1776
|
+
participants: {
|
|
1777
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1778
|
+
},
|
|
1779
|
+
} as any,
|
|
1780
|
+
});
|
|
1781
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1782
|
+
enableWxBetterTogether: true,
|
|
1783
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1784
|
+
});
|
|
1785
|
+
primeConnectedState(voice, taskData);
|
|
1786
|
+
expect(voice.getWxAppMuted()).toBe(false);
|
|
1787
|
+
|
|
1788
|
+
const firstToggle = voice.toggleMute();
|
|
1789
|
+
const secondToggle = voice.toggleMute();
|
|
1790
|
+
const thirdToggle = voice.toggleMute();
|
|
1791
|
+
|
|
1792
|
+
await new Promise<void>((resolve) => {
|
|
1793
|
+
setImmediate(resolve);
|
|
1794
|
+
});
|
|
1795
|
+
|
|
1796
|
+
expect(mockSvc.muteCall).toHaveBeenCalledTimes(1);
|
|
1797
|
+
expect(mockSvc.unmuteCall).not.toHaveBeenCalled();
|
|
1798
|
+
|
|
1799
|
+
resolveFirstMute!();
|
|
1800
|
+
await Promise.all([firstToggle, secondToggle, thirdToggle]);
|
|
1801
|
+
|
|
1802
|
+
expect(mockSvc.muteCall).toHaveBeenCalledTimes(2);
|
|
1803
|
+
expect(mockSvc.unmuteCall).toHaveBeenCalledTimes(1);
|
|
1804
|
+
expect(voice.getWxAppMuted()).toBe(true);
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
it('records duration_ms on each serialized toggleMute success metric', async () => {
|
|
1808
|
+
let resolveFirstMute: () => void;
|
|
1809
|
+
const firstMuteGate = new Promise<void>((resolve) => {
|
|
1810
|
+
resolveFirstMute = resolve;
|
|
1811
|
+
});
|
|
1812
|
+
const mockSvc = {
|
|
1813
|
+
muteCall: jest.fn().mockImplementationOnce(() => firstMuteGate),
|
|
1814
|
+
unmuteCall: jest.fn().mockResolvedValue(undefined),
|
|
1815
|
+
};
|
|
1816
|
+
const taskData = createBaseData({
|
|
1817
|
+
agentId: 'agent-1',
|
|
1818
|
+
interaction: {
|
|
1819
|
+
participants: {
|
|
1820
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1821
|
+
},
|
|
1822
|
+
} as any,
|
|
1823
|
+
});
|
|
1824
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1825
|
+
enableWxBetterTogether: true,
|
|
1826
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1827
|
+
});
|
|
1828
|
+
primeConnectedState(voice, taskData);
|
|
1829
|
+
const metricsManager = MetricsManager.getInstance();
|
|
1830
|
+
const trackEventSpy = jest.spyOn(metricsManager, 'trackEvent');
|
|
1831
|
+
|
|
1832
|
+
const firstToggle = voice.toggleMute();
|
|
1833
|
+
const secondToggle = voice.toggleMute();
|
|
1834
|
+
|
|
1835
|
+
resolveFirstMute!();
|
|
1836
|
+
await Promise.all([firstToggle, secondToggle]);
|
|
1837
|
+
|
|
1838
|
+
const successCalls = trackEventSpy.mock.calls.filter(
|
|
1839
|
+
([eventName]) => eventName === METRIC_EVENT_NAMES.WXAPP_TASK_MUTE_SUCCESS
|
|
1840
|
+
);
|
|
1841
|
+
expect(successCalls).toHaveLength(2);
|
|
1842
|
+
successCalls.forEach(([, payload]) => {
|
|
1843
|
+
expect(payload).toEqual(
|
|
1844
|
+
expect.objectContaining({
|
|
1845
|
+
duration_ms: expect.any(Number),
|
|
1846
|
+
})
|
|
1847
|
+
);
|
|
1848
|
+
});
|
|
1849
|
+
});
|
|
1850
|
+
|
|
1851
|
+
it('continues queued toggleMute after predecessor failure', async () => {
|
|
1852
|
+
const mockSvc = {
|
|
1853
|
+
muteCall: jest.fn().mockRejectedValue(new Error('mute failed')),
|
|
1854
|
+
unmuteCall: jest.fn().mockResolvedValue(undefined),
|
|
1855
|
+
};
|
|
1856
|
+
const taskData = createBaseData({
|
|
1857
|
+
agentId: 'agent-1',
|
|
1858
|
+
interaction: {
|
|
1859
|
+
participants: {
|
|
1860
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1861
|
+
},
|
|
1862
|
+
} as any,
|
|
1863
|
+
});
|
|
1864
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1865
|
+
enableWxBetterTogether: true,
|
|
1866
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1867
|
+
});
|
|
1868
|
+
primeConnectedState(voice, taskData);
|
|
1869
|
+
|
|
1870
|
+
const firstToggle = voice.toggleMute({muted: true});
|
|
1871
|
+
const secondToggle = voice.toggleMute({muted: false});
|
|
1872
|
+
|
|
1873
|
+
await expect(firstToggle).rejects.toThrow('mute failed');
|
|
1874
|
+
await secondToggle;
|
|
1875
|
+
|
|
1876
|
+
expect(mockSvc.muteCall).toHaveBeenCalledTimes(1);
|
|
1877
|
+
expect(mockSvc.unmuteCall).toHaveBeenCalledTimes(1);
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1880
|
+
it('transmitDtmf() routes wxApp engaged call to telephony transmitDtmf', async () => {
|
|
1881
|
+
const mockSvc = {
|
|
1882
|
+
transmitDtmf: jest.fn().mockResolvedValue(undefined),
|
|
1883
|
+
};
|
|
1884
|
+
const taskData = createBaseData({
|
|
1885
|
+
agentId: 'agent-1',
|
|
1886
|
+
interaction: {
|
|
1887
|
+
participants: {
|
|
1888
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1889
|
+
},
|
|
1890
|
+
} as any,
|
|
1891
|
+
});
|
|
1892
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1893
|
+
enableWxBetterTogether: true,
|
|
1894
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1895
|
+
});
|
|
1896
|
+
primeConnectedState(voice, taskData);
|
|
1897
|
+
|
|
1898
|
+
await voice.transmitDtmf({dtmf: '5'});
|
|
1899
|
+
|
|
1900
|
+
expect(mockSvc.transmitDtmf).toHaveBeenCalledWith({
|
|
1901
|
+
callId: 'call-id-1',
|
|
1902
|
+
dtmf: '5',
|
|
1903
|
+
lineOwnerId: undefined,
|
|
1904
|
+
});
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
it('serializes concurrent transmitDtmf() so each digit completes before the next', async () => {
|
|
1908
|
+
let resolveFirst: () => void;
|
|
1909
|
+
const firstGate = new Promise<void>((resolve) => {
|
|
1910
|
+
resolveFirst = resolve;
|
|
1911
|
+
});
|
|
1912
|
+
const mockSvc = {
|
|
1913
|
+
transmitDtmf: jest
|
|
1914
|
+
.fn()
|
|
1915
|
+
.mockImplementationOnce(() => firstGate)
|
|
1916
|
+
.mockResolvedValueOnce(undefined),
|
|
1917
|
+
};
|
|
1918
|
+
const taskData = createBaseData({
|
|
1919
|
+
agentId: 'agent-1',
|
|
1920
|
+
interaction: {
|
|
1921
|
+
participants: {
|
|
1922
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1923
|
+
},
|
|
1924
|
+
} as any,
|
|
1925
|
+
});
|
|
1926
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1927
|
+
enableWxBetterTogether: true,
|
|
1928
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1929
|
+
});
|
|
1930
|
+
primeConnectedState(voice, taskData);
|
|
1931
|
+
|
|
1932
|
+
const firstDtmf = voice.transmitDtmf({dtmf: '1'});
|
|
1933
|
+
const secondDtmf = voice.transmitDtmf({dtmf: '2'});
|
|
1934
|
+
|
|
1935
|
+
await new Promise<void>((resolve) => {
|
|
1936
|
+
setImmediate(resolve);
|
|
1937
|
+
});
|
|
1938
|
+
|
|
1939
|
+
expect(mockSvc.transmitDtmf).toHaveBeenCalledTimes(1);
|
|
1940
|
+
expect(mockSvc.transmitDtmf).toHaveBeenCalledWith({
|
|
1941
|
+
callId: 'call-id-1',
|
|
1942
|
+
dtmf: '1',
|
|
1943
|
+
lineOwnerId: undefined,
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
resolveFirst!();
|
|
1947
|
+
await Promise.all([firstDtmf, secondDtmf]);
|
|
1948
|
+
|
|
1949
|
+
expect(mockSvc.transmitDtmf).toHaveBeenCalledTimes(2);
|
|
1950
|
+
expect(mockSvc.transmitDtmf).toHaveBeenNthCalledWith(2, {
|
|
1951
|
+
callId: 'call-id-1',
|
|
1952
|
+
dtmf: '2',
|
|
1953
|
+
lineOwnerId: undefined,
|
|
1954
|
+
});
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
it('queues three concurrent transmitDtmf() calls in order', async () => {
|
|
1958
|
+
let resolveFirst: () => void;
|
|
1959
|
+
const firstGate = new Promise<void>((resolve) => {
|
|
1960
|
+
resolveFirst = resolve;
|
|
1961
|
+
});
|
|
1962
|
+
const mockSvc = {
|
|
1963
|
+
transmitDtmf: jest
|
|
1964
|
+
.fn()
|
|
1965
|
+
.mockImplementationOnce(() => firstGate)
|
|
1966
|
+
.mockResolvedValue(undefined),
|
|
1967
|
+
};
|
|
1968
|
+
const taskData = createBaseData({
|
|
1969
|
+
agentId: 'agent-1',
|
|
1970
|
+
interaction: {
|
|
1971
|
+
participants: {
|
|
1972
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1973
|
+
},
|
|
1974
|
+
} as any,
|
|
1975
|
+
});
|
|
1976
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1977
|
+
enableWxBetterTogether: true,
|
|
1978
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1979
|
+
});
|
|
1980
|
+
primeConnectedState(voice, taskData);
|
|
1981
|
+
|
|
1982
|
+
const firstDtmf = voice.transmitDtmf({dtmf: '1'});
|
|
1983
|
+
const secondDtmf = voice.transmitDtmf({dtmf: '2'});
|
|
1984
|
+
const thirdDtmf = voice.transmitDtmf({dtmf: '3'});
|
|
1985
|
+
|
|
1986
|
+
await new Promise<void>((resolve) => {
|
|
1987
|
+
setImmediate(resolve);
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
expect(mockSvc.transmitDtmf).toHaveBeenCalledTimes(1);
|
|
1991
|
+
expect(mockSvc.transmitDtmf).toHaveBeenCalledWith({
|
|
1992
|
+
callId: 'call-id-1',
|
|
1993
|
+
dtmf: '1',
|
|
1994
|
+
lineOwnerId: undefined,
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
resolveFirst!();
|
|
1998
|
+
await Promise.all([firstDtmf, secondDtmf, thirdDtmf]);
|
|
1999
|
+
|
|
2000
|
+
expect(mockSvc.transmitDtmf).toHaveBeenCalledTimes(3);
|
|
2001
|
+
expect(mockSvc.transmitDtmf).toHaveBeenNthCalledWith(2, {
|
|
2002
|
+
callId: 'call-id-1',
|
|
2003
|
+
dtmf: '2',
|
|
2004
|
+
lineOwnerId: undefined,
|
|
2005
|
+
});
|
|
2006
|
+
expect(mockSvc.transmitDtmf).toHaveBeenNthCalledWith(3, {
|
|
2007
|
+
callId: 'call-id-1',
|
|
2008
|
+
dtmf: '3',
|
|
2009
|
+
lineOwnerId: undefined,
|
|
2010
|
+
});
|
|
2011
|
+
});
|
|
2012
|
+
|
|
2013
|
+
it('records duration_ms on each serialized transmitDtmf success metric', async () => {
|
|
2014
|
+
let resolveFirst: () => void;
|
|
2015
|
+
const firstGate = new Promise<void>((resolve) => {
|
|
2016
|
+
resolveFirst = resolve;
|
|
2017
|
+
});
|
|
2018
|
+
const mockSvc = {
|
|
2019
|
+
transmitDtmf: jest
|
|
2020
|
+
.fn()
|
|
2021
|
+
.mockImplementationOnce(() => firstGate)
|
|
2022
|
+
.mockResolvedValueOnce(undefined),
|
|
2023
|
+
};
|
|
2024
|
+
const taskData = createBaseData({
|
|
2025
|
+
agentId: 'agent-1',
|
|
2026
|
+
interaction: {
|
|
2027
|
+
participants: {
|
|
2028
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
2029
|
+
},
|
|
2030
|
+
} as any,
|
|
2031
|
+
});
|
|
2032
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
2033
|
+
enableWxBetterTogether: true,
|
|
2034
|
+
answerCallOnWebexService: mockSvc as any,
|
|
2035
|
+
});
|
|
2036
|
+
primeConnectedState(voice, taskData);
|
|
2037
|
+
const metricsManager = MetricsManager.getInstance();
|
|
2038
|
+
const trackEventSpy = jest.spyOn(metricsManager, 'trackEvent');
|
|
2039
|
+
|
|
2040
|
+
const firstDtmf = voice.transmitDtmf({dtmf: '1'});
|
|
2041
|
+
const secondDtmf = voice.transmitDtmf({dtmf: '2'});
|
|
2042
|
+
|
|
2043
|
+
resolveFirst!();
|
|
2044
|
+
await Promise.all([firstDtmf, secondDtmf]);
|
|
2045
|
+
|
|
2046
|
+
const successCalls = trackEventSpy.mock.calls.filter(
|
|
2047
|
+
([eventName]) => eventName === METRIC_EVENT_NAMES.WXAPP_TASK_DTMF_SUCCESS
|
|
2048
|
+
);
|
|
2049
|
+
expect(successCalls).toHaveLength(2);
|
|
2050
|
+
successCalls.forEach(([, payload]) => {
|
|
2051
|
+
expect(payload).toEqual(
|
|
2052
|
+
expect.objectContaining({
|
|
2053
|
+
duration_ms: expect.any(Number),
|
|
2054
|
+
})
|
|
2055
|
+
);
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
trackEventSpy.mockRestore();
|
|
2059
|
+
});
|
|
2060
|
+
});
|
|
649
2061
|
});
|