@webex/contact-center 3.12.0-llmrefactor.2 → 3.12.0-llmrefactor.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.sdd/manifest.json +5 -4
- package/ai-docs/ARCHITECTURE.md +1 -1
- package/ai-docs/CONTRACTS.md +5 -1
- 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 +440 -10
- 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/behavioral-events.js +13 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +2 -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 +174 -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 +171 -0
- package/dist/services/WebexCrossClientService.js.map +1 -0
- package/dist/services/WxAppTelephonyMercurySync.js +93 -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 +176 -20
- 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 +135 -37
- 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 +38 -11
- 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 +350 -14
- 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 +201 -0
- package/dist/services/task/voice/wxAppVoiceMethods.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 +77 -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/constants.d.ts +2 -0
- package/dist/types/services/AddressBook.d.ts +2 -1
- package/dist/types/services/AnswerCallOnWebexService.d.ts +37 -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 +28 -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 +260 -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 +48 -9
- package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
- package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +52 -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 +1 -1
- package/src/cc.ts +506 -9
- package/src/config.ts +7 -0
- package/src/constants.ts +8 -0
- package/src/index.ts +2 -0
- package/src/metrics/ai-docs/metrics-spec.md +9 -3
- package/src/metrics/behavioral-events.ts +14 -0
- package/src/metrics/constants.ts +2 -0
- package/src/services/AddressBook.ts +17 -6
- package/src/services/AnswerCallOnWebexService.ts +206 -0
- package/src/services/EntryPoint.ts +59 -60
- package/src/services/Queue.ts +29 -12
- package/src/services/WebexCrossClientService.ts +212 -0
- package/src/services/WxAppTelephonyMercurySync.ts +115 -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 +254 -15
- 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 +117 -2
- 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 +210 -63
- package/src/services/task/state-machine/actions.ts +5 -2
- package/src/services/task/state-machine/ai-docs/AGENTS.md +8 -4
- package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +17 -8
- package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +46 -22
- package/src/services/task/state-machine/guards.ts +64 -11
- 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 +462 -12
- package/src/services/task/voice/WebRTC.ts +5 -1
- package/src/services/task/voice/wxAppVoiceMethods.ts +307 -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 +1274 -0
- package/test/unit/spec/metrics/behavioral-events.ts +18 -0
- package/test/unit/spec/services/AddressBook.ts +37 -6
- package/test/unit/spec/services/AnswerCallOnWebexService.ts +223 -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 +261 -0
- package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +113 -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 +200 -0
- package/test/unit/spec/services/task/TaskFactory.ts +39 -2
- package/test/unit/spec/services/task/TaskManager.ts +653 -1
- 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 +854 -72
- 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 +874 -0
- package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +459 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -1,13 +1,17 @@
|
|
|
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,
|
|
5
7
|
} from '../../../../../../src/services/task/types';
|
|
6
8
|
import {CC_EVENTS} from '../../../../../../src/services/config/types';
|
|
7
9
|
import {TaskEvent, TaskState} from '../../../../../../src/services/task/state-machine';
|
|
8
10
|
import {computeUIControls} from '../../../../../../src/services/task/state-machine/uiControlsComputer';
|
|
9
11
|
import * as Utils from '../../../../../../src/services/core/Utils';
|
|
10
12
|
import {createTaskData} from '../taskTestUtils';
|
|
13
|
+
import MetricsManager from '../../../../../../src/metrics/MetricsManager';
|
|
14
|
+
import {METRIC_EVENT_NAMES} from '../../../../../../src/metrics/constants';
|
|
11
15
|
|
|
12
16
|
jest.mock('../../../../../../src/services/core/WebexRequest', () => ({
|
|
13
17
|
__esModule: true,
|
|
@@ -33,7 +37,13 @@ const dummyContact = {
|
|
|
33
37
|
resumeRecording: jest.fn().mockResolvedValue('resumedRecording'),
|
|
34
38
|
consult: jest.fn().mockResolvedValue('consulted'),
|
|
35
39
|
consultConference: jest.fn().mockResolvedValue('conferenceStarted'),
|
|
40
|
+
dropConferenceParticipant: jest.fn().mockResolvedValue({
|
|
41
|
+
type: 'RoutingMessage',
|
|
42
|
+
trackingId: 'drop-tracking-id',
|
|
43
|
+
data: {},
|
|
44
|
+
}),
|
|
36
45
|
consultTransfer: jest.fn().mockResolvedValue('consultTransferred'),
|
|
46
|
+
cancelTask: jest.fn().mockResolvedValue(undefined),
|
|
37
47
|
} as any;
|
|
38
48
|
|
|
39
49
|
const createBaseData = (overrides: Partial<TaskData> = {}): TaskData =>
|
|
@@ -94,6 +104,28 @@ describe('Voice Task', () => {
|
|
|
94
104
|
expect(outdialFailedCall).toBeDefined();
|
|
95
105
|
expect(outdialFailedCall![1]).toBe('CUSTOMER_BUSY');
|
|
96
106
|
});
|
|
107
|
+
|
|
108
|
+
it('does not emit task:outdialFailed when suppressOutdialFailedPopup is set', () => {
|
|
109
|
+
const taskData = createBaseData({
|
|
110
|
+
interaction: {
|
|
111
|
+
outboundType: 'OUTDIAL',
|
|
112
|
+
} as any,
|
|
113
|
+
suppressOutdialFailedPopup: true,
|
|
114
|
+
});
|
|
115
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
116
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
117
|
+
|
|
118
|
+
voice.sendStateMachineEvent({
|
|
119
|
+
type: TaskEvent.OUTBOUND_FAILED,
|
|
120
|
+
taskData,
|
|
121
|
+
reason: 'AGENT_ENDS',
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const outdialFailedCall = emitSpy.mock.calls.find(
|
|
125
|
+
(call) => call[0] === 'task:outdialFailed'
|
|
126
|
+
);
|
|
127
|
+
expect(outdialFailedCall).toBeUndefined();
|
|
128
|
+
});
|
|
97
129
|
});
|
|
98
130
|
|
|
99
131
|
it('hides end and endConsult when disabled', () => {
|
|
@@ -646,4 +678,846 @@ describe('Voice Task', () => {
|
|
|
646
678
|
});
|
|
647
679
|
});
|
|
648
680
|
});
|
|
681
|
+
|
|
682
|
+
describe('dropConferenceParticipant()', () => {
|
|
683
|
+
afterEach(() => {
|
|
684
|
+
jest.restoreAllMocks();
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
it.each([
|
|
688
|
+
['missing payload', undefined],
|
|
689
|
+
['missing participantId', {}],
|
|
690
|
+
['non-string participantId', {participantId: 42}],
|
|
691
|
+
['blank participantId', {participantId: ' '}],
|
|
692
|
+
])('rejects %s before registering a request', async (_label, payload) => {
|
|
693
|
+
const taskData = createBaseData();
|
|
694
|
+
const voice = new Voice(dummyContact, taskData, {});
|
|
695
|
+
const timeEventSpy = jest.spyOn(MetricsManager.getInstance(), 'timeEvent');
|
|
696
|
+
|
|
697
|
+
await expect(voice.dropConferenceParticipant(payload as any)).rejects.toThrow(
|
|
698
|
+
'participantId must be a non-empty string'
|
|
699
|
+
);
|
|
700
|
+
|
|
701
|
+
expect(dummyContact.dropConferenceParticipant).not.toHaveBeenCalled();
|
|
702
|
+
expect(timeEventSpy).not.toHaveBeenCalled();
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
it('uses the latest main interaction ID and records PII-safe success telemetry', async () => {
|
|
706
|
+
const participantId = '+1/participant-secret';
|
|
707
|
+
const taskData = createBaseData({
|
|
708
|
+
interaction: {mainInteractionId: 'main-interaction-id'} as any,
|
|
709
|
+
});
|
|
710
|
+
const voice = new Voice(dummyContact, taskData, {}, undefined, 'current-agent-id');
|
|
711
|
+
const metricsManager = MetricsManager.getInstance();
|
|
712
|
+
const timeEventSpy = jest.spyOn(metricsManager, 'timeEvent').mockImplementation();
|
|
713
|
+
const trackEventSpy = jest.spyOn(metricsManager, 'trackEvent').mockImplementation();
|
|
714
|
+
const infoSpy = jest.spyOn(LoggerProxy, 'info').mockImplementation();
|
|
715
|
+
const logSpy = jest.spyOn(LoggerProxy, 'log').mockImplementation();
|
|
716
|
+
|
|
717
|
+
await voice.dropConferenceParticipant({participantId});
|
|
718
|
+
|
|
719
|
+
expect(dummyContact.dropConferenceParticipant).toHaveBeenCalledWith({
|
|
720
|
+
interactionId: 'main-interaction-id',
|
|
721
|
+
participantId,
|
|
722
|
+
});
|
|
723
|
+
expect(timeEventSpy).toHaveBeenCalledWith([
|
|
724
|
+
METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_SUCCESS,
|
|
725
|
+
METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_FAILED,
|
|
726
|
+
]);
|
|
727
|
+
expect(trackEventSpy).toHaveBeenCalledWith(
|
|
728
|
+
METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_SUCCESS,
|
|
729
|
+
expect.objectContaining({
|
|
730
|
+
taskId: 'int1',
|
|
731
|
+
requestInteractionId: 'main-interaction-id',
|
|
732
|
+
agentId: 'current-agent-id',
|
|
733
|
+
}),
|
|
734
|
+
['operational', 'behavioral', 'business']
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
const telemetryAndLogs = JSON.stringify([
|
|
738
|
+
timeEventSpy.mock.calls,
|
|
739
|
+
trackEventSpy.mock.calls,
|
|
740
|
+
infoSpy.mock.calls,
|
|
741
|
+
logSpy.mock.calls,
|
|
742
|
+
]);
|
|
743
|
+
expect(telemetryAndLogs).not.toContain(participantId);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it('records PII-safe failure telemetry and rethrows the AQM error', async () => {
|
|
747
|
+
const participantId = '+1/failure-secret';
|
|
748
|
+
const failure = {
|
|
749
|
+
details: {
|
|
750
|
+
type: 'ParticipantDropConferenceFailed',
|
|
751
|
+
trackingId: 'failure-tracking-id',
|
|
752
|
+
data: {reason: participantId},
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
dummyContact.dropConferenceParticipant.mockRejectedValueOnce(failure);
|
|
756
|
+
const taskData = createBaseData({
|
|
757
|
+
interaction: {mainInteractionId: 'main-interaction-id'} as any,
|
|
758
|
+
});
|
|
759
|
+
const voice = new Voice(dummyContact, taskData, {}, undefined, 'current-agent-id');
|
|
760
|
+
const metricsManager = MetricsManager.getInstance();
|
|
761
|
+
jest.spyOn(metricsManager, 'timeEvent').mockImplementation();
|
|
762
|
+
const trackEventSpy = jest.spyOn(metricsManager, 'trackEvent').mockImplementation();
|
|
763
|
+
const errorSpy = jest.spyOn(LoggerProxy, 'error').mockImplementation();
|
|
764
|
+
|
|
765
|
+
await expect(voice.dropConferenceParticipant({participantId})).rejects.toBe(failure);
|
|
766
|
+
|
|
767
|
+
expect(trackEventSpy).toHaveBeenCalledWith(
|
|
768
|
+
METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_FAILED,
|
|
769
|
+
expect.objectContaining({
|
|
770
|
+
taskId: 'int1',
|
|
771
|
+
requestInteractionId: 'main-interaction-id',
|
|
772
|
+
agentId: 'current-agent-id',
|
|
773
|
+
}),
|
|
774
|
+
['operational', 'behavioral', 'business']
|
|
775
|
+
);
|
|
776
|
+
expect(errorSpy).toHaveBeenCalledWith('Failed to drop conference participant', {
|
|
777
|
+
module: 'cc',
|
|
778
|
+
method: 'dropConferenceParticipant',
|
|
779
|
+
trackingId: 'failure-tracking-id',
|
|
780
|
+
interactionId: 'main-interaction-id',
|
|
781
|
+
});
|
|
782
|
+
expect(JSON.stringify([trackEventSpy.mock.calls, errorSpy.mock.calls])).not.toContain(
|
|
783
|
+
participantId
|
|
784
|
+
);
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
it.each([
|
|
788
|
+
['null', null],
|
|
789
|
+
['undefined', undefined],
|
|
790
|
+
['string', 'primitive failure'],
|
|
791
|
+
['number', 503],
|
|
792
|
+
])('records failure telemetry and preserves a %s rejection', async (_label, failure) => {
|
|
793
|
+
dummyContact.dropConferenceParticipant.mockRejectedValueOnce(failure);
|
|
794
|
+
const voice = new Voice(
|
|
795
|
+
dummyContact,
|
|
796
|
+
createBaseData(),
|
|
797
|
+
{},
|
|
798
|
+
undefined,
|
|
799
|
+
'current-agent-id'
|
|
800
|
+
);
|
|
801
|
+
const metricsManager = MetricsManager.getInstance();
|
|
802
|
+
const failureFieldsSpy = jest.spyOn(
|
|
803
|
+
MetricsManager,
|
|
804
|
+
'getCommonTrackingFieldForAQMResponseFailed'
|
|
805
|
+
);
|
|
806
|
+
jest.spyOn(metricsManager, 'timeEvent').mockImplementation();
|
|
807
|
+
const trackEventSpy = jest.spyOn(metricsManager, 'trackEvent').mockImplementation();
|
|
808
|
+
const errorSpy = jest.spyOn(LoggerProxy, 'error').mockImplementation();
|
|
809
|
+
|
|
810
|
+
await expect(
|
|
811
|
+
voice.dropConferenceParticipant({participantId: 'participant-id'})
|
|
812
|
+
).rejects.toBe(failure);
|
|
813
|
+
|
|
814
|
+
expect(failureFieldsSpy).toHaveBeenCalledWith({});
|
|
815
|
+
expect(trackEventSpy).toHaveBeenCalledWith(
|
|
816
|
+
METRIC_EVENT_NAMES.TASK_CONFERENCE_PARTICIPANT_DROP_FAILED,
|
|
817
|
+
expect.objectContaining({
|
|
818
|
+
taskId: 'int1',
|
|
819
|
+
requestInteractionId: 'int1',
|
|
820
|
+
agentId: 'current-agent-id',
|
|
821
|
+
}),
|
|
822
|
+
['operational', 'behavioral', 'business']
|
|
823
|
+
);
|
|
824
|
+
expect(errorSpy).toHaveBeenCalledWith('Failed to drop conference participant', {
|
|
825
|
+
module: 'cc',
|
|
826
|
+
method: 'dropConferenceParticipant',
|
|
827
|
+
trackingId: undefined,
|
|
828
|
+
interactionId: 'int1',
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
describe('setEnableWxBetterTogether', () => {
|
|
834
|
+
it('updates runtime flag, uiControlConfig, and emits ui control updates', () => {
|
|
835
|
+
const voice = new Voice(dummyContact, createBaseData(), {enableWxBetterTogether: true});
|
|
836
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
837
|
+
|
|
838
|
+
voice.setEnableWxBetterTogether(false);
|
|
839
|
+
|
|
840
|
+
expect(voice['enableWxBetterTogether']).toBe(false);
|
|
841
|
+
expect(voice['uiControlConfig'].enableWxBetterTogether).toBe(false);
|
|
842
|
+
expect(emitSpy).toHaveBeenCalledWith(
|
|
843
|
+
TASK_EVENTS.TASK_UI_CONTROLS_UPDATED,
|
|
844
|
+
expect.any(Object)
|
|
845
|
+
);
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
describe('applyWxAppMuteStateFromSync', () => {
|
|
850
|
+
const wxAppParticipant = {
|
|
851
|
+
deviceType: 'wxApp',
|
|
852
|
+
deviceId: 'device-id-1',
|
|
853
|
+
deviceCallId: 'call-id-1',
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
const makeWxAppTaskData = () =>
|
|
857
|
+
createBaseData({
|
|
858
|
+
agentId: 'agent-1',
|
|
859
|
+
interaction: {
|
|
860
|
+
participants: {
|
|
861
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
862
|
+
},
|
|
863
|
+
} as any,
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it('emits TASK_WXAPP_MUTE_STATE_UPDATED when callId matches via endsWith', () => {
|
|
867
|
+
const taskData = makeWxAppTaskData();
|
|
868
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
869
|
+
primeConnectedState(voice, taskData);
|
|
870
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
871
|
+
|
|
872
|
+
voice.applyWxAppMuteStateFromSync('prefix:call-id-1', true);
|
|
873
|
+
|
|
874
|
+
expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {muted: true});
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it('no-ops when callId does not match active call', () => {
|
|
878
|
+
const taskData = makeWxAppTaskData();
|
|
879
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
880
|
+
primeConnectedState(voice, taskData);
|
|
881
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
882
|
+
|
|
883
|
+
voice.applyWxAppMuteStateFromSync('other-call-id', true);
|
|
884
|
+
|
|
885
|
+
expect(emitSpy).not.toHaveBeenCalledWith(
|
|
886
|
+
TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED,
|
|
887
|
+
expect.anything()
|
|
888
|
+
);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('no-ops when enableWxBetterTogether is false', () => {
|
|
892
|
+
const taskData = makeWxAppTaskData();
|
|
893
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: false});
|
|
894
|
+
primeConnectedState(voice, taskData);
|
|
895
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
896
|
+
|
|
897
|
+
voice.applyWxAppMuteStateFromSync('prefix:call-id-1', true);
|
|
898
|
+
|
|
899
|
+
expect(emitSpy).not.toHaveBeenCalledWith(
|
|
900
|
+
TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED,
|
|
901
|
+
expect.anything()
|
|
902
|
+
);
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it('no-ops when muted state is unchanged', () => {
|
|
906
|
+
const taskData = makeWxAppTaskData();
|
|
907
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
908
|
+
primeConnectedState(voice, taskData);
|
|
909
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
910
|
+
|
|
911
|
+
voice.applyWxAppMuteStateFromSync('prefix:call-id-1', true);
|
|
912
|
+
emitSpy.mockClear();
|
|
913
|
+
|
|
914
|
+
voice.applyWxAppMuteStateFromSync('prefix:call-id-1', true);
|
|
915
|
+
|
|
916
|
+
expect(emitSpy).not.toHaveBeenCalledWith(
|
|
917
|
+
TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED,
|
|
918
|
+
expect.anything()
|
|
919
|
+
);
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
describe('syncWxAppMuteFromCallDetails', () => {
|
|
924
|
+
const wxAppParticipant = {
|
|
925
|
+
deviceType: 'wxApp',
|
|
926
|
+
deviceId: 'device-id-1',
|
|
927
|
+
deviceCallId: 'call-id-1',
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const makeWxAppTaskData = () =>
|
|
931
|
+
createBaseData({
|
|
932
|
+
agentId: 'agent-1',
|
|
933
|
+
interaction: {
|
|
934
|
+
participants: {
|
|
935
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
936
|
+
},
|
|
937
|
+
} as any,
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
it('seeds mute state from telephony GET call details on engaged wxApp call', async () => {
|
|
941
|
+
const mockSvc = {
|
|
942
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: true}),
|
|
943
|
+
};
|
|
944
|
+
const taskData = makeWxAppTaskData();
|
|
945
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
946
|
+
enableWxBetterTogether: true,
|
|
947
|
+
answerCallOnWebexService: mockSvc as any,
|
|
948
|
+
});
|
|
949
|
+
primeConnectedState(voice, taskData);
|
|
950
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
951
|
+
|
|
952
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
953
|
+
|
|
954
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledWith({callId: 'call-id-1'});
|
|
955
|
+
expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {
|
|
956
|
+
muted: true,
|
|
957
|
+
});
|
|
958
|
+
expect(voice.getWxAppMuted()).toBe(true);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it('returns resolved mute state from syncWxAppMuteFromCallDetails', async () => {
|
|
962
|
+
const mockSvc = {
|
|
963
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: true}),
|
|
964
|
+
};
|
|
965
|
+
const taskData = makeWxAppTaskData();
|
|
966
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
967
|
+
enableWxBetterTogether: true,
|
|
968
|
+
answerCallOnWebexService: mockSvc as any,
|
|
969
|
+
});
|
|
970
|
+
primeConnectedState(voice, taskData);
|
|
971
|
+
|
|
972
|
+
const result = await voice.syncWxAppMuteFromCallDetails();
|
|
973
|
+
|
|
974
|
+
expect(result).toBe(true);
|
|
975
|
+
expect(voice.getWxAppMuted()).toBe(true);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it('seeds mute via participant deviceCallId when state machine is still IDLE', async () => {
|
|
979
|
+
const mockSvc = {
|
|
980
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: true}),
|
|
981
|
+
};
|
|
982
|
+
const taskData = makeWxAppTaskData();
|
|
983
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
984
|
+
enableWxBetterTogether: true,
|
|
985
|
+
answerCallOnWebexService: mockSvc as any,
|
|
986
|
+
});
|
|
987
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
988
|
+
|
|
989
|
+
const result = await voice.syncWxAppMuteFromCallDetails();
|
|
990
|
+
|
|
991
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledWith({callId: 'call-id-1'});
|
|
992
|
+
expect(result).toBe(true);
|
|
993
|
+
expect(voice.getWxAppMuted()).toBe(true);
|
|
994
|
+
expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {
|
|
995
|
+
muted: true,
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
it('no-ops when enableWxBetterTogether is false', async () => {
|
|
1000
|
+
const mockSvc = {getCallDetails: jest.fn()};
|
|
1001
|
+
const taskData = makeWxAppTaskData();
|
|
1002
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1003
|
+
enableWxBetterTogether: false,
|
|
1004
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1005
|
+
});
|
|
1006
|
+
primeConnectedState(voice, taskData);
|
|
1007
|
+
|
|
1008
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1009
|
+
|
|
1010
|
+
expect(mockSvc.getCallDetails).not.toHaveBeenCalled();
|
|
1011
|
+
});
|
|
1012
|
+
|
|
1013
|
+
it('syncs mute state after task assignment once CONNECTED (onTaskAssigned)', async () => {
|
|
1014
|
+
const mockSvc = {
|
|
1015
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: true}),
|
|
1016
|
+
};
|
|
1017
|
+
const taskData = makeWxAppTaskData();
|
|
1018
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1019
|
+
enableWxBetterTogether: true,
|
|
1020
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1024
|
+
expect(voice.getWxAppMuted()).toBe(false);
|
|
1025
|
+
voice.stateMachineService?.send({type: TaskEvent.ASSIGN, taskData});
|
|
1026
|
+
|
|
1027
|
+
await new Promise<void>((resolve) => {
|
|
1028
|
+
setImmediate(resolve);
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledWith({callId: 'call-id-1'});
|
|
1032
|
+
expect(voice.getWxAppMuted()).toBe(true);
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
it('seeds mute on ASSIGN when wxApp offer was accepted on another session', async () => {
|
|
1036
|
+
const mockSvc = {
|
|
1037
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: false}),
|
|
1038
|
+
};
|
|
1039
|
+
const taskData = makeWxAppTaskData();
|
|
1040
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1041
|
+
enableWxBetterTogether: true,
|
|
1042
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1046
|
+
expect(voice['uiControlConfig']?.wxAppAnswerPending).toBeFalsy();
|
|
1047
|
+
voice.stateMachineService?.send({type: TaskEvent.ASSIGN, taskData});
|
|
1048
|
+
|
|
1049
|
+
await new Promise<void>((resolve) => {
|
|
1050
|
+
setImmediate(resolve);
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledWith({callId: 'call-id-1'});
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
it('syncs mute state after task hydrate (onTaskHydrated)', async () => {
|
|
1057
|
+
const mockSvc = {
|
|
1058
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: true}),
|
|
1059
|
+
};
|
|
1060
|
+
const taskData = makeWxAppTaskData();
|
|
1061
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1062
|
+
enableWxBetterTogether: true,
|
|
1063
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1064
|
+
});
|
|
1065
|
+
primeConnectedState(voice, taskData);
|
|
1066
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
1067
|
+
|
|
1068
|
+
voice.onTaskHydrated();
|
|
1069
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1070
|
+
|
|
1071
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledWith({callId: 'call-id-1'});
|
|
1072
|
+
expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {
|
|
1073
|
+
muted: true,
|
|
1074
|
+
});
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
it('preserves wxAppAnswerPending on hydrate while OFFERED and answer is pending', async () => {
|
|
1078
|
+
const mockSvc = {
|
|
1079
|
+
answerCall: jest.fn().mockResolvedValue(undefined),
|
|
1080
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: false}),
|
|
1081
|
+
};
|
|
1082
|
+
const taskData = makeWxAppTaskData();
|
|
1083
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1084
|
+
enableWxBetterTogether: true,
|
|
1085
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1086
|
+
});
|
|
1087
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1088
|
+
|
|
1089
|
+
await voice.accept();
|
|
1090
|
+
expect(voice.uiControlConfig.wxAppAnswerPending).toBe(true);
|
|
1091
|
+
|
|
1092
|
+
voice.onTaskHydrated();
|
|
1093
|
+
|
|
1094
|
+
expect(voice.uiControlConfig.wxAppAnswerPending).toBe(true);
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
it('clears wxAppAnswerPending on hydrate when task is no longer OFFERED', async () => {
|
|
1098
|
+
const mockSvc = {
|
|
1099
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: false}),
|
|
1100
|
+
};
|
|
1101
|
+
const taskData = makeWxAppTaskData();
|
|
1102
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1103
|
+
enableWxBetterTogether: true,
|
|
1104
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1105
|
+
});
|
|
1106
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1107
|
+
voice.uiControlConfig = {...voice.uiControlConfig, wxAppAnswerPending: true};
|
|
1108
|
+
primeConnectedState(voice, taskData);
|
|
1109
|
+
|
|
1110
|
+
voice.onTaskHydrated();
|
|
1111
|
+
|
|
1112
|
+
expect(voice.uiControlConfig.wxAppAnswerPending).toBe(false);
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
it('coalesces parallel syncWxAppMuteFromCallDetails into one getCallDetails', async () => {
|
|
1116
|
+
let resolveGetCallDetails: (value: {muted: boolean}) => void;
|
|
1117
|
+
const getCallDetailsPromise = new Promise<{muted: boolean}>((resolve) => {
|
|
1118
|
+
resolveGetCallDetails = resolve;
|
|
1119
|
+
});
|
|
1120
|
+
const mockSvc = {
|
|
1121
|
+
getCallDetails: jest.fn().mockReturnValue(getCallDetailsPromise),
|
|
1122
|
+
};
|
|
1123
|
+
const taskData = makeWxAppTaskData();
|
|
1124
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1125
|
+
enableWxBetterTogether: true,
|
|
1126
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1127
|
+
});
|
|
1128
|
+
primeConnectedState(voice, taskData);
|
|
1129
|
+
|
|
1130
|
+
const sync1 = voice.syncWxAppMuteFromCallDetails();
|
|
1131
|
+
const sync2 = voice.syncWxAppMuteFromCallDetails();
|
|
1132
|
+
|
|
1133
|
+
resolveGetCallDetails!({muted: true});
|
|
1134
|
+
const [result1, result2] = await Promise.all([sync1, sync2]);
|
|
1135
|
+
|
|
1136
|
+
expect(result1).toBe(true);
|
|
1137
|
+
expect(result2).toBe(true);
|
|
1138
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledTimes(1);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
it('issues a new getCallDetails on sequential syncWxAppMuteFromCallDetails after the first completes', async () => {
|
|
1142
|
+
const mockSvc = {
|
|
1143
|
+
getCallDetails: jest
|
|
1144
|
+
.fn()
|
|
1145
|
+
.mockResolvedValueOnce({muted: false})
|
|
1146
|
+
.mockResolvedValueOnce({muted: true}),
|
|
1147
|
+
};
|
|
1148
|
+
const taskData = makeWxAppTaskData();
|
|
1149
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1150
|
+
enableWxBetterTogether: true,
|
|
1151
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1152
|
+
});
|
|
1153
|
+
primeConnectedState(voice, taskData);
|
|
1154
|
+
|
|
1155
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1156
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1157
|
+
|
|
1158
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledTimes(2);
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
it('skips mute GET for terminated wxApp outdial with deviceCallId', async () => {
|
|
1162
|
+
const mockSvc = {getCallDetails: jest.fn()};
|
|
1163
|
+
const taskData = createBaseData({
|
|
1164
|
+
agentId: 'agent-1',
|
|
1165
|
+
interaction: {
|
|
1166
|
+
outboundType: 'OUTDIAL',
|
|
1167
|
+
isTerminated: true,
|
|
1168
|
+
participants: {
|
|
1169
|
+
'agent-1': {
|
|
1170
|
+
id: 'agent-1',
|
|
1171
|
+
deviceType: 'wxApp',
|
|
1172
|
+
deviceId: 'device-id-1',
|
|
1173
|
+
deviceCallId: 'callhalf-dead',
|
|
1174
|
+
},
|
|
1175
|
+
},
|
|
1176
|
+
} as any,
|
|
1177
|
+
});
|
|
1178
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1179
|
+
enableWxBetterTogether: true,
|
|
1180
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1181
|
+
});
|
|
1182
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1183
|
+
|
|
1184
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1185
|
+
|
|
1186
|
+
expect(mockSvc.getCallDetails).not.toHaveBeenCalled();
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
it('skips mute GET for pre-accept wxApp OFFERED offer', async () => {
|
|
1190
|
+
const mockSvc = {getCallDetails: jest.fn()};
|
|
1191
|
+
const wxAppParticipant = {
|
|
1192
|
+
deviceType: 'wxApp',
|
|
1193
|
+
deviceId: 'device-id-1',
|
|
1194
|
+
deviceCallId: 'call-id-1',
|
|
1195
|
+
};
|
|
1196
|
+
const taskData = createBaseData({
|
|
1197
|
+
agentId: 'agent-1',
|
|
1198
|
+
interaction: {
|
|
1199
|
+
outboundType: 'OUTDIAL',
|
|
1200
|
+
participants: {
|
|
1201
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1202
|
+
},
|
|
1203
|
+
} as any,
|
|
1204
|
+
});
|
|
1205
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1206
|
+
enableWxBetterTogether: true,
|
|
1207
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1208
|
+
});
|
|
1209
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1210
|
+
|
|
1211
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1212
|
+
|
|
1213
|
+
expect(mockSvc.getCallDetails).not.toHaveBeenCalled();
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
it('still calls mute GET for post-accept OFFERED wxApp offer', async () => {
|
|
1217
|
+
const mockSvc = {
|
|
1218
|
+
answerCall: jest.fn().mockResolvedValue(undefined),
|
|
1219
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: false}),
|
|
1220
|
+
};
|
|
1221
|
+
const wxAppParticipant = {
|
|
1222
|
+
deviceType: 'wxApp',
|
|
1223
|
+
deviceId: 'device-id-1',
|
|
1224
|
+
deviceCallId: 'call-id-1',
|
|
1225
|
+
};
|
|
1226
|
+
const taskData = createBaseData({
|
|
1227
|
+
agentId: 'agent-1',
|
|
1228
|
+
interaction: {
|
|
1229
|
+
outboundType: 'OUTDIAL',
|
|
1230
|
+
participants: {
|
|
1231
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1232
|
+
},
|
|
1233
|
+
} as any,
|
|
1234
|
+
});
|
|
1235
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1236
|
+
enableWxBetterTogether: true,
|
|
1237
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1238
|
+
});
|
|
1239
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1240
|
+
await voice.accept();
|
|
1241
|
+
|
|
1242
|
+
expect(voice.uiControlConfig.wxAppAcceptInFlight).toBe(false);
|
|
1243
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1244
|
+
|
|
1245
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalledWith({callId: 'call-id-1'});
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
it('does not log error for expected wxApp call-not-found on mute sync', async () => {
|
|
1249
|
+
const errorSpy = jest.spyOn(LoggerProxy, 'error');
|
|
1250
|
+
const mockSvc = {
|
|
1251
|
+
getCallDetails: jest.fn().mockRejectedValue({status: 400, message: 'Call not found'}),
|
|
1252
|
+
};
|
|
1253
|
+
const wxAppParticipant = {
|
|
1254
|
+
deviceType: 'wxApp',
|
|
1255
|
+
deviceId: 'device-id-1',
|
|
1256
|
+
deviceCallId: 'call-id-1',
|
|
1257
|
+
};
|
|
1258
|
+
const taskData = createBaseData({
|
|
1259
|
+
agentId: 'agent-1',
|
|
1260
|
+
interaction: {
|
|
1261
|
+
participants: {
|
|
1262
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1263
|
+
},
|
|
1264
|
+
} as any,
|
|
1265
|
+
});
|
|
1266
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1267
|
+
enableWxBetterTogether: true,
|
|
1268
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1269
|
+
});
|
|
1270
|
+
primeConnectedState(voice, taskData);
|
|
1271
|
+
|
|
1272
|
+
await voice.syncWxAppMuteFromCallDetails();
|
|
1273
|
+
|
|
1274
|
+
expect(mockSvc.getCallDetails).toHaveBeenCalled();
|
|
1275
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
1276
|
+
errorSpy.mockRestore();
|
|
1277
|
+
});
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
describe('decline', () => {
|
|
1281
|
+
const wxAppParticipant = {
|
|
1282
|
+
deviceType: 'wxApp',
|
|
1283
|
+
deviceId: 'device-id-1',
|
|
1284
|
+
deviceCallId: 'call-id-1',
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
const makeWxAppOutdialTaskData = () =>
|
|
1288
|
+
createBaseData({
|
|
1289
|
+
agentId: 'agent-1',
|
|
1290
|
+
interaction: {
|
|
1291
|
+
outboundType: 'OUTDIAL',
|
|
1292
|
+
participants: {
|
|
1293
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1294
|
+
},
|
|
1295
|
+
} as any,
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
it('cancels wxApp outdial via contact.cancelTask', async () => {
|
|
1299
|
+
const taskData = makeWxAppOutdialTaskData();
|
|
1300
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
1301
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1302
|
+
|
|
1303
|
+
await voice.decline();
|
|
1304
|
+
|
|
1305
|
+
expect(dummyContact.cancelTask).toHaveBeenCalledWith({interactionId: 'int1'});
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
it('clears wxAppAnswerPending when declining outdial', async () => {
|
|
1309
|
+
const mockSvc = {
|
|
1310
|
+
answerCall: jest.fn().mockResolvedValue(undefined),
|
|
1311
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: false}),
|
|
1312
|
+
};
|
|
1313
|
+
const taskData = makeWxAppOutdialTaskData();
|
|
1314
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1315
|
+
enableWxBetterTogether: true,
|
|
1316
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1317
|
+
});
|
|
1318
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1319
|
+
|
|
1320
|
+
await voice.accept();
|
|
1321
|
+
expect(voice.uiControlConfig.wxAppAnswerPending).toBe(true);
|
|
1322
|
+
expect(voice.uiControlConfig.wxAppAcceptInFlight).toBe(false);
|
|
1323
|
+
|
|
1324
|
+
await voice.decline();
|
|
1325
|
+
|
|
1326
|
+
expect(voice.uiControlConfig.wxAppAnswerPending).toBe(false);
|
|
1327
|
+
expect(dummyContact.cancelTask).toHaveBeenCalledWith({interactionId: 'int1'});
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
it('throws unsupported for non-outdial voice tasks', async () => {
|
|
1331
|
+
const taskData = createBaseData();
|
|
1332
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: true});
|
|
1333
|
+
|
|
1334
|
+
await expect(voice.decline()).rejects.toThrow('Unsupported operation: decline');
|
|
1335
|
+
expect(dummyContact.cancelTask).not.toHaveBeenCalled();
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
it('throws unsupported when enableWxBetterTogether is false', async () => {
|
|
1339
|
+
const taskData = makeWxAppOutdialTaskData();
|
|
1340
|
+
const voice = new Voice(dummyContact, taskData, {enableWxBetterTogether: false});
|
|
1341
|
+
|
|
1342
|
+
await expect(voice.decline()).rejects.toThrow('Unsupported operation: decline');
|
|
1343
|
+
expect(dummyContact.cancelTask).not.toHaveBeenCalled();
|
|
1344
|
+
});
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
describe('unified task API routing (WXCC-6026)', () => {
|
|
1348
|
+
const wxAppParticipant = {
|
|
1349
|
+
deviceType: 'wxApp',
|
|
1350
|
+
deviceId: 'device-id-1',
|
|
1351
|
+
deviceCallId: 'call-id-1',
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
it('accept() routes wxApp inbound offer to telephony answerCall', async () => {
|
|
1355
|
+
const mockSvc = {
|
|
1356
|
+
answerCall: jest.fn().mockResolvedValue(undefined),
|
|
1357
|
+
getCallDetails: jest.fn().mockResolvedValue({muted: false}),
|
|
1358
|
+
};
|
|
1359
|
+
const taskData = createBaseData({
|
|
1360
|
+
agentId: 'agent-1',
|
|
1361
|
+
interaction: {
|
|
1362
|
+
participants: {
|
|
1363
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1364
|
+
},
|
|
1365
|
+
} as any,
|
|
1366
|
+
});
|
|
1367
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1368
|
+
enableWxBetterTogether: true,
|
|
1369
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1370
|
+
});
|
|
1371
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1372
|
+
|
|
1373
|
+
await voice.accept();
|
|
1374
|
+
|
|
1375
|
+
expect(mockSvc.answerCall).toHaveBeenCalledWith({
|
|
1376
|
+
callId: 'call-id-1',
|
|
1377
|
+
endpointId: 'device-id-1',
|
|
1378
|
+
lineOwnerId: undefined,
|
|
1379
|
+
});
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
it('decline() routes wxApp inbound offer to telephony rejectCall', async () => {
|
|
1383
|
+
const mockSvc = {
|
|
1384
|
+
rejectCall: jest.fn().mockResolvedValue(undefined),
|
|
1385
|
+
};
|
|
1386
|
+
const taskData = createBaseData({
|
|
1387
|
+
agentId: 'agent-1',
|
|
1388
|
+
interaction: {
|
|
1389
|
+
participants: {
|
|
1390
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1391
|
+
},
|
|
1392
|
+
} as any,
|
|
1393
|
+
});
|
|
1394
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1395
|
+
enableWxBetterTogether: true,
|
|
1396
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1397
|
+
});
|
|
1398
|
+
voice.stateMachineService?.send({type: TaskEvent.TASK_INCOMING, taskData});
|
|
1399
|
+
|
|
1400
|
+
await voice.decline();
|
|
1401
|
+
|
|
1402
|
+
expect(mockSvc.rejectCall).toHaveBeenCalledWith({
|
|
1403
|
+
callId: 'call-id-1',
|
|
1404
|
+
lineOwnerId: undefined,
|
|
1405
|
+
});
|
|
1406
|
+
expect(dummyContact.cancelTask).not.toHaveBeenCalled();
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
it('toggleMute() routes wxApp engaged call to telephony muteCall', async () => {
|
|
1410
|
+
const mockSvc = {
|
|
1411
|
+
muteCall: jest.fn().mockResolvedValue(undefined),
|
|
1412
|
+
};
|
|
1413
|
+
const taskData = createBaseData({
|
|
1414
|
+
agentId: 'agent-1',
|
|
1415
|
+
interaction: {
|
|
1416
|
+
participants: {
|
|
1417
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1418
|
+
},
|
|
1419
|
+
} as any,
|
|
1420
|
+
});
|
|
1421
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1422
|
+
enableWxBetterTogether: true,
|
|
1423
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1424
|
+
});
|
|
1425
|
+
primeConnectedState(voice, taskData);
|
|
1426
|
+
|
|
1427
|
+
await voice.toggleMute({muted: true});
|
|
1428
|
+
|
|
1429
|
+
expect(mockSvc.muteCall).toHaveBeenCalledWith({callId: 'call-id-1', lineOwnerId: undefined});
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
it('serializes concurrent no-arg toggleMute() so mute then unmute', async () => {
|
|
1433
|
+
let resolveMute: () => void;
|
|
1434
|
+
const muteGate = new Promise<void>((resolve) => {
|
|
1435
|
+
resolveMute = resolve;
|
|
1436
|
+
});
|
|
1437
|
+
const mockSvc = {
|
|
1438
|
+
muteCall: jest.fn().mockImplementation(() => muteGate),
|
|
1439
|
+
unmuteCall: jest.fn().mockResolvedValue(undefined),
|
|
1440
|
+
};
|
|
1441
|
+
const taskData = createBaseData({
|
|
1442
|
+
agentId: 'agent-1',
|
|
1443
|
+
interaction: {
|
|
1444
|
+
participants: {
|
|
1445
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1446
|
+
},
|
|
1447
|
+
} as any,
|
|
1448
|
+
});
|
|
1449
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1450
|
+
enableWxBetterTogether: true,
|
|
1451
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1452
|
+
});
|
|
1453
|
+
primeConnectedState(voice, taskData);
|
|
1454
|
+
expect(voice.getWxAppMuted()).toBe(false);
|
|
1455
|
+
|
|
1456
|
+
const firstToggle = voice.toggleMute();
|
|
1457
|
+
const secondToggle = voice.toggleMute();
|
|
1458
|
+
|
|
1459
|
+
resolveMute!();
|
|
1460
|
+
await Promise.all([firstToggle, secondToggle]);
|
|
1461
|
+
|
|
1462
|
+
expect(mockSvc.muteCall).toHaveBeenCalledTimes(1);
|
|
1463
|
+
expect(mockSvc.unmuteCall).toHaveBeenCalledTimes(1);
|
|
1464
|
+
expect(voice.getWxAppMuted()).toBe(false);
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
it('continues queued toggleMute after predecessor failure', async () => {
|
|
1468
|
+
const mockSvc = {
|
|
1469
|
+
muteCall: jest.fn().mockRejectedValue(new Error('mute failed')),
|
|
1470
|
+
unmuteCall: jest.fn().mockResolvedValue(undefined),
|
|
1471
|
+
};
|
|
1472
|
+
const taskData = createBaseData({
|
|
1473
|
+
agentId: 'agent-1',
|
|
1474
|
+
interaction: {
|
|
1475
|
+
participants: {
|
|
1476
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1477
|
+
},
|
|
1478
|
+
} as any,
|
|
1479
|
+
});
|
|
1480
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1481
|
+
enableWxBetterTogether: true,
|
|
1482
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1483
|
+
});
|
|
1484
|
+
primeConnectedState(voice, taskData);
|
|
1485
|
+
|
|
1486
|
+
const firstToggle = voice.toggleMute({muted: true});
|
|
1487
|
+
const secondToggle = voice.toggleMute({muted: false});
|
|
1488
|
+
|
|
1489
|
+
await expect(firstToggle).rejects.toThrow('mute failed');
|
|
1490
|
+
await secondToggle;
|
|
1491
|
+
|
|
1492
|
+
expect(mockSvc.muteCall).toHaveBeenCalledTimes(1);
|
|
1493
|
+
expect(mockSvc.unmuteCall).toHaveBeenCalledTimes(1);
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
it('transmitDtmf() routes wxApp engaged call to telephony transmitDtmf', async () => {
|
|
1497
|
+
const mockSvc = {
|
|
1498
|
+
transmitDtmf: jest.fn().mockResolvedValue(undefined),
|
|
1499
|
+
};
|
|
1500
|
+
const taskData = createBaseData({
|
|
1501
|
+
agentId: 'agent-1',
|
|
1502
|
+
interaction: {
|
|
1503
|
+
participants: {
|
|
1504
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
1505
|
+
},
|
|
1506
|
+
} as any,
|
|
1507
|
+
});
|
|
1508
|
+
const voice = new Voice(dummyContact, taskData, {
|
|
1509
|
+
enableWxBetterTogether: true,
|
|
1510
|
+
answerCallOnWebexService: mockSvc as any,
|
|
1511
|
+
});
|
|
1512
|
+
primeConnectedState(voice, taskData);
|
|
1513
|
+
|
|
1514
|
+
await voice.transmitDtmf({dtmf: '5'});
|
|
1515
|
+
|
|
1516
|
+
expect(mockSvc.transmitDtmf).toHaveBeenCalledWith({
|
|
1517
|
+
callId: 'call-id-1',
|
|
1518
|
+
dtmf: '5',
|
|
1519
|
+
lineOwnerId: undefined,
|
|
1520
|
+
});
|
|
1521
|
+
});
|
|
1522
|
+
});
|
|
649
1523
|
});
|