@webex/contact-center 3.12.0-next.147 → 3.12.0-next.148
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 +4 -2
- package/ai-docs/CONTRACTS.md +2 -0
- package/dist/cc.js +3 -1
- package/dist/cc.js.map +1 -1
- package/dist/constants.js +10 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/metrics/constants.js +9 -0
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/ApiAiAssistant.js +87 -10
- package/dist/services/ApiAiAssistant.js.map +1 -1
- package/dist/services/config/types.js +10 -4
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/core/types.js.map +1 -1
- package/dist/services/task/Task.js +202 -1
- package/dist/services/task/Task.js.map +1 -1
- package/dist/services/task/TaskFactory.js +4 -4
- package/dist/services/task/TaskFactory.js.map +1 -1
- package/dist/services/task/TaskManager.js +113 -26
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/TaskUtils.js +11 -1
- package/dist/services/task/TaskUtils.js.map +1 -1
- package/dist/services/task/constants.js +11 -2
- package/dist/services/task/constants.js.map +1 -1
- package/dist/services/task/digital/Digital.js +2 -2
- package/dist/services/task/digital/Digital.js.map +1 -1
- package/dist/services/task/types.js +31 -0
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +2 -2
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/services/task/voice/WebRTC.js +2 -2
- package/dist/services/task/voice/WebRTC.js.map +1 -1
- package/dist/types/constants.d.ts +9 -0
- package/dist/types/index.d.ts +4 -5
- package/dist/types/metrics/constants.d.ts +8 -0
- package/dist/types/services/ApiAiAssistant.d.ts +4 -2
- package/dist/types/services/config/types.d.ts +17 -7
- package/dist/types/services/task/Task.d.ts +15 -4
- package/dist/types/services/task/TaskFactory.d.ts +3 -2
- package/dist/types/services/task/TaskUtils.d.ts +6 -0
- package/dist/types/services/task/constants.d.ts +9 -0
- package/dist/types/services/task/digital/Digital.d.ts +3 -2
- package/dist/types/services/task/types.d.ts +109 -1
- package/dist/types/services/task/voice/Voice.d.ts +3 -2
- package/dist/types/services/task/voice/WebRTC.d.ts +3 -2
- package/dist/types/types.d.ts +10 -4
- package/dist/types.js +8 -4
- package/dist/types.js.map +1 -1
- package/dist/webex.js +1 -1
- package/package.json +2 -2
- package/src/cc.ts +4 -0
- package/src/constants.ts +12 -0
- package/src/index.ts +10 -2
- package/src/metrics/ai-docs/metrics-spec.md +12 -4
- package/src/metrics/constants.ts +10 -0
- package/src/services/ApiAiAssistant.ts +120 -12
- package/src/services/config/types.ts +10 -4
- package/src/services/core/types.ts +31 -1
- package/src/services/task/Task.ts +308 -5
- package/src/services/task/TaskFactory.ts +26 -5
- package/src/services/task/TaskManager.ts +148 -29
- package/src/services/task/TaskUtils.ts +13 -0
- package/src/services/task/ai-docs/task-spec.md +30 -5
- package/src/services/task/constants.ts +10 -0
- package/src/services/task/digital/Digital.ts +8 -3
- package/src/services/task/types.ts +139 -0
- package/src/services/task/voice/Voice.ts +8 -3
- package/src/services/task/voice/WebRTC.ts +6 -3
- package/src/types.ts +10 -4
- package/test/unit/fixtures/aiSummaryTestUtils.ts +40 -0
- package/test/unit/spec/cc.ts +1314 -32
- package/test/unit/spec/index.ts +881 -0
- package/test/unit/spec/services/ApiAiAssistant.ts +122 -6
- package/test/unit/spec/services/core/WebexRequest.ts +5 -6
- package/test/unit/spec/services/task/Task.ts +1988 -10
- package/test/unit/spec/services/task/TaskFactory.ts +17 -0
- package/test/unit/spec/services/task/TaskManager.ts +2327 -180
- package/test/unit/spec/services/task/TaskUtils.ts +72 -0
- package/test/unit/spec/services/task/voice/Voice.ts +353 -18
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -14,9 +14,14 @@ import {
|
|
|
14
14
|
isSecondaryEpDnAgent,
|
|
15
15
|
getConsultMediaResourceId,
|
|
16
16
|
getIsConsultInProgressForConferenceControls,
|
|
17
|
+
getIsConsultedAgentForControls,
|
|
18
|
+
getServerHoldStateForControls,
|
|
19
|
+
isCampaignPreviewTask,
|
|
20
|
+
isCampaignPreviewReservation,
|
|
17
21
|
} from '../../../../../src/services/task/TaskUtils';
|
|
18
22
|
import {ITask, Interaction, TaskData} from '../../../../../src/services/task/types';
|
|
19
23
|
import {LoginOption} from '../../../../../src/types';
|
|
24
|
+
import {CC_EVENTS} from '../../../../../src/services/config/types';
|
|
20
25
|
|
|
21
26
|
describe('TaskUtils', () => {
|
|
22
27
|
let mockTask: ITask;
|
|
@@ -47,6 +52,73 @@ describe('TaskUtils', () => {
|
|
|
47
52
|
} as any;
|
|
48
53
|
});
|
|
49
54
|
|
|
55
|
+
describe('state-control helper branches', () => {
|
|
56
|
+
it('should derive consulted-agent and hold states from task context', () => {
|
|
57
|
+
expect(
|
|
58
|
+
getIsConsultedAgentForControls({isConsulted: true} as TaskData, {} as any, false)
|
|
59
|
+
).toBe(true);
|
|
60
|
+
expect(
|
|
61
|
+
getIsConsultedAgentForControls(
|
|
62
|
+
{} as TaskData,
|
|
63
|
+
{consultInitiator: false} as any,
|
|
64
|
+
true
|
|
65
|
+
)
|
|
66
|
+
).toBe(true);
|
|
67
|
+
expect(
|
|
68
|
+
getIsConsultedAgentForControls(
|
|
69
|
+
{} as TaskData,
|
|
70
|
+
{consultInitiator: true} as any,
|
|
71
|
+
true
|
|
72
|
+
)
|
|
73
|
+
).toBe(false);
|
|
74
|
+
|
|
75
|
+
const taskData = {
|
|
76
|
+
interaction: {
|
|
77
|
+
media: {
|
|
78
|
+
main: {isHold: true},
|
|
79
|
+
fallback: {isHold: false},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
mediaResourceId: 'fallback',
|
|
83
|
+
} as TaskData;
|
|
84
|
+
|
|
85
|
+
expect(getServerHoldStateForControls({taskData} as any, 'main')).toBe(true);
|
|
86
|
+
expect(getServerHoldStateForControls({taskData} as any)).toBe(false);
|
|
87
|
+
expect(getServerHoldStateForControls({} as any)).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should detect campaign preview tasks and reservations', () => {
|
|
91
|
+
expect(
|
|
92
|
+
isCampaignPreviewTask({
|
|
93
|
+
interaction: {
|
|
94
|
+
outboundType: 'STANDARD_PREVIEW_CAMPAIGN',
|
|
95
|
+
callProcessingDetails: {},
|
|
96
|
+
},
|
|
97
|
+
} as TaskData)
|
|
98
|
+
).toBe(true);
|
|
99
|
+
expect(
|
|
100
|
+
isCampaignPreviewTask({
|
|
101
|
+
interaction: {
|
|
102
|
+
outboundType: 'OUTDIAL',
|
|
103
|
+
callProcessingDetails: {campaignType: 'preview_standard'},
|
|
104
|
+
},
|
|
105
|
+
} as unknown as TaskData)
|
|
106
|
+
).toBe(true);
|
|
107
|
+
expect(isCampaignPreviewTask({interaction: {callProcessingDetails: {}}} as TaskData)).toBe(
|
|
108
|
+
false
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(
|
|
112
|
+
isCampaignPreviewReservation({
|
|
113
|
+
data: {type: CC_EVENTS.AGENT_OFFER_CAMPAIGN_RESERVATION},
|
|
114
|
+
} as ITask)
|
|
115
|
+
).toBe(true);
|
|
116
|
+
expect(isCampaignPreviewReservation({data: {type: CC_EVENTS.AGENT_CONTACT}} as ITask)).toBe(
|
|
117
|
+
false
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
50
122
|
describe('isPrimary', () => {
|
|
51
123
|
it('should return true when agent is the owner', () => {
|
|
52
124
|
expect(isPrimary(mockTask, mockAgentId)).toBe(true);
|
|
@@ -3,6 +3,7 @@ import LoggerProxy from '../../../../../../src/logger-proxy';
|
|
|
3
3
|
import {
|
|
4
4
|
TaskData,
|
|
5
5
|
CONSULT_TRANSFER_DESTINATION_TYPE,
|
|
6
|
+
DESTINATION_TYPE,
|
|
6
7
|
TASK_EVENTS,
|
|
7
8
|
VOICE_VARIANT,
|
|
8
9
|
} from '../../../../../../src/services/task/types';
|
|
@@ -11,6 +12,10 @@ import {TaskEvent, TaskState} from '../../../../../../src/services/task/state-ma
|
|
|
11
12
|
import {computeUIControls} from '../../../../../../src/services/task/state-machine/uiControlsComputer';
|
|
12
13
|
import * as Utils from '../../../../../../src/services/core/Utils';
|
|
13
14
|
import {createTaskData} from '../taskTestUtils';
|
|
15
|
+
import {AIAssistantEventName} from '../../../../../../src/types';
|
|
16
|
+
import {AI_SUMMARY_ERROR_CODES} from '../../../../../../src/constants';
|
|
17
|
+
import {AI_SUMMARY_REQUEST_CANCELLED} from '../../../../../../src/services/task/constants';
|
|
18
|
+
import {createAISummaryError, flushMicrotasks} from '../../../../fixtures/aiSummaryTestUtils';
|
|
14
19
|
import MetricsManager from '../../../../../../src/metrics/MetricsManager';
|
|
15
20
|
import {METRIC_EVENT_NAMES} from '../../../../../../src/metrics/constants';
|
|
16
21
|
import * as wxAppDiagnosticLogging from '../../../../../../src/services/wxAppDiagnosticLogging';
|
|
@@ -104,9 +109,7 @@ describe('Voice Task', () => {
|
|
|
104
109
|
reason: 'CUSTOMER_BUSY',
|
|
105
110
|
});
|
|
106
111
|
|
|
107
|
-
const outdialFailedCall = emitSpy.mock.calls.find(
|
|
108
|
-
(call) => call[0] === 'task:outdialFailed'
|
|
109
|
-
);
|
|
112
|
+
const outdialFailedCall = emitSpy.mock.calls.find((call) => call[0] === 'task:outdialFailed');
|
|
110
113
|
expect(outdialFailedCall).toBeDefined();
|
|
111
114
|
expect(outdialFailedCall![1]).toBe('CUSTOMER_BUSY');
|
|
112
115
|
});
|
|
@@ -127,9 +130,7 @@ describe('Voice Task', () => {
|
|
|
127
130
|
reason: 'AGENT_ENDS',
|
|
128
131
|
});
|
|
129
132
|
|
|
130
|
-
const outdialFailedCall = emitSpy.mock.calls.find(
|
|
131
|
-
(call) => call[0] === 'task:outdialFailed'
|
|
132
|
-
);
|
|
133
|
+
const outdialFailedCall = emitSpy.mock.calls.find((call) => call[0] === 'task:outdialFailed');
|
|
133
134
|
expect(outdialFailedCall).toBeUndefined();
|
|
134
135
|
});
|
|
135
136
|
});
|
|
@@ -211,7 +212,10 @@ describe('Voice Task', () => {
|
|
|
211
212
|
}) as any;
|
|
212
213
|
const voice = new Voice(dummyContact, heldConferenceData, {});
|
|
213
214
|
primeConnectedState(voice, heldConferenceData);
|
|
214
|
-
voice.stateMachineService?.send({
|
|
215
|
+
voice.stateMachineService?.send({
|
|
216
|
+
type: TaskEvent.CONFERENCE_START,
|
|
217
|
+
taskData: heldConferenceData,
|
|
218
|
+
});
|
|
215
219
|
expect(voice.stateMachineService?.getSnapshot().value).toBe(TaskState.CONFERENCING);
|
|
216
220
|
|
|
217
221
|
await voice.holdResume();
|
|
@@ -598,6 +602,341 @@ describe('Voice Task', () => {
|
|
|
598
602
|
});
|
|
599
603
|
});
|
|
600
604
|
|
|
605
|
+
describe('AI summary noninterference for call controls', () => {
|
|
606
|
+
const summaryAgentId = 'summary-agent-1';
|
|
607
|
+
const wrapupPayload = {wrapUpReason: 'Customer resolved', auxCodeId: 'wrapup-code-1'} as const;
|
|
608
|
+
const consultPayload = {
|
|
609
|
+
to: 'consult-agent-1',
|
|
610
|
+
destinationType: DESTINATION_TYPE.AGENT,
|
|
611
|
+
holdParticipants: true,
|
|
612
|
+
} as const;
|
|
613
|
+
const transferPayload = {
|
|
614
|
+
to: 'transfer-agent-1',
|
|
615
|
+
destinationType: DESTINATION_TYPE.AGENT,
|
|
616
|
+
} as const;
|
|
617
|
+
const callControlMethods = [
|
|
618
|
+
'wrapup',
|
|
619
|
+
'consult',
|
|
620
|
+
'blindTransfer',
|
|
621
|
+
'vteamTransfer',
|
|
622
|
+
'consultTransfer',
|
|
623
|
+
] as const;
|
|
624
|
+
type CallControlMethod = (typeof callControlMethods)[number];
|
|
625
|
+
type CallControlCase = {
|
|
626
|
+
label: string;
|
|
627
|
+
createOperationResult: () => unknown;
|
|
628
|
+
expectedContactCalls: Record<CallControlMethod, unknown[][]>;
|
|
629
|
+
prime: (voice: Voice, taskData: TaskData) => void;
|
|
630
|
+
invoke: (voice: Voice) => Promise<unknown>;
|
|
631
|
+
requestSummary: (voice: Voice) => Promise<unknown>;
|
|
632
|
+
summaryEventName: AIAssistantEventName;
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const createContactMock = (operationResult: any) =>
|
|
636
|
+
({
|
|
637
|
+
hold: jest.fn().mockResolvedValue('held'),
|
|
638
|
+
unHold: jest.fn().mockResolvedValue('resumed'),
|
|
639
|
+
pauseRecording: jest.fn().mockResolvedValue('paused'),
|
|
640
|
+
resumeRecording: jest.fn().mockResolvedValue('resumedRecording'),
|
|
641
|
+
wrapup: jest.fn().mockResolvedValue(operationResult),
|
|
642
|
+
consult: jest.fn().mockResolvedValue(operationResult),
|
|
643
|
+
blindTransfer: jest.fn().mockResolvedValue(operationResult),
|
|
644
|
+
vteamTransfer: jest.fn().mockResolvedValue(operationResult),
|
|
645
|
+
consultTransfer: jest.fn().mockResolvedValue(operationResult),
|
|
646
|
+
} as any);
|
|
647
|
+
|
|
648
|
+
const createSummaryAdapter = () => {
|
|
649
|
+
let rejectPending: ((error: Error) => void) | undefined;
|
|
650
|
+
const adapter: any = {
|
|
651
|
+
sendEvent: jest.fn().mockResolvedValue(undefined),
|
|
652
|
+
requestAndWaitForRtd: jest.fn(async (options: any) => {
|
|
653
|
+
const result = new Promise((_resolve, reject) => {
|
|
654
|
+
rejectPending = reject;
|
|
655
|
+
});
|
|
656
|
+
await adapter.sendEvent(
|
|
657
|
+
options.agentId,
|
|
658
|
+
options.interactionId,
|
|
659
|
+
options.eventType,
|
|
660
|
+
options.eventName,
|
|
661
|
+
options.eventMetaData,
|
|
662
|
+
undefined,
|
|
663
|
+
undefined,
|
|
664
|
+
Date.now(),
|
|
665
|
+
options.timeout
|
|
666
|
+
);
|
|
667
|
+
return result;
|
|
668
|
+
}),
|
|
669
|
+
clear: jest.fn(() =>
|
|
670
|
+
rejectPending?.(createAISummaryError(AI_SUMMARY_REQUEST_CANCELLED))
|
|
671
|
+
),
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
return adapter;
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const injectAISummaryDependencies = (
|
|
678
|
+
voice: Voice,
|
|
679
|
+
adapter = createSummaryAdapter(),
|
|
680
|
+
featureEnablement = {
|
|
681
|
+
interactionId: 'int1',
|
|
682
|
+
postCallEnabled: true,
|
|
683
|
+
midCallEnabled: true,
|
|
684
|
+
}
|
|
685
|
+
) => {
|
|
686
|
+
adapter.aiFeature = {
|
|
687
|
+
id: 'summary-flags',
|
|
688
|
+
generatedSummaries: {
|
|
689
|
+
wrapUpSummariesEnabled: true,
|
|
690
|
+
consultTransferSummariesEnabled: true,
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
(voice as any).apiAIAssistant = adapter;
|
|
694
|
+
voice.setFeatureEnablement(featureEnablement);
|
|
695
|
+
|
|
696
|
+
return {adapter, coordinator: adapter};
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
const normalizeEmitCalls = (emitSpy: jest.SpyInstance, voice: Voice) =>
|
|
700
|
+
emitSpy.mock.calls.map(([eventName, ...args]) => [
|
|
701
|
+
eventName,
|
|
702
|
+
...args.map((arg) => (arg === voice ? 'VOICE_SELF' : arg)),
|
|
703
|
+
]);
|
|
704
|
+
|
|
705
|
+
const getContactCalls = (contact: any) =>
|
|
706
|
+
callControlMethods.reduce((calls, method) => {
|
|
707
|
+
calls[method] = contact[method].mock.calls;
|
|
708
|
+
|
|
709
|
+
return calls;
|
|
710
|
+
}, {} as Record<CallControlMethod, unknown[][]>);
|
|
711
|
+
|
|
712
|
+
const createVoiceHarness = (testCase: CallControlCase) => {
|
|
713
|
+
const taskData = createBaseData();
|
|
714
|
+
const operationResult = testCase.createOperationResult();
|
|
715
|
+
const contact = createContactMock(operationResult);
|
|
716
|
+
const voice = new Voice(
|
|
717
|
+
contact,
|
|
718
|
+
taskData,
|
|
719
|
+
{isEndTaskEnabled: true, isEndConsultEnabled: true},
|
|
720
|
+
undefined,
|
|
721
|
+
summaryAgentId
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
testCase.prime(voice, taskData);
|
|
725
|
+
const emitSpy = jest.spyOn(voice, 'emit');
|
|
726
|
+
|
|
727
|
+
return {contact, emitSpy, operationResult, taskData, voice};
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
const captureCallControlOutcome = (
|
|
731
|
+
harness: ReturnType<typeof createVoiceHarness>,
|
|
732
|
+
result: unknown
|
|
733
|
+
) => {
|
|
734
|
+
const snapshot = harness.voice.stateMachineService?.getSnapshot();
|
|
735
|
+
|
|
736
|
+
return {
|
|
737
|
+
contactCalls: getContactCalls(harness.contact),
|
|
738
|
+
emitCalls: normalizeEmitCalls(harness.emitSpy, harness.voice),
|
|
739
|
+
result,
|
|
740
|
+
state: snapshot?.value,
|
|
741
|
+
context: snapshot?.context,
|
|
742
|
+
uiControls: harness.voice.uiControls,
|
|
743
|
+
};
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
const runCallControl = async (testCase: CallControlCase) => {
|
|
747
|
+
const harness = createVoiceHarness(testCase);
|
|
748
|
+
const result = await testCase.invoke(harness.voice);
|
|
749
|
+
|
|
750
|
+
expect(result).toBe(harness.operationResult);
|
|
751
|
+
|
|
752
|
+
return captureCallControlOutcome(harness, result);
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
const callControlCases: CallControlCase[] = [
|
|
756
|
+
{
|
|
757
|
+
label: 'wrapup',
|
|
758
|
+
createOperationResult: () => ({
|
|
759
|
+
trackingId: 'wrapup-tracking-1',
|
|
760
|
+
data: {wrappedUp: true},
|
|
761
|
+
}),
|
|
762
|
+
expectedContactCalls: {
|
|
763
|
+
wrapup: [[{interactionId: 'int1', data: wrapupPayload}]],
|
|
764
|
+
consult: [],
|
|
765
|
+
blindTransfer: [],
|
|
766
|
+
vteamTransfer: [],
|
|
767
|
+
consultTransfer: [],
|
|
768
|
+
},
|
|
769
|
+
prime: primeConnectedState,
|
|
770
|
+
invoke: (voice: Voice) => voice.wrapup({...wrapupPayload}),
|
|
771
|
+
requestSummary: (voice: Voice) => voice.requestPostCallSummary(),
|
|
772
|
+
summaryEventName: AIAssistantEventName.GET_POST_CALL_SUMMARY,
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
label: 'consult',
|
|
776
|
+
createOperationResult: () => ({
|
|
777
|
+
trackingId: 'consult-tracking-1',
|
|
778
|
+
data: createBaseData({
|
|
779
|
+
destinationType: DESTINATION_TYPE.AGENT,
|
|
780
|
+
destAgentId: 'consult-agent-1',
|
|
781
|
+
interaction: {state: 'consulting'} as any,
|
|
782
|
+
}),
|
|
783
|
+
}),
|
|
784
|
+
expectedContactCalls: {
|
|
785
|
+
wrapup: [],
|
|
786
|
+
consult: [[{interactionId: 'int1', data: consultPayload}]],
|
|
787
|
+
blindTransfer: [],
|
|
788
|
+
vteamTransfer: [],
|
|
789
|
+
consultTransfer: [],
|
|
790
|
+
},
|
|
791
|
+
prime: primeConnectedState,
|
|
792
|
+
invoke: (voice: Voice) => voice.consult({...consultPayload}),
|
|
793
|
+
requestSummary: (voice: Voice) => voice.requestMidCallSummary('CONSULT'),
|
|
794
|
+
summaryEventName: AIAssistantEventName.GET_MID_CALL_CONSULT_SUMMARY,
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
label: 'transfer',
|
|
798
|
+
createOperationResult: () => ({
|
|
799
|
+
trackingId: 'transfer-tracking-1',
|
|
800
|
+
data: {transferred: true},
|
|
801
|
+
}),
|
|
802
|
+
expectedContactCalls: {
|
|
803
|
+
wrapup: [],
|
|
804
|
+
consult: [],
|
|
805
|
+
blindTransfer: [[{interactionId: 'int1', data: transferPayload}]],
|
|
806
|
+
vteamTransfer: [],
|
|
807
|
+
consultTransfer: [],
|
|
808
|
+
},
|
|
809
|
+
prime: primeConnectedState,
|
|
810
|
+
invoke: (voice: Voice) => voice.transfer({...transferPayload}),
|
|
811
|
+
requestSummary: (voice: Voice) => voice.requestMidCallSummary('TRANSFER'),
|
|
812
|
+
summaryEventName: AIAssistantEventName.GET_MID_CALL_TRANSFER_SUMMARY,
|
|
813
|
+
},
|
|
814
|
+
];
|
|
815
|
+
|
|
816
|
+
const expectSummaryGetRequest = (
|
|
817
|
+
adapter: ReturnType<typeof createSummaryAdapter>,
|
|
818
|
+
summaryEventName: AIAssistantEventName
|
|
819
|
+
) => {
|
|
820
|
+
expect(adapter.sendEvent).toHaveBeenCalledTimes(1);
|
|
821
|
+
expect(adapter.sendEvent).toHaveBeenCalledWith(
|
|
822
|
+
summaryAgentId,
|
|
823
|
+
'int1',
|
|
824
|
+
'CTI_EVENT',
|
|
825
|
+
summaryEventName,
|
|
826
|
+
expect.objectContaining({
|
|
827
|
+
conversationId: 'int1',
|
|
828
|
+
clientType: 'WxCC',
|
|
829
|
+
}),
|
|
830
|
+
undefined,
|
|
831
|
+
undefined,
|
|
832
|
+
expect.any(Number),
|
|
833
|
+
15000
|
|
834
|
+
);
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
const expectCancelledSummary = async (summaryRequest: Promise<unknown>) => {
|
|
838
|
+
await expect(summaryRequest).rejects.toMatchObject({
|
|
839
|
+
message: AI_SUMMARY_REQUEST_CANCELLED,
|
|
840
|
+
data: {errorCode: AI_SUMMARY_REQUEST_CANCELLED},
|
|
841
|
+
});
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
const summaryActivityCases = [
|
|
845
|
+
{
|
|
846
|
+
label: 'while the matching AI summary request is pending',
|
|
847
|
+
prepare: async (harness: VoiceHarnessWithCase) => {
|
|
848
|
+
const {adapter, coordinator} = injectAISummaryDependencies(harness.voice);
|
|
849
|
+
const summaryRequest = harness.testCase.requestSummary(harness.voice);
|
|
850
|
+
const summaryObserver = jest.fn();
|
|
851
|
+
|
|
852
|
+
summaryRequest.then(summaryObserver, summaryObserver);
|
|
853
|
+
await flushMicrotasks();
|
|
854
|
+
|
|
855
|
+
expectSummaryGetRequest(adapter, harness.testCase.summaryEventName);
|
|
856
|
+
expect(summaryObserver).not.toHaveBeenCalled();
|
|
857
|
+
|
|
858
|
+
return async () => {
|
|
859
|
+
expect(summaryObserver).not.toHaveBeenCalled();
|
|
860
|
+
coordinator.clear('int1', 'int1');
|
|
861
|
+
await expectCancelledSummary(summaryRequest);
|
|
862
|
+
};
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
label: 'after coordinator owner cancellation',
|
|
867
|
+
prepare: async (harness: VoiceHarnessWithCase) => {
|
|
868
|
+
const {adapter, coordinator} = injectAISummaryDependencies(harness.voice);
|
|
869
|
+
const summaryRequest = harness.testCase.requestSummary(harness.voice);
|
|
870
|
+
|
|
871
|
+
await flushMicrotasks();
|
|
872
|
+
expectSummaryGetRequest(adapter, harness.testCase.summaryEventName);
|
|
873
|
+
|
|
874
|
+
coordinator.clear('int1', 'int1');
|
|
875
|
+
await expectCancelledSummary(summaryRequest);
|
|
876
|
+
|
|
877
|
+
return async () => undefined;
|
|
878
|
+
},
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
label: 'after adapter rejection',
|
|
882
|
+
prepare: async (harness: VoiceHarnessWithCase) => {
|
|
883
|
+
const adapter = createSummaryAdapter();
|
|
884
|
+
const adapterError = createAISummaryError(
|
|
885
|
+
AI_SUMMARY_ERROR_CODES.AI_ASSISTANT_BASE_URL_NOT_AVAILABLE
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
adapter.sendEvent.mockRejectedValueOnce(adapterError);
|
|
889
|
+
injectAISummaryDependencies(harness.voice, adapter);
|
|
890
|
+
|
|
891
|
+
await expect(harness.testCase.requestSummary(harness.voice)).rejects.toBe(adapterError);
|
|
892
|
+
expectSummaryGetRequest(adapter, harness.testCase.summaryEventName);
|
|
893
|
+
|
|
894
|
+
return async () => undefined;
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
];
|
|
898
|
+
|
|
899
|
+
type VoiceHarnessWithCase = ReturnType<typeof createVoiceHarness> & {
|
|
900
|
+
testCase: CallControlCase;
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
const createVoiceHarnessWithCase = (testCase: CallControlCase): VoiceHarnessWithCase => ({
|
|
904
|
+
...createVoiceHarness(testCase),
|
|
905
|
+
testCase,
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
beforeEach(() => {
|
|
909
|
+
jest.useFakeTimers();
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
afterEach(() => {
|
|
913
|
+
jest.clearAllTimers();
|
|
914
|
+
jest.useRealTimers();
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
describe.each(summaryActivityCases)('$label', (summaryActivity) => {
|
|
918
|
+
it.each(callControlCases)(
|
|
919
|
+
'$label preserves contact arguments, resolved value, and task events',
|
|
920
|
+
async (testCase) => {
|
|
921
|
+
const baseline = await runCallControl(testCase);
|
|
922
|
+
const harness = createVoiceHarnessWithCase(testCase);
|
|
923
|
+
const finalizeSummaryActivity = await summaryActivity.prepare(harness);
|
|
924
|
+
|
|
925
|
+
try {
|
|
926
|
+
const result = await testCase.invoke(harness.voice);
|
|
927
|
+
const observed = captureCallControlOutcome(harness, result);
|
|
928
|
+
|
|
929
|
+
expect(result).toBe(harness.operationResult);
|
|
930
|
+
expect(observed.contactCalls).toEqual(testCase.expectedContactCalls);
|
|
931
|
+
expect(observed).toEqual(baseline);
|
|
932
|
+
} finally {
|
|
933
|
+
await finalizeSummaryActivity();
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
);
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
|
|
601
940
|
describe('exitConference()', () => {
|
|
602
941
|
const conferenceTaskData = () =>
|
|
603
942
|
createBaseData({
|
|
@@ -951,13 +1290,7 @@ describe('Voice Task', () => {
|
|
|
951
1290
|
['number', 503],
|
|
952
1291
|
])('records failure telemetry and preserves a %s rejection', async (_label, failure) => {
|
|
953
1292
|
dummyContact.dropConferenceParticipant.mockRejectedValueOnce(failure);
|
|
954
|
-
const voice = new Voice(
|
|
955
|
-
dummyContact,
|
|
956
|
-
createBaseData(),
|
|
957
|
-
{},
|
|
958
|
-
undefined,
|
|
959
|
-
'current-agent-id'
|
|
960
|
-
);
|
|
1293
|
+
const voice = new Voice(dummyContact, createBaseData(), {}, undefined, 'current-agent-id');
|
|
961
1294
|
const metricsManager = MetricsManager.getInstance();
|
|
962
1295
|
const failureFieldsSpy = jest.spyOn(
|
|
963
1296
|
MetricsManager,
|
|
@@ -967,9 +1300,9 @@ describe('Voice Task', () => {
|
|
|
967
1300
|
const trackEventSpy = jest.spyOn(metricsManager, 'trackEvent').mockImplementation();
|
|
968
1301
|
const errorSpy = jest.spyOn(LoggerProxy, 'error').mockImplementation();
|
|
969
1302
|
|
|
970
|
-
await expect(
|
|
971
|
-
|
|
972
|
-
)
|
|
1303
|
+
await expect(voice.dropConferenceParticipant({participantId: 'participant-id'})).rejects.toBe(
|
|
1304
|
+
failure
|
|
1305
|
+
);
|
|
973
1306
|
|
|
974
1307
|
expect(failureFieldsSpy).toHaveBeenCalledWith({});
|
|
975
1308
|
expect(trackEventSpy).toHaveBeenCalledWith(
|
|
@@ -1057,7 +1390,9 @@ describe('Voice Task', () => {
|
|
|
1057
1390
|
|
|
1058
1391
|
voice.applyWxAppMuteStateFromSync('prefix:call-id-1', true);
|
|
1059
1392
|
|
|
1060
|
-
expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {
|
|
1393
|
+
expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {
|
|
1394
|
+
muted: true,
|
|
1395
|
+
});
|
|
1061
1396
|
});
|
|
1062
1397
|
|
|
1063
1398
|
it('no-ops when callId does not match active call', () => {
|