@webex/contact-center 3.12.0-next.147 → 3.12.0-next.149
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 +3 -3
- 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
|
@@ -6,14 +6,44 @@ import {
|
|
|
6
6
|
TASK_CHANNEL_TYPE,
|
|
7
7
|
TransferPayLoad,
|
|
8
8
|
VOICE_VARIANT,
|
|
9
|
+
AISummaryAction,
|
|
10
|
+
AISummaryResponse,
|
|
9
11
|
} from '../../../../../src/services/task/types';
|
|
10
12
|
import {TaskEvent} from '../../../../../src/services/task/state-machine';
|
|
11
13
|
import {ENTRY_POINT_TRANSFER_DESTINATION_TYPE} from '../../../../../src/services/task/constants';
|
|
12
14
|
import LoggerProxy from '../../../../../src/logger-proxy';
|
|
13
15
|
import {createTaskData} from './taskTestUtils';
|
|
16
|
+
import {AIAssistantEventName} from '../../../../../src/types';
|
|
17
|
+
import {AI_SUMMARY_ERROR_CODES} from '../../../../../src/constants';
|
|
18
|
+
import {METRIC_EVENT_NAMES} from '../../../../../src/metrics/constants';
|
|
19
|
+
import {
|
|
20
|
+
AI_SUMMARY_DURATION_MS,
|
|
21
|
+
AI_SUMMARY_REQUEST_CANCELLED,
|
|
22
|
+
METHODS,
|
|
23
|
+
} from '../../../../../src/services/task/constants';
|
|
24
|
+
import {
|
|
25
|
+
createAISummaryError,
|
|
26
|
+
createAISummaryErrorExpectation,
|
|
27
|
+
createDeferred,
|
|
28
|
+
flushEventLoopTurn,
|
|
29
|
+
} from '../../../fixtures/aiSummaryTestUtils';
|
|
30
|
+
import type ApiAIAssistant from '../../../../../src/services/ApiAiAssistant';
|
|
31
|
+
|
|
32
|
+
const AI_SUMMARY_TRANSPORT_ERROR_CODES = {
|
|
33
|
+
VALIDATION_FAILED: 'AI_SUMMARY_TRANSPORT_VALIDATION_FAILED',
|
|
34
|
+
HTTP_REQUEST_FAILED: 'AI_SUMMARY_HTTP_REQUEST_FAILED',
|
|
35
|
+
TIMEOUT: 'AI_SUMMARY_TRANSPORT_TIMEOUT',
|
|
36
|
+
} as const;
|
|
14
37
|
|
|
15
38
|
class DummyTask extends Task {
|
|
16
|
-
constructor(
|
|
39
|
+
constructor(
|
|
40
|
+
contact: any,
|
|
41
|
+
data: TaskData,
|
|
42
|
+
wrapupData?: any,
|
|
43
|
+
agentId = 'agent-1',
|
|
44
|
+
agentName = 'Receiving Agent',
|
|
45
|
+
apiAIAssistant?: ApiAIAssistant
|
|
46
|
+
) {
|
|
17
47
|
super(
|
|
18
48
|
contact,
|
|
19
49
|
data,
|
|
@@ -23,7 +53,9 @@ class DummyTask extends Task {
|
|
|
23
53
|
isEndConsultEnabled: true,
|
|
24
54
|
},
|
|
25
55
|
wrapupData,
|
|
26
|
-
agentId
|
|
56
|
+
agentId,
|
|
57
|
+
agentName,
|
|
58
|
+
apiAIAssistant
|
|
27
59
|
);
|
|
28
60
|
}
|
|
29
61
|
|
|
@@ -64,7 +96,170 @@ class SpyAcceptTask extends Task {
|
|
|
64
96
|
}
|
|
65
97
|
}
|
|
66
98
|
|
|
67
|
-
|
|
99
|
+
jest.mock('xstate', () => ({
|
|
100
|
+
createActor: jest.fn((machine) => {
|
|
101
|
+
let snapshot = {value: 'IDLE', context: {}, event: undefined as any};
|
|
102
|
+
const subscribers = new Set<any>();
|
|
103
|
+
const actions = machine?.options?.actions ?? {};
|
|
104
|
+
const notify = () => {
|
|
105
|
+
subscribers.forEach((subscriber) => subscriber(snapshot));
|
|
106
|
+
};
|
|
107
|
+
const transition = (value: string, event: any) => {
|
|
108
|
+
snapshot = {value, context: {}, event};
|
|
109
|
+
notify();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
subscribe: jest.fn((subscriber) => {
|
|
114
|
+
subscribers.add(subscriber);
|
|
115
|
+
|
|
116
|
+
return {unsubscribe: () => subscribers.delete(subscriber)};
|
|
117
|
+
}),
|
|
118
|
+
start: jest.fn(() => notify()),
|
|
119
|
+
stop: jest.fn(),
|
|
120
|
+
getSnapshot: jest.fn(() => snapshot),
|
|
121
|
+
send: jest.fn((event) => {
|
|
122
|
+
switch (event?.type) {
|
|
123
|
+
case 'TASK_INCOMING':
|
|
124
|
+
actions.emitTaskIncoming?.({event});
|
|
125
|
+
transition('OFFERED', event);
|
|
126
|
+
break;
|
|
127
|
+
case 'TASK_OFFERED':
|
|
128
|
+
actions.requestAutoAnswer?.({event});
|
|
129
|
+
transition('OFFERED', event);
|
|
130
|
+
break;
|
|
131
|
+
case 'ASSIGN':
|
|
132
|
+
actions.emitTaskAssigned?.({event});
|
|
133
|
+
transition('CONNECTED', event);
|
|
134
|
+
break;
|
|
135
|
+
case 'CONTACT_UPDATED':
|
|
136
|
+
actions.syncTaskDataFromEvent?.({event});
|
|
137
|
+
transition(snapshot.value, event);
|
|
138
|
+
break;
|
|
139
|
+
case 'CONTACT_OWNER_CHANGED':
|
|
140
|
+
actions.emitTaskHydrate?.({event});
|
|
141
|
+
transition(snapshot.value, event);
|
|
142
|
+
break;
|
|
143
|
+
case 'CONFERENCE_START':
|
|
144
|
+
transition('CONFERENCING', event);
|
|
145
|
+
break;
|
|
146
|
+
case 'PARTICIPANT_LEAVE':
|
|
147
|
+
actions.emitTaskParticipantLeft?.({event});
|
|
148
|
+
transition(snapshot.value, event);
|
|
149
|
+
break;
|
|
150
|
+
case 'CONTACT_ENDED':
|
|
151
|
+
actions.requestCleanup?.({event});
|
|
152
|
+
transition('WRAPUP', event);
|
|
153
|
+
break;
|
|
154
|
+
case 'RONA':
|
|
155
|
+
actions.cleanupResources?.({event});
|
|
156
|
+
transition('ENDED', event);
|
|
157
|
+
break;
|
|
158
|
+
default:
|
|
159
|
+
transition(snapshot.value, event);
|
|
160
|
+
}
|
|
161
|
+
}),
|
|
162
|
+
};
|
|
163
|
+
}),
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
jest.mock('../../../../../src/services/task/state-machine', () => ({
|
|
167
|
+
__esModule: true,
|
|
168
|
+
TaskEvent: {
|
|
169
|
+
TASK_INCOMING: 'TASK_INCOMING',
|
|
170
|
+
TASK_OFFERED: 'TASK_OFFERED',
|
|
171
|
+
ASSIGN: 'ASSIGN',
|
|
172
|
+
CONTACT_UPDATED: 'CONTACT_UPDATED',
|
|
173
|
+
CONTACT_OWNER_CHANGED: 'CONTACT_OWNER_CHANGED',
|
|
174
|
+
CONFERENCE_START: 'CONFERENCE_START',
|
|
175
|
+
PARTICIPANT_LEAVE: 'PARTICIPANT_LEAVE',
|
|
176
|
+
CONTACT_ENDED: 'CONTACT_ENDED',
|
|
177
|
+
RONA: 'RONA',
|
|
178
|
+
TASK_WRAPUP: 'TASK_WRAPUP',
|
|
179
|
+
},
|
|
180
|
+
createTaskStateMachine: jest.fn((_config, options) => ({options})),
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
jest.mock('../../../../../src/services/task/state-machine/uiControlsComputer', () => {
|
|
184
|
+
const createControl = () => ({isVisible: false, isEnabled: false});
|
|
185
|
+
const createLegControls = () => ({
|
|
186
|
+
accept: createControl(),
|
|
187
|
+
decline: createControl(),
|
|
188
|
+
hold: createControl(),
|
|
189
|
+
transfer: createControl(),
|
|
190
|
+
consult: createControl(),
|
|
191
|
+
end: createControl(),
|
|
192
|
+
recording: createControl(),
|
|
193
|
+
mute: createControl(),
|
|
194
|
+
consultTransfer: createControl(),
|
|
195
|
+
endConsult: createControl(),
|
|
196
|
+
conference: createControl(),
|
|
197
|
+
exitConference: createControl(),
|
|
198
|
+
transferConference: createControl(),
|
|
199
|
+
mergeToConference: createControl(),
|
|
200
|
+
wrapup: createControl(),
|
|
201
|
+
switch: createControl(),
|
|
202
|
+
});
|
|
203
|
+
const createDefaultControls = () => ({
|
|
204
|
+
main: createLegControls(),
|
|
205
|
+
consult: createLegControls(),
|
|
206
|
+
activeLeg: 'main',
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
__esModule: true,
|
|
211
|
+
computeUIControls: jest.fn((state) => {
|
|
212
|
+
const controls = createDefaultControls();
|
|
213
|
+
if (state === 'CONFERENCING') {
|
|
214
|
+
controls.main.exitConference = {isVisible: true, isEnabled: true};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return controls;
|
|
218
|
+
}),
|
|
219
|
+
getDefaultUIControls: jest.fn(createDefaultControls),
|
|
220
|
+
haveUIControlsChanged: jest.fn(() => false),
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
jest.mock('../../../../../src/services/task/AutoWrapup', () => ({
|
|
225
|
+
__esModule: true,
|
|
226
|
+
default: jest.fn().mockImplementation(() => ({
|
|
227
|
+
start: jest.fn(),
|
|
228
|
+
clear: jest.fn(),
|
|
229
|
+
})),
|
|
230
|
+
}));
|
|
231
|
+
|
|
232
|
+
jest.mock('../../../../../src/services/task/TaskUtils', () => ({
|
|
233
|
+
createSummaryError: jest.fn((errorCode) => {
|
|
234
|
+
const error = new Error(errorCode) as Error & {data?: Record<string, unknown>};
|
|
235
|
+
error.data = {errorCode};
|
|
236
|
+
|
|
237
|
+
return error;
|
|
238
|
+
}),
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
jest.mock('../../../../../src/services/core/Utils', () => ({
|
|
242
|
+
getErrorDetails: jest.fn((_error, method) => ({
|
|
243
|
+
error: new Error(`Error while performing ${method}`),
|
|
244
|
+
})),
|
|
245
|
+
}));
|
|
246
|
+
|
|
247
|
+
jest.mock('../../../../../src/metrics/MetricsManager', () => {
|
|
248
|
+
const metricsInstance = {
|
|
249
|
+
trackEvent: jest.fn(),
|
|
250
|
+
timeEvent: jest.fn(),
|
|
251
|
+
};
|
|
252
|
+
const MetricsManager = jest.fn();
|
|
253
|
+
|
|
254
|
+
(MetricsManager as any).getInstance = jest.fn(() => metricsInstance);
|
|
255
|
+
(MetricsManager as any).getCommonTrackingFieldForAQMResponse = jest.fn(() => ({}));
|
|
256
|
+
(MetricsManager as any).getCommonTrackingFieldForAQMResponseFailed = jest.fn(() => ({}));
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
__esModule: true,
|
|
260
|
+
default: MetricsManager,
|
|
261
|
+
};
|
|
262
|
+
});
|
|
68
263
|
|
|
69
264
|
jest.mock('../../../../../src/logger-proxy', () => ({
|
|
70
265
|
__esModule: true,
|
|
@@ -83,6 +278,10 @@ jest.mock('../../../../../src/services/core/WebexRequest', () => ({
|
|
|
83
278
|
},
|
|
84
279
|
}));
|
|
85
280
|
|
|
281
|
+
beforeEach(() => {
|
|
282
|
+
jest.clearAllMocks();
|
|
283
|
+
});
|
|
284
|
+
|
|
86
285
|
describe('Task (base class)', () => {
|
|
87
286
|
const dummyContact = {} as any;
|
|
88
287
|
const initialData = {
|
|
@@ -256,6 +455,33 @@ describe('Task (base class)', () => {
|
|
|
256
455
|
expect(emitSpy).not.toHaveBeenCalledWith(TASK_EVENTS.TASK_WRAPUP, task);
|
|
257
456
|
});
|
|
258
457
|
|
|
458
|
+
it('adapts incoming, consulting, and rejection state-machine actions to task events', () => {
|
|
459
|
+
const actions = (task as any).getStateMachineActionOverrides();
|
|
460
|
+
const incomingData = createTaskData({interactionId: 'action-incoming'});
|
|
461
|
+
const emitSpy = jest.spyOn(task, 'emit');
|
|
462
|
+
|
|
463
|
+
actions.emitTaskIncoming({event: {type: 'TASK_INCOMING', taskData: incomingData}});
|
|
464
|
+
actions.emitTaskConsulting({
|
|
465
|
+
event: {
|
|
466
|
+
type: 'CONSULTING_ACTIVE',
|
|
467
|
+
taskData: {...incomingData, isConsulted: false},
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
actions.emitTaskConsulting({
|
|
471
|
+
event: {
|
|
472
|
+
type: 'CONSULTING_ACTIVE',
|
|
473
|
+
taskData: {...incomingData, isConsulted: true},
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
actions.emitTaskReject({event: {type: 'ASSIGN_FAILED', reason: 'busy'}});
|
|
477
|
+
actions.emitTaskReject({event: undefined});
|
|
478
|
+
|
|
479
|
+
expect(task.data.interactionId).toBe('action-incoming');
|
|
480
|
+
expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONSULTING, task);
|
|
481
|
+
expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_CONSULT_ACCEPTED, task);
|
|
482
|
+
expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_REJECT, 'busy');
|
|
483
|
+
});
|
|
484
|
+
|
|
259
485
|
it('starts auto wrap-up only when wrapUpRequired is true and profile autoWrapup is enabled', () => {
|
|
260
486
|
const wrapUpProps = {
|
|
261
487
|
autoWrapup: true,
|
|
@@ -301,12 +527,12 @@ describe('Task (base class)', () => {
|
|
|
301
527
|
'agent-1'
|
|
302
528
|
);
|
|
303
529
|
|
|
304
|
-
jest.useFakeTimers();
|
|
305
530
|
try {
|
|
306
531
|
wrappingTask.updateTaskData(createTaskData({wrapUpRequired: true}) as TaskData);
|
|
307
532
|
expect(wrappingTask.autoWrapup).toBeDefined();
|
|
308
533
|
|
|
309
|
-
|
|
534
|
+
const onComplete = (wrappingTask.autoWrapup as any).start.mock.calls[0][0];
|
|
535
|
+
await onComplete();
|
|
310
536
|
|
|
311
537
|
expect(contact.wrapup).toHaveBeenCalledWith({
|
|
312
538
|
interactionId: 'interaction-1',
|
|
@@ -314,7 +540,6 @@ describe('Task (base class)', () => {
|
|
|
314
540
|
});
|
|
315
541
|
} finally {
|
|
316
542
|
wrappingTask.cancelAutoWrapupTimer();
|
|
317
|
-
jest.useRealTimers();
|
|
318
543
|
}
|
|
319
544
|
});
|
|
320
545
|
|
|
@@ -544,7 +769,7 @@ describe('Task (base class)', () => {
|
|
|
544
769
|
taskData: {...data, isAutoAnswering: true} as any,
|
|
545
770
|
});
|
|
546
771
|
|
|
547
|
-
await
|
|
772
|
+
await flushEventLoopTurn();
|
|
548
773
|
expect(webrtcTask.acceptMock).toHaveBeenCalled();
|
|
549
774
|
expect(autoAnsweredSpy).toHaveBeenCalledWith(webrtcTask);
|
|
550
775
|
});
|
|
@@ -556,7 +781,7 @@ describe('Task (base class)', () => {
|
|
|
556
781
|
webrtcTask.sendStateMachineEvent({type: TaskEvent.TASK_INCOMING, taskData: data});
|
|
557
782
|
webrtcTask.sendStateMachineEvent({type: TaskEvent.TASK_OFFERED, taskData: data});
|
|
558
783
|
|
|
559
|
-
await
|
|
784
|
+
await flushEventLoopTurn();
|
|
560
785
|
expect(webrtcTask.acceptMock).not.toHaveBeenCalled();
|
|
561
786
|
});
|
|
562
787
|
|
|
@@ -567,7 +792,7 @@ describe('Task (base class)', () => {
|
|
|
567
792
|
pstnTask.sendStateMachineEvent({type: TaskEvent.TASK_INCOMING, taskData: data});
|
|
568
793
|
pstnTask.sendStateMachineEvent({type: TaskEvent.TASK_OFFERED, taskData: data});
|
|
569
794
|
|
|
570
|
-
await
|
|
795
|
+
await flushEventLoopTurn();
|
|
571
796
|
expect(pstnTask.acceptMock).not.toHaveBeenCalled();
|
|
572
797
|
});
|
|
573
798
|
|
|
@@ -579,7 +804,7 @@ describe('Task (base class)', () => {
|
|
|
579
804
|
webrtcTask.sendStateMachineEvent({type: TaskEvent.TASK_INCOMING, taskData: data});
|
|
580
805
|
webrtcTask.sendStateMachineEvent({type: TaskEvent.TASK_OFFERED, taskData: data});
|
|
581
806
|
|
|
582
|
-
await
|
|
807
|
+
await flushEventLoopTurn();
|
|
583
808
|
expect(webrtcTask.data.isAutoAnswering).toBe(false);
|
|
584
809
|
});
|
|
585
810
|
|
|
@@ -743,3 +968,1756 @@ describe('Task failure scenarios', () => {
|
|
|
743
968
|
await expect(task.wrapup(payload)).rejects.toThrow('Error while performing wrapup');
|
|
744
969
|
});
|
|
745
970
|
});
|
|
971
|
+
|
|
972
|
+
const createAISummaryTaskData = (overrides: Partial<TaskData> = {}): TaskData => {
|
|
973
|
+
const {interaction: interactionOverrides, ...taskOverrides} = overrides;
|
|
974
|
+
const interactionId = taskOverrides.interactionId ?? 'interaction-1';
|
|
975
|
+
|
|
976
|
+
return createTaskData({
|
|
977
|
+
taskId: 'task-owner-1',
|
|
978
|
+
...taskOverrides,
|
|
979
|
+
interactionId,
|
|
980
|
+
interaction: {
|
|
981
|
+
interactionId,
|
|
982
|
+
mainInteractionId: 'conversation-1',
|
|
983
|
+
...(interactionOverrides as Record<string, unknown> | undefined),
|
|
984
|
+
},
|
|
985
|
+
});
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
const createPostCallSummaryPayload = () => ({
|
|
989
|
+
conversationId: 'conversation-1',
|
|
990
|
+
summaryText: 'generated post-call summary',
|
|
991
|
+
sections: {initialContactReason: 'billing'},
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
const createMidCallSummaryPayload = () => ({
|
|
995
|
+
conversationId: 'conversation-1',
|
|
996
|
+
summaryText: 'generated mid-call summary',
|
|
997
|
+
sections: {reasonForTransferOrConsult: 'specialist'},
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
const createPostCallResponsePayload = (
|
|
1001
|
+
overrides: Partial<AISummaryResponse> = {}
|
|
1002
|
+
): AISummaryResponse =>
|
|
1003
|
+
({
|
|
1004
|
+
summary: {summarySectionKeySentinel: 'summary-section-value-sentinel'} as any,
|
|
1005
|
+
feedback: 'thumbs_up',
|
|
1006
|
+
state: 'DEFAULT',
|
|
1007
|
+
wrapUpCode: 'resolved',
|
|
1008
|
+
numberOfTimesViewed: 1,
|
|
1009
|
+
numberOfTimesEdited: 0,
|
|
1010
|
+
numberOfTimesCopied: 0,
|
|
1011
|
+
...overrides,
|
|
1012
|
+
} as AISummaryResponse);
|
|
1013
|
+
|
|
1014
|
+
const createMidCallResponsePayload = (
|
|
1015
|
+
overrides: Partial<AISummaryResponse> = {}
|
|
1016
|
+
): AISummaryResponse =>
|
|
1017
|
+
({
|
|
1018
|
+
summaryReceived: true,
|
|
1019
|
+
summary: {midCallSectionKeySentinel: 'mid-call-section-value-sentinel'} as any,
|
|
1020
|
+
feedback: 'none',
|
|
1021
|
+
state: 'DEFAULT',
|
|
1022
|
+
numberOfTimesViewed: 1,
|
|
1023
|
+
numberOfTimesEdited: 0,
|
|
1024
|
+
numberOfTimesCopied: 0,
|
|
1025
|
+
...overrides,
|
|
1026
|
+
} as AISummaryResponse);
|
|
1027
|
+
|
|
1028
|
+
const summaryRequestCases: Array<{
|
|
1029
|
+
label: string;
|
|
1030
|
+
invoke: (task: DummyTask) => Promise<any>;
|
|
1031
|
+
eventName: AIAssistantEventName;
|
|
1032
|
+
timeoutCode: string;
|
|
1033
|
+
createPayload: () => any;
|
|
1034
|
+
}> = [
|
|
1035
|
+
{
|
|
1036
|
+
label: 'post-call',
|
|
1037
|
+
invoke: (task) => task.requestPostCallSummary(),
|
|
1038
|
+
eventName: AIAssistantEventName.GET_POST_CALL_SUMMARY,
|
|
1039
|
+
timeoutCode: AI_SUMMARY_ERROR_CODES.POST_CALL_SUMMARY_TIMEOUT,
|
|
1040
|
+
createPayload: createPostCallSummaryPayload,
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
label: 'consult mid-call',
|
|
1044
|
+
invoke: (task) => task.requestMidCallSummary('CONSULT'),
|
|
1045
|
+
eventName: AIAssistantEventName.GET_MID_CALL_CONSULT_SUMMARY,
|
|
1046
|
+
timeoutCode: AI_SUMMARY_ERROR_CODES.MID_CALL_SUMMARY_TIMEOUT,
|
|
1047
|
+
createPayload: createMidCallSummaryPayload,
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
label: 'transfer mid-call',
|
|
1051
|
+
invoke: (task) => task.requestMidCallSummary('TRANSFER'),
|
|
1052
|
+
eventName: AIAssistantEventName.GET_MID_CALL_TRANSFER_SUMMARY,
|
|
1053
|
+
timeoutCode: AI_SUMMARY_ERROR_CODES.MID_CALL_SUMMARY_TIMEOUT,
|
|
1054
|
+
createPayload: createMidCallSummaryPayload,
|
|
1055
|
+
},
|
|
1056
|
+
];
|
|
1057
|
+
|
|
1058
|
+
describe('Task AI summary APIs', () => {
|
|
1059
|
+
const dummyContact = {} as any;
|
|
1060
|
+
const injectSummaryDependencies = (
|
|
1061
|
+
task: DummyTask,
|
|
1062
|
+
apiAIAssistant: any,
|
|
1063
|
+
flags: {wrapUpSummariesEnabled?: boolean; consultTransferSummariesEnabled?: boolean}
|
|
1064
|
+
) => {
|
|
1065
|
+
apiAIAssistant.aiFeature = {id: 'summary-flags', generatedSummaries: flags};
|
|
1066
|
+
(task as any).apiAIAssistant = apiAIAssistant;
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
const createSummaryMocks = (
|
|
1070
|
+
task: DummyTask,
|
|
1071
|
+
options: {
|
|
1072
|
+
postCallEnabled?: boolean;
|
|
1073
|
+
midCallEnabled?: boolean;
|
|
1074
|
+
wrapUpSummariesEnabled?: boolean;
|
|
1075
|
+
consultTransferSummariesEnabled?: boolean;
|
|
1076
|
+
registrationResult?: Promise<any>;
|
|
1077
|
+
} = {}
|
|
1078
|
+
) => {
|
|
1079
|
+
const pendingRequests = new Map<string, any>();
|
|
1080
|
+
const adapter: any = {
|
|
1081
|
+
sendEvent: jest.fn().mockResolvedValue(undefined),
|
|
1082
|
+
requestAndWaitForRtd: jest.fn(async (requestOptions: any) => {
|
|
1083
|
+
const key = JSON.stringify([requestOptions.rtdEventType, requestOptions.correlationId]);
|
|
1084
|
+
let resolveResult: (value: any) => void = () => undefined;
|
|
1085
|
+
let rejectResult: (error: Error) => void = () => undefined;
|
|
1086
|
+
const result = new Promise((resolve, reject) => {
|
|
1087
|
+
resolveResult = resolve;
|
|
1088
|
+
rejectResult = reject;
|
|
1089
|
+
});
|
|
1090
|
+
const pending = {
|
|
1091
|
+
result,
|
|
1092
|
+
timeoutId: undefined as any,
|
|
1093
|
+
resolve: resolveResult,
|
|
1094
|
+
reject: rejectResult,
|
|
1095
|
+
};
|
|
1096
|
+
pendingRequests.set(key, pending);
|
|
1097
|
+
const publishTimestamp = Date.now();
|
|
1098
|
+
const acknowledgement = adapter.sendEvent(
|
|
1099
|
+
requestOptions.agentId,
|
|
1100
|
+
requestOptions.interactionId,
|
|
1101
|
+
requestOptions.eventType,
|
|
1102
|
+
requestOptions.eventName,
|
|
1103
|
+
requestOptions.eventMetaData,
|
|
1104
|
+
undefined,
|
|
1105
|
+
undefined,
|
|
1106
|
+
publishTimestamp,
|
|
1107
|
+
requestOptions.timeout
|
|
1108
|
+
);
|
|
1109
|
+
const value = options.registrationResult ?? Promise.resolve(createPostCallSummaryPayload());
|
|
1110
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
1111
|
+
pending.timeoutId = setTimeout(
|
|
1112
|
+
() => reject(requestOptions.createTimeoutError()),
|
|
1113
|
+
requestOptions.timeoutMs
|
|
1114
|
+
);
|
|
1115
|
+
});
|
|
1116
|
+
try {
|
|
1117
|
+
const [resolvedValue] = await Promise.all([
|
|
1118
|
+
Promise.race([value, timeout]),
|
|
1119
|
+
acknowledgement,
|
|
1120
|
+
]);
|
|
1121
|
+
clearTimeout(pending.timeoutId);
|
|
1122
|
+
pendingRequests.delete(key);
|
|
1123
|
+
return resolvedValue;
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
clearTimeout(pending.timeoutId);
|
|
1126
|
+
pendingRequests.delete(key);
|
|
1127
|
+
throw error;
|
|
1128
|
+
}
|
|
1129
|
+
}),
|
|
1130
|
+
resolveFromRtdEvent: jest.fn((rtdEventType: string, correlationId: string, payload: any) => {
|
|
1131
|
+
const key = JSON.stringify([rtdEventType, correlationId]);
|
|
1132
|
+
const pending = pendingRequests.get(key);
|
|
1133
|
+
if (!pending) return 'not-found';
|
|
1134
|
+
clearTimeout(pending.timeoutId);
|
|
1135
|
+
pendingRequests.delete(key);
|
|
1136
|
+
pending.resolve(payload);
|
|
1137
|
+
return 'resolved';
|
|
1138
|
+
}),
|
|
1139
|
+
clearAllRtdRequests: jest.fn(() => {
|
|
1140
|
+
pendingRequests.forEach((pending) => {
|
|
1141
|
+
clearTimeout(pending.timeoutId);
|
|
1142
|
+
pending.reject(new Error(AI_SUMMARY_REQUEST_CANCELLED));
|
|
1143
|
+
});
|
|
1144
|
+
pendingRequests.clear();
|
|
1145
|
+
}),
|
|
1146
|
+
clear: jest.fn(),
|
|
1147
|
+
pendingRequests,
|
|
1148
|
+
};
|
|
1149
|
+
const coordinator = adapter;
|
|
1150
|
+
const postCallEnabled = Object.prototype.hasOwnProperty.call(options, 'postCallEnabled')
|
|
1151
|
+
? options.postCallEnabled
|
|
1152
|
+
: true;
|
|
1153
|
+
const midCallEnabled = Object.prototype.hasOwnProperty.call(options, 'midCallEnabled')
|
|
1154
|
+
? options.midCallEnabled
|
|
1155
|
+
: true;
|
|
1156
|
+
const wrapUpSummariesEnabled = Object.prototype.hasOwnProperty.call(
|
|
1157
|
+
options,
|
|
1158
|
+
'wrapUpSummariesEnabled'
|
|
1159
|
+
)
|
|
1160
|
+
? options.wrapUpSummariesEnabled
|
|
1161
|
+
: true;
|
|
1162
|
+
const consultTransferSummariesEnabled = Object.prototype.hasOwnProperty.call(
|
|
1163
|
+
options,
|
|
1164
|
+
'consultTransferSummariesEnabled'
|
|
1165
|
+
)
|
|
1166
|
+
? options.consultTransferSummariesEnabled
|
|
1167
|
+
: true;
|
|
1168
|
+
|
|
1169
|
+
const generatedSummaryFlags = {
|
|
1170
|
+
wrapUpSummariesEnabled,
|
|
1171
|
+
consultTransferSummariesEnabled,
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
task.setFeatureEnablement({
|
|
1175
|
+
interactionId: task.data.interactionId,
|
|
1176
|
+
postCallEnabled,
|
|
1177
|
+
midCallEnabled,
|
|
1178
|
+
});
|
|
1179
|
+
injectSummaryDependencies(task, adapter, generatedSummaryFlags);
|
|
1180
|
+
|
|
1181
|
+
return {adapter, coordinator};
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
const createRealSummaryMocks = (task: DummyTask) => {
|
|
1185
|
+
const result = createDeferred<any>();
|
|
1186
|
+
const adapter = {
|
|
1187
|
+
sendEvent: jest.fn().mockResolvedValue(undefined),
|
|
1188
|
+
requestAndWaitForRtd: jest.fn(async (requestOptions: any) => {
|
|
1189
|
+
const publishTimestamp = Date.now();
|
|
1190
|
+
let timeoutId: ReturnType<typeof setTimeout>;
|
|
1191
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
1192
|
+
timeoutId = setTimeout(
|
|
1193
|
+
() => reject(requestOptions.createTimeoutError()),
|
|
1194
|
+
requestOptions.timeoutMs
|
|
1195
|
+
);
|
|
1196
|
+
});
|
|
1197
|
+
await adapter.sendEvent(
|
|
1198
|
+
requestOptions.agentId,
|
|
1199
|
+
requestOptions.interactionId,
|
|
1200
|
+
requestOptions.eventType,
|
|
1201
|
+
requestOptions.eventName,
|
|
1202
|
+
requestOptions.eventMetaData,
|
|
1203
|
+
undefined,
|
|
1204
|
+
undefined,
|
|
1205
|
+
publishTimestamp,
|
|
1206
|
+
requestOptions.timeout
|
|
1207
|
+
);
|
|
1208
|
+
try {
|
|
1209
|
+
return await Promise.race([result.promise, timeout]);
|
|
1210
|
+
} finally {
|
|
1211
|
+
clearTimeout(timeoutId);
|
|
1212
|
+
}
|
|
1213
|
+
}),
|
|
1214
|
+
resolveFromRtdEvent: jest.fn((_eventType: string, _correlationId: string, payload: any) => {
|
|
1215
|
+
result.resolve(payload);
|
|
1216
|
+
return 'resolved';
|
|
1217
|
+
}),
|
|
1218
|
+
clearAll: jest.fn(() => result.reject(new Error(AI_SUMMARY_REQUEST_CANCELLED))),
|
|
1219
|
+
clear: jest.fn(),
|
|
1220
|
+
} as any;
|
|
1221
|
+
const generatedSummaryFlags = {
|
|
1222
|
+
wrapUpSummariesEnabled: true,
|
|
1223
|
+
consultTransferSummariesEnabled: true,
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
task.setFeatureEnablement({
|
|
1227
|
+
interactionId: task.data.interactionId,
|
|
1228
|
+
postCallEnabled: true,
|
|
1229
|
+
midCallEnabled: true,
|
|
1230
|
+
});
|
|
1231
|
+
injectSummaryDependencies(task, adapter, generatedSummaryFlags);
|
|
1232
|
+
|
|
1233
|
+
return {adapter, coordinator: adapter, result};
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
const expectSummaryGetEvent = (
|
|
1237
|
+
adapter: {sendEvent: jest.Mock},
|
|
1238
|
+
eventName: string,
|
|
1239
|
+
callIndex?: number
|
|
1240
|
+
) => {
|
|
1241
|
+
const expectedArgs = [
|
|
1242
|
+
'agent-1',
|
|
1243
|
+
'interaction-1',
|
|
1244
|
+
'CTI_EVENT',
|
|
1245
|
+
eventName,
|
|
1246
|
+
expect.objectContaining({
|
|
1247
|
+
conversationId: 'conversation-1',
|
|
1248
|
+
clientType: 'WxCC',
|
|
1249
|
+
}),
|
|
1250
|
+
undefined,
|
|
1251
|
+
undefined,
|
|
1252
|
+
expect.any(Number),
|
|
1253
|
+
AI_SUMMARY_DURATION_MS,
|
|
1254
|
+
];
|
|
1255
|
+
|
|
1256
|
+
if (callIndex === undefined) {
|
|
1257
|
+
expect(adapter.sendEvent).toHaveBeenCalledWith(...expectedArgs);
|
|
1258
|
+
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
expect(adapter.sendEvent).toHaveBeenNthCalledWith(callIndex, ...expectedArgs);
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
const getSummaryEventPayload = (adapter: {sendEvent: jest.Mock}, callIndex = 0) => {
|
|
1266
|
+
const [agentId, interactionId, , eventName, eventData, , , publishTimestamp] =
|
|
1267
|
+
adapter.sendEvent.mock.calls[callIndex];
|
|
1268
|
+
const {clientType: _clientType, action: _action, ...summaryData} = eventData;
|
|
1269
|
+
|
|
1270
|
+
return {
|
|
1271
|
+
agentId,
|
|
1272
|
+
interactionId,
|
|
1273
|
+
eventName,
|
|
1274
|
+
...summaryData,
|
|
1275
|
+
...(publishTimestamp !== undefined ? {publishTimestamp} : {}),
|
|
1276
|
+
};
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
const spyOnAISummaryMetrics = (task: DummyTask) => {
|
|
1280
|
+
const metricsManager = (task as any).metricsManager;
|
|
1281
|
+
|
|
1282
|
+
return {
|
|
1283
|
+
trackEvent: jest.spyOn(metricsManager, 'trackEvent').mockImplementation(() => undefined),
|
|
1284
|
+
timeEvent: jest.spyOn(metricsManager, 'timeEvent').mockImplementation(() => undefined),
|
|
1285
|
+
};
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
const getPendingRequest = (
|
|
1289
|
+
coordinator: any,
|
|
1290
|
+
eventType: 'POST_CALL_SUMMARY' | 'MID_CALL_SUMMARY',
|
|
1291
|
+
conversationId: string
|
|
1292
|
+
) =>
|
|
1293
|
+
(coordinator as any).pendingRequests.get(JSON.stringify([eventType, conversationId]));
|
|
1294
|
+
|
|
1295
|
+
afterEach(() => {
|
|
1296
|
+
jest.useRealTimers();
|
|
1297
|
+
jest.restoreAllMocks();
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
it('defines the exact AI summary operation metric names', () => {
|
|
1301
|
+
expect(METRIC_EVENT_NAMES).toMatchObject({
|
|
1302
|
+
AI_SUMMARY_GET_POST_CALL_SUCCESS: 'Post Call Summary Get Success',
|
|
1303
|
+
AI_SUMMARY_GET_POST_CALL_FAILED: 'Post Call Summary Get Failed',
|
|
1304
|
+
AI_SUMMARY_GET_MID_CALL_SUCCESS: 'Mid Call Summary Get Success',
|
|
1305
|
+
AI_SUMMARY_GET_MID_CALL_FAILED: 'Mid Call Summary Get Failed',
|
|
1306
|
+
AI_SUMMARY_POST_CALL_RESPONSE_SUCCESS: 'Post Call Summary Response Success',
|
|
1307
|
+
AI_SUMMARY_POST_CALL_RESPONSE_FAILED: 'Post Call Summary Response Failed',
|
|
1308
|
+
AI_SUMMARY_MID_CALL_RESPONSE_SUCCESS: 'Mid Call Summary Response Success',
|
|
1309
|
+
AI_SUMMARY_MID_CALL_RESPONSE_FAILED: 'Mid Call Summary Response Failed',
|
|
1310
|
+
});
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
it('requests a post-call summary through accepted registration, HTTP acknowledgement, and matching RTD result', async () => {
|
|
1314
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
1315
|
+
const metrics = spyOnAISummaryMetrics(task);
|
|
1316
|
+
const postCallResult = createPostCallSummaryPayload();
|
|
1317
|
+
const {adapter, coordinator} = createSummaryMocks(task, {
|
|
1318
|
+
registrationResult: Promise.resolve(postCallResult),
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
await expect(task.requestPostCallSummary()).resolves.toBe(postCallResult);
|
|
1322
|
+
|
|
1323
|
+
expect(coordinator.requestAndWaitForRtd).toHaveBeenCalledWith(
|
|
1324
|
+
expect.objectContaining({
|
|
1325
|
+
correlationId: 'conversation-1',
|
|
1326
|
+
rtdEventType: 'POST_CALL_SUMMARY',
|
|
1327
|
+
timeoutMs: AI_SUMMARY_DURATION_MS,
|
|
1328
|
+
})
|
|
1329
|
+
);
|
|
1330
|
+
expectSummaryGetEvent(adapter, AIAssistantEventName.GET_POST_CALL_SUMMARY);
|
|
1331
|
+
expect(metrics.trackEvent).toHaveBeenCalledWith(
|
|
1332
|
+
METRIC_EVENT_NAMES.AI_SUMMARY_GET_POST_CALL_SUCCESS,
|
|
1333
|
+
expect.objectContaining({
|
|
1334
|
+
conversationId: 'conversation-1',
|
|
1335
|
+
interactionId: 'interaction-1',
|
|
1336
|
+
}),
|
|
1337
|
+
['operational']
|
|
1338
|
+
);
|
|
1339
|
+
expect(metrics.timeEvent).toHaveBeenCalledWith([
|
|
1340
|
+
METRIC_EVENT_NAMES.AI_SUMMARY_GET_POST_CALL_SUCCESS,
|
|
1341
|
+
METRIC_EVENT_NAMES.AI_SUMMARY_GET_POST_CALL_FAILED,
|
|
1342
|
+
]);
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
it('maps consult and transfer mid-call summary requests to their exact outbound event names', async () => {
|
|
1346
|
+
const consultTask = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
1347
|
+
const consultMetrics = spyOnAISummaryMetrics(consultTask);
|
|
1348
|
+
const consultResult = createMidCallSummaryPayload();
|
|
1349
|
+
const consultMocks = createSummaryMocks(consultTask, {
|
|
1350
|
+
registrationResult: Promise.resolve(consultResult),
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
await expect(consultTask.requestMidCallSummary('CONSULT')).resolves.toBe(consultResult);
|
|
1354
|
+
|
|
1355
|
+
expect(consultMocks.coordinator.requestAndWaitForRtd).toHaveBeenCalledWith(
|
|
1356
|
+
expect.objectContaining({
|
|
1357
|
+
correlationId: 'conversation-1',
|
|
1358
|
+
rtdEventType: 'MID_CALL_SUMMARY',
|
|
1359
|
+
timeoutMs: AI_SUMMARY_DURATION_MS,
|
|
1360
|
+
})
|
|
1361
|
+
);
|
|
1362
|
+
expectSummaryGetEvent(
|
|
1363
|
+
consultMocks.adapter,
|
|
1364
|
+
AIAssistantEventName.GET_MID_CALL_CONSULT_SUMMARY
|
|
1365
|
+
);
|
|
1366
|
+
expect(consultMetrics.trackEvent).toHaveBeenCalledWith(
|
|
1367
|
+
METRIC_EVENT_NAMES.AI_SUMMARY_GET_MID_CALL_SUCCESS,
|
|
1368
|
+
expect.objectContaining({
|
|
1369
|
+
operation: METHODS.REQUEST_MID_CALL_SUMMARY,
|
|
1370
|
+
actionType: 'CONSULT',
|
|
1371
|
+
conversationId: 'conversation-1',
|
|
1372
|
+
interactionId: 'interaction-1',
|
|
1373
|
+
}),
|
|
1374
|
+
['operational']
|
|
1375
|
+
);
|
|
1376
|
+
|
|
1377
|
+
const transferTask = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
1378
|
+
const transferResult = createMidCallSummaryPayload();
|
|
1379
|
+
const transferMocks = createSummaryMocks(transferTask, {
|
|
1380
|
+
registrationResult: Promise.resolve(transferResult),
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
await expect(transferTask.requestMidCallSummary('TRANSFER')).resolves.toBe(transferResult);
|
|
1384
|
+
expectSummaryGetEvent(
|
|
1385
|
+
transferMocks.adapter,
|
|
1386
|
+
AIAssistantEventName.GET_MID_CALL_TRANSFER_SUMMARY
|
|
1387
|
+
);
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
it.each([
|
|
1391
|
+
{
|
|
1392
|
+
label: 'post-call registration first',
|
|
1393
|
+
invoke: (task: DummyTask) => task.requestPostCallSummary(),
|
|
1394
|
+
inboundType: 'POST_CALL_SUMMARY' as const,
|
|
1395
|
+
timeoutCode: AI_SUMMARY_ERROR_CODES.POST_CALL_SUMMARY_TIMEOUT,
|
|
1396
|
+
eventName: AIAssistantEventName.GET_POST_CALL_SUMMARY,
|
|
1397
|
+
payload: createPostCallSummaryPayload(),
|
|
1398
|
+
successMetric: METRIC_EVENT_NAMES.AI_SUMMARY_GET_POST_CALL_SUCCESS,
|
|
1399
|
+
failureMetric: METRIC_EVENT_NAMES.AI_SUMMARY_GET_POST_CALL_FAILED,
|
|
1400
|
+
operation: METHODS.REQUEST_POST_CALL_SUMMARY,
|
|
1401
|
+
settleFirst: 'result' as const,
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
label: 'mid-call acknowledgement first',
|
|
1405
|
+
invoke: (task: DummyTask) => task.requestMidCallSummary('CONSULT'),
|
|
1406
|
+
inboundType: 'MID_CALL_SUMMARY' as const,
|
|
1407
|
+
timeoutCode: AI_SUMMARY_ERROR_CODES.MID_CALL_SUMMARY_TIMEOUT,
|
|
1408
|
+
eventName: AIAssistantEventName.GET_MID_CALL_CONSULT_SUMMARY,
|
|
1409
|
+
payload: createMidCallSummaryPayload(),
|
|
1410
|
+
successMetric: METRIC_EVENT_NAMES.AI_SUMMARY_GET_MID_CALL_SUCCESS,
|
|
1411
|
+
failureMetric: METRIC_EVENT_NAMES.AI_SUMMARY_GET_MID_CALL_FAILED,
|
|
1412
|
+
operation: METHODS.REQUEST_MID_CALL_SUMMARY,
|
|
1413
|
+
actionType: 'CONSULT',
|
|
1414
|
+
settleFirst: 'acknowledgement' as const,
|
|
1415
|
+
},
|
|
1416
|
+
])(
|
|
1417
|
+
'sends the HTTP request and waits for both request branches on $label',
|
|
1418
|
+
async ({
|
|
1419
|
+
invoke,
|
|
1420
|
+
inboundType,
|
|
1421
|
+
timeoutCode,
|
|
1422
|
+
eventName,
|
|
1423
|
+
payload,
|
|
1424
|
+
successMetric,
|
|
1425
|
+
failureMetric,
|
|
1426
|
+
operation,
|
|
1427
|
+
actionType,
|
|
1428
|
+
settleFirst,
|
|
1429
|
+
}) => {
|
|
1430
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
1431
|
+
const metrics = spyOnAISummaryMetrics(task);
|
|
1432
|
+
const acknowledgement = createDeferred<void>();
|
|
1433
|
+
const result = createDeferred<any>();
|
|
1434
|
+
const {adapter, coordinator} = createSummaryMocks(task, {
|
|
1435
|
+
registrationResult: result.promise,
|
|
1436
|
+
});
|
|
1437
|
+
let publicSettled = false;
|
|
1438
|
+
|
|
1439
|
+
adapter.sendEvent.mockReturnValueOnce(acknowledgement.promise);
|
|
1440
|
+
|
|
1441
|
+
const publicRequest = invoke(task);
|
|
1442
|
+
|
|
1443
|
+
publicRequest.then(
|
|
1444
|
+
() => {
|
|
1445
|
+
publicSettled = true;
|
|
1446
|
+
},
|
|
1447
|
+
() => {
|
|
1448
|
+
publicSettled = true;
|
|
1449
|
+
}
|
|
1450
|
+
);
|
|
1451
|
+
await Promise.resolve();
|
|
1452
|
+
|
|
1453
|
+
expect(coordinator.requestAndWaitForRtd).toHaveBeenCalledWith(
|
|
1454
|
+
expect.objectContaining({
|
|
1455
|
+
correlationId: 'conversation-1',
|
|
1456
|
+
rtdEventType: inboundType,
|
|
1457
|
+
timeoutMs: AI_SUMMARY_DURATION_MS,
|
|
1458
|
+
})
|
|
1459
|
+
);
|
|
1460
|
+
expectSummaryGetEvent(adapter, eventName);
|
|
1461
|
+
expect(publicSettled).toBe(false);
|
|
1462
|
+
|
|
1463
|
+
if (settleFirst === 'result') {
|
|
1464
|
+
result.resolve(payload);
|
|
1465
|
+
await Promise.resolve();
|
|
1466
|
+
expect(publicSettled).toBe(false);
|
|
1467
|
+
expect(metrics.trackEvent).not.toHaveBeenCalled();
|
|
1468
|
+
|
|
1469
|
+
acknowledgement.resolve(undefined);
|
|
1470
|
+
} else {
|
|
1471
|
+
acknowledgement.resolve(undefined);
|
|
1472
|
+
await Promise.resolve();
|
|
1473
|
+
expect(publicSettled).toBe(false);
|
|
1474
|
+
expect(metrics.trackEvent).not.toHaveBeenCalled();
|
|
1475
|
+
|
|
1476
|
+
result.resolve(payload);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
await expect(publicRequest).resolves.toBe(payload);
|
|
1480
|
+
expect(metrics.trackEvent).toHaveBeenCalledTimes(1);
|
|
1481
|
+
expect(metrics.trackEvent).toHaveBeenCalledWith(
|
|
1482
|
+
successMetric,
|
|
1483
|
+
expect.objectContaining({
|
|
1484
|
+
operation,
|
|
1485
|
+
...(actionType ? {actionType} : {}),
|
|
1486
|
+
conversationId: 'conversation-1',
|
|
1487
|
+
interactionId: 'interaction-1',
|
|
1488
|
+
}),
|
|
1489
|
+
['operational']
|
|
1490
|
+
);
|
|
1491
|
+
expect(metrics.timeEvent).toHaveBeenCalledWith([successMetric, failureMetric]);
|
|
1492
|
+
}
|
|
1493
|
+
);
|
|
1494
|
+
|
|
1495
|
+
it.each([
|
|
1496
|
+
{
|
|
1497
|
+
label: 'post-call organization false',
|
|
1498
|
+
invoke: (task: DummyTask) => task.requestPostCallSummary(),
|
|
1499
|
+
options: {wrapUpSummariesEnabled: false, postCallEnabled: true},
|
|
1500
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.POST_CALL_SUMMARY_DISABLED,
|
|
1501
|
+
failureMetric: METRIC_EVENT_NAMES.AI_SUMMARY_GET_POST_CALL_FAILED,
|
|
1502
|
+
operation: METHODS.REQUEST_POST_CALL_SUMMARY,
|
|
1503
|
+
},
|
|
1504
|
+
{
|
|
1505
|
+
label: 'post-call organization undefined',
|
|
1506
|
+
invoke: (task: DummyTask) => task.requestPostCallSummary(),
|
|
1507
|
+
options: {wrapUpSummariesEnabled: undefined, postCallEnabled: true},
|
|
1508
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.POST_CALL_SUMMARY_DISABLED,
|
|
1509
|
+
failureMetric: METRIC_EVENT_NAMES.AI_SUMMARY_GET_POST_CALL_FAILED,
|
|
1510
|
+
operation: METHODS.REQUEST_POST_CALL_SUMMARY,
|
|
1511
|
+
},
|
|
1512
|
+
{
|
|
1513
|
+
label: 'post-call interaction false',
|
|
1514
|
+
invoke: (task: DummyTask) => task.requestPostCallSummary(),
|
|
1515
|
+
options: {wrapUpSummariesEnabled: true, postCallEnabled: false},
|
|
1516
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.POST_CALL_SUMMARY_DISABLED,
|
|
1517
|
+
failureMetric: METRIC_EVENT_NAMES.AI_SUMMARY_GET_POST_CALL_FAILED,
|
|
1518
|
+
operation: METHODS.REQUEST_POST_CALL_SUMMARY,
|
|
1519
|
+
},
|
|
1520
|
+
{
|
|
1521
|
+
label: 'post-call interaction undefined',
|
|
1522
|
+
invoke: (task: DummyTask) => task.requestPostCallSummary(),
|
|
1523
|
+
options: {wrapUpSummariesEnabled: true, postCallEnabled: undefined},
|
|
1524
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.POST_CALL_SUMMARY_DISABLED,
|
|
1525
|
+
failureMetric: METRIC_EVENT_NAMES.AI_SUMMARY_GET_POST_CALL_FAILED,
|
|
1526
|
+
operation: METHODS.REQUEST_POST_CALL_SUMMARY,
|
|
1527
|
+
},
|
|
1528
|
+
{
|
|
1529
|
+
label: 'mid-call organization false',
|
|
1530
|
+
invoke: (task: DummyTask) => task.requestMidCallSummary('CONSULT'),
|
|
1531
|
+
options: {consultTransferSummariesEnabled: false, midCallEnabled: true},
|
|
1532
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.MID_CALL_SUMMARY_DISABLED,
|
|
1533
|
+
failureMetric: METRIC_EVENT_NAMES.AI_SUMMARY_GET_MID_CALL_FAILED,
|
|
1534
|
+
operation: METHODS.REQUEST_MID_CALL_SUMMARY,
|
|
1535
|
+
actionType: 'CONSULT',
|
|
1536
|
+
},
|
|
1537
|
+
{
|
|
1538
|
+
label: 'mid-call organization undefined',
|
|
1539
|
+
invoke: (task: DummyTask) => task.requestMidCallSummary('CONSULT'),
|
|
1540
|
+
options: {consultTransferSummariesEnabled: undefined, midCallEnabled: true},
|
|
1541
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.MID_CALL_SUMMARY_DISABLED,
|
|
1542
|
+
failureMetric: METRIC_EVENT_NAMES.AI_SUMMARY_GET_MID_CALL_FAILED,
|
|
1543
|
+
operation: METHODS.REQUEST_MID_CALL_SUMMARY,
|
|
1544
|
+
actionType: 'CONSULT',
|
|
1545
|
+
},
|
|
1546
|
+
{
|
|
1547
|
+
label: 'mid-call interaction false',
|
|
1548
|
+
invoke: (task: DummyTask) => task.requestMidCallSummary('CONSULT'),
|
|
1549
|
+
options: {consultTransferSummariesEnabled: true, midCallEnabled: false},
|
|
1550
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.MID_CALL_SUMMARY_DISABLED,
|
|
1551
|
+
failureMetric: METRIC_EVENT_NAMES.AI_SUMMARY_GET_MID_CALL_FAILED,
|
|
1552
|
+
operation: METHODS.REQUEST_MID_CALL_SUMMARY,
|
|
1553
|
+
actionType: 'CONSULT',
|
|
1554
|
+
},
|
|
1555
|
+
{
|
|
1556
|
+
label: 'mid-call interaction undefined',
|
|
1557
|
+
invoke: (task: DummyTask) => task.requestMidCallSummary('CONSULT'),
|
|
1558
|
+
options: {consultTransferSummariesEnabled: true, midCallEnabled: undefined},
|
|
1559
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.MID_CALL_SUMMARY_DISABLED,
|
|
1560
|
+
failureMetric: METRIC_EVENT_NAMES.AI_SUMMARY_GET_MID_CALL_FAILED,
|
|
1561
|
+
operation: METHODS.REQUEST_MID_CALL_SUMMARY,
|
|
1562
|
+
actionType: 'CONSULT',
|
|
1563
|
+
},
|
|
1564
|
+
])(
|
|
1565
|
+
'rejects disabled $label summary requests before registration or HTTP',
|
|
1566
|
+
async ({invoke, options, disabledCode, failureMetric, operation, actionType}) => {
|
|
1567
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
1568
|
+
const metrics = spyOnAISummaryMetrics(task);
|
|
1569
|
+
const {adapter, coordinator} = createSummaryMocks(task, options);
|
|
1570
|
+
|
|
1571
|
+
await expect(invoke(task)).rejects.toMatchObject(
|
|
1572
|
+
createAISummaryErrorExpectation(disabledCode)
|
|
1573
|
+
);
|
|
1574
|
+
expect(coordinator.requestAndWaitForRtd).not.toHaveBeenCalled();
|
|
1575
|
+
expect(adapter.sendEvent).not.toHaveBeenCalled();
|
|
1576
|
+
expect(metrics.trackEvent).toHaveBeenCalledTimes(1);
|
|
1577
|
+
expect(metrics.trackEvent).toHaveBeenCalledWith(
|
|
1578
|
+
failureMetric,
|
|
1579
|
+
expect.objectContaining({
|
|
1580
|
+
operation,
|
|
1581
|
+
...(actionType ? {actionType} : {}),
|
|
1582
|
+
failureCode: disabledCode,
|
|
1583
|
+
}),
|
|
1584
|
+
['operational']
|
|
1585
|
+
);
|
|
1586
|
+
expect(metrics.timeEvent).toHaveBeenCalled();
|
|
1587
|
+
}
|
|
1588
|
+
);
|
|
1589
|
+
|
|
1590
|
+
it.each([
|
|
1591
|
+
{
|
|
1592
|
+
label: 'post-call',
|
|
1593
|
+
invoke: (task: DummyTask) => task.requestPostCallSummary(),
|
|
1594
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.POST_CALL_SUMMARY_DISABLED,
|
|
1595
|
+
featureEnablement: {postCallEnabled: false, midCallEnabled: true},
|
|
1596
|
+
},
|
|
1597
|
+
{
|
|
1598
|
+
label: 'mid-call',
|
|
1599
|
+
invoke: (task: DummyTask) => task.requestMidCallSummary('CONSULT'),
|
|
1600
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.MID_CALL_SUMMARY_DISABLED,
|
|
1601
|
+
featureEnablement: {postCallEnabled: true, midCallEnabled: false},
|
|
1602
|
+
},
|
|
1603
|
+
])(
|
|
1604
|
+
'uses the top-level interaction feature key only for $label requests',
|
|
1605
|
+
async ({invoke, disabledCode, featureEnablement}) => {
|
|
1606
|
+
const task = new DummyTask(
|
|
1607
|
+
dummyContact,
|
|
1608
|
+
createAISummaryTaskData({
|
|
1609
|
+
interactionId: 'child-interaction-1',
|
|
1610
|
+
interaction: {mainInteractionId: 'conversation-1'} as any,
|
|
1611
|
+
})
|
|
1612
|
+
);
|
|
1613
|
+
const {adapter, coordinator} = createSummaryMocks(task, featureEnablement);
|
|
1614
|
+
|
|
1615
|
+
await expect(invoke(task)).rejects.toMatchObject(
|
|
1616
|
+
createAISummaryErrorExpectation(disabledCode)
|
|
1617
|
+
);
|
|
1618
|
+
expect(coordinator.requestAndWaitForRtd).not.toHaveBeenCalled();
|
|
1619
|
+
expect(adapter.sendEvent).not.toHaveBeenCalled();
|
|
1620
|
+
}
|
|
1621
|
+
);
|
|
1622
|
+
|
|
1623
|
+
it.each([
|
|
1624
|
+
{
|
|
1625
|
+
label: 'post-call organization false',
|
|
1626
|
+
invoke: (task: DummyTask) => task.requestPostCallSummary(),
|
|
1627
|
+
inboundType: 'POST_CALL_SUMMARY' as const,
|
|
1628
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.POST_CALL_SUMMARY_DISABLED,
|
|
1629
|
+
flags: {wrapUpSummariesEnabled: false, consultTransferSummariesEnabled: true},
|
|
1630
|
+
featureEnablement: {interactionId: 'interaction-1', postCallEnabled: true},
|
|
1631
|
+
},
|
|
1632
|
+
{
|
|
1633
|
+
label: 'post-call raw undefined interaction flag',
|
|
1634
|
+
invoke: (task: DummyTask) => task.requestPostCallSummary(),
|
|
1635
|
+
inboundType: 'POST_CALL_SUMMARY' as const,
|
|
1636
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.POST_CALL_SUMMARY_DISABLED,
|
|
1637
|
+
flags: {wrapUpSummariesEnabled: true, consultTransferSummariesEnabled: true},
|
|
1638
|
+
},
|
|
1639
|
+
{
|
|
1640
|
+
label: 'post-call mainInteractionId mismatch',
|
|
1641
|
+
invoke: (task: DummyTask) => task.requestPostCallSummary(),
|
|
1642
|
+
inboundType: 'POST_CALL_SUMMARY' as const,
|
|
1643
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.POST_CALL_SUMMARY_DISABLED,
|
|
1644
|
+
flags: {wrapUpSummariesEnabled: true, consultTransferSummariesEnabled: true},
|
|
1645
|
+
taskData: createAISummaryTaskData({
|
|
1646
|
+
interactionId: 'child-interaction-1',
|
|
1647
|
+
interaction: {mainInteractionId: 'conversation-1'} as any,
|
|
1648
|
+
}),
|
|
1649
|
+
featureEnablement: {interactionId: 'conversation-1', postCallEnabled: true},
|
|
1650
|
+
},
|
|
1651
|
+
{
|
|
1652
|
+
label: 'mid-call organization false',
|
|
1653
|
+
invoke: (task: DummyTask) => task.requestMidCallSummary('CONSULT'),
|
|
1654
|
+
inboundType: 'MID_CALL_SUMMARY' as const,
|
|
1655
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.MID_CALL_SUMMARY_DISABLED,
|
|
1656
|
+
flags: {wrapUpSummariesEnabled: true, consultTransferSummariesEnabled: false},
|
|
1657
|
+
featureEnablement: {interactionId: 'interaction-1', midCallEnabled: true},
|
|
1658
|
+
},
|
|
1659
|
+
{
|
|
1660
|
+
label: 'mid-call raw undefined interaction flag',
|
|
1661
|
+
invoke: (task: DummyTask) => task.requestMidCallSummary('CONSULT'),
|
|
1662
|
+
inboundType: 'MID_CALL_SUMMARY' as const,
|
|
1663
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.MID_CALL_SUMMARY_DISABLED,
|
|
1664
|
+
flags: {wrapUpSummariesEnabled: true, consultTransferSummariesEnabled: true},
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
label: 'mid-call mainInteractionId mismatch',
|
|
1668
|
+
invoke: (task: DummyTask) => task.requestMidCallSummary('CONSULT'),
|
|
1669
|
+
inboundType: 'MID_CALL_SUMMARY' as const,
|
|
1670
|
+
disabledCode: AI_SUMMARY_ERROR_CODES.MID_CALL_SUMMARY_DISABLED,
|
|
1671
|
+
flags: {wrapUpSummariesEnabled: true, consultTransferSummariesEnabled: true},
|
|
1672
|
+
taskData: createAISummaryTaskData({
|
|
1673
|
+
interactionId: 'child-interaction-1',
|
|
1674
|
+
interaction: {mainInteractionId: 'conversation-1'} as any,
|
|
1675
|
+
}),
|
|
1676
|
+
featureEnablement: {interactionId: 'conversation-1', midCallEnabled: true},
|
|
1677
|
+
},
|
|
1678
|
+
])(
|
|
1679
|
+
'leaves the real coordinator idle when $label disables a request',
|
|
1680
|
+
async ({invoke, inboundType, disabledCode, flags, taskData, featureEnablement}) => {
|
|
1681
|
+
jest.useFakeTimers();
|
|
1682
|
+
const task = new DummyTask(dummyContact, taskData ?? createAISummaryTaskData());
|
|
1683
|
+
const adapter: any = {
|
|
1684
|
+
sendEvent: jest.fn().mockResolvedValue(undefined),
|
|
1685
|
+
requestAndWaitForRtd: jest.fn(),
|
|
1686
|
+
pendingRequests: new Map(),
|
|
1687
|
+
};
|
|
1688
|
+
const coordinator = adapter;
|
|
1689
|
+
const registerSpy = jest.spyOn(coordinator, 'requestAndWaitForRtd');
|
|
1690
|
+
injectSummaryDependencies(task, adapter, flags);
|
|
1691
|
+
task.setFeatureEnablement({
|
|
1692
|
+
interactionId: task.data.interactionId,
|
|
1693
|
+
...(featureEnablement ?? {}),
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
await expect(invoke(task)).rejects.toMatchObject(
|
|
1697
|
+
createAISummaryErrorExpectation(disabledCode)
|
|
1698
|
+
);
|
|
1699
|
+
expect(registerSpy).not.toHaveBeenCalled();
|
|
1700
|
+
expect(adapter.sendEvent).not.toHaveBeenCalled();
|
|
1701
|
+
expect(getPendingRequest(coordinator, inboundType, 'conversation-1')).toBeUndefined();
|
|
1702
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
1703
|
+
}
|
|
1704
|
+
);
|
|
1705
|
+
|
|
1706
|
+
it('uses the agent configuration’s current generated-summary flags for each request', async () => {
|
|
1707
|
+
const firstFlags = {
|
|
1708
|
+
wrapUpSummariesEnabled: false,
|
|
1709
|
+
consultTransferSummariesEnabled: true,
|
|
1710
|
+
};
|
|
1711
|
+
const secondFlags = {
|
|
1712
|
+
wrapUpSummariesEnabled: true,
|
|
1713
|
+
consultTransferSummariesEnabled: true,
|
|
1714
|
+
};
|
|
1715
|
+
const adapter: any = {
|
|
1716
|
+
sendEvent: jest.fn().mockResolvedValue(undefined),
|
|
1717
|
+
pendingRequests: new Map(),
|
|
1718
|
+
};
|
|
1719
|
+
adapter.aiFeature = {id: 'summary-flags', generatedSummaries: firstFlags};
|
|
1720
|
+
const featureEnablement = {interactionId: 'interaction-1', postCallEnabled: true};
|
|
1721
|
+
const coordinator = adapter;
|
|
1722
|
+
let resolveResult: (payload: any) => void = () => undefined;
|
|
1723
|
+
adapter.requestAndWaitForRtd = jest.fn(async (requestOptions: any) => {
|
|
1724
|
+
const result = new Promise((resolve) => {
|
|
1725
|
+
resolveResult = resolve;
|
|
1726
|
+
});
|
|
1727
|
+
await adapter.sendEvent(
|
|
1728
|
+
requestOptions.agentId,
|
|
1729
|
+
requestOptions.interactionId,
|
|
1730
|
+
requestOptions.eventType,
|
|
1731
|
+
requestOptions.eventName,
|
|
1732
|
+
requestOptions.eventMetaData,
|
|
1733
|
+
undefined,
|
|
1734
|
+
undefined,
|
|
1735
|
+
Date.now(),
|
|
1736
|
+
requestOptions.timeout
|
|
1737
|
+
);
|
|
1738
|
+
return result;
|
|
1739
|
+
});
|
|
1740
|
+
coordinator.resolveFromRtdEvent = jest.fn(() => {
|
|
1741
|
+
resolveResult(createPostCallSummaryPayload());
|
|
1742
|
+
return 'resolved';
|
|
1743
|
+
});
|
|
1744
|
+
const task = new DummyTask(
|
|
1745
|
+
dummyContact,
|
|
1746
|
+
createAISummaryTaskData(),
|
|
1747
|
+
undefined,
|
|
1748
|
+
'agent-1',
|
|
1749
|
+
'Receiving Agent',
|
|
1750
|
+
adapter
|
|
1751
|
+
);
|
|
1752
|
+
task.setFeatureEnablement({interactionId: task.data.interactionId, ...featureEnablement});
|
|
1753
|
+
|
|
1754
|
+
await expect(task.requestPostCallSummary()).rejects.toMatchObject(
|
|
1755
|
+
createAISummaryErrorExpectation(AI_SUMMARY_ERROR_CODES.POST_CALL_SUMMARY_DISABLED)
|
|
1756
|
+
);
|
|
1757
|
+
expect(adapter.sendEvent).not.toHaveBeenCalled();
|
|
1758
|
+
|
|
1759
|
+
adapter.aiFeature = {id: 'summary-flags', generatedSummaries: secondFlags};
|
|
1760
|
+
const request = task.requestPostCallSummary();
|
|
1761
|
+
|
|
1762
|
+
await Promise.resolve();
|
|
1763
|
+
expect(adapter.sendEvent).toHaveBeenCalledTimes(1);
|
|
1764
|
+
expect(
|
|
1765
|
+
coordinator.resolveFromRtdEvent(
|
|
1766
|
+
'POST_CALL_SUMMARY',
|
|
1767
|
+
'conversation-1',
|
|
1768
|
+
createPostCallSummaryPayload()
|
|
1769
|
+
)
|
|
1770
|
+
).toBe('resolved');
|
|
1771
|
+
await expect(request).resolves.toEqual(createPostCallSummaryPayload());
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
it('cleans up only the accepted request token when the request acknowledgement rejects', async () => {
|
|
1775
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
1776
|
+
const neverSettlingResult = createDeferred<any>();
|
|
1777
|
+
const {adapter, coordinator} = createSummaryMocks(task, {
|
|
1778
|
+
registrationResult: neverSettlingResult.promise,
|
|
1779
|
+
});
|
|
1780
|
+
const baseUrlError = createAISummaryError(
|
|
1781
|
+
AI_SUMMARY_ERROR_CODES.AI_ASSISTANT_BASE_URL_NOT_AVAILABLE
|
|
1782
|
+
);
|
|
1783
|
+
|
|
1784
|
+
adapter.sendEvent.mockRejectedValue(baseUrlError);
|
|
1785
|
+
|
|
1786
|
+
await expect(task.requestPostCallSummary()).rejects.toBe(baseUrlError);
|
|
1787
|
+
expect(coordinator.requestAndWaitForRtd).toHaveBeenCalled();
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
it.each(summaryRequestCases.map((testCase) => [testCase.label, testCase] as const))(
|
|
1791
|
+
'%s request sends one outbound adapter invocation when it later fails from inbound timeout',
|
|
1792
|
+
async (_label, {invoke, eventName, timeoutCode}) => {
|
|
1793
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
1794
|
+
const timeoutResult = createDeferred<any>();
|
|
1795
|
+
const {adapter} = createSummaryMocks(task, {registrationResult: timeoutResult.promise});
|
|
1796
|
+
|
|
1797
|
+
const timedOutRequest = invoke(task);
|
|
1798
|
+
await flushEventLoopTurn();
|
|
1799
|
+
|
|
1800
|
+
expect(adapter.sendEvent).toHaveBeenCalledTimes(1);
|
|
1801
|
+
expectSummaryGetEvent(adapter, eventName);
|
|
1802
|
+
|
|
1803
|
+
timeoutResult.reject(createAISummaryError(timeoutCode));
|
|
1804
|
+
|
|
1805
|
+
await expect(timedOutRequest).rejects.toMatchObject({
|
|
1806
|
+
message: timeoutCode,
|
|
1807
|
+
data: {errorCode: timeoutCode},
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
);
|
|
1811
|
+
|
|
1812
|
+
it.each(summaryRequestCases.map((testCase) => [testCase.label, testCase] as const))(
|
|
1813
|
+
'issues a fresh second adapter call when %s request is invoked again after settlement',
|
|
1814
|
+
async (_label, {invoke, eventName, createPayload}) => {
|
|
1815
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
1816
|
+
const firstPayload = createPayload();
|
|
1817
|
+
const secondPayload = createPayload();
|
|
1818
|
+
const {adapter, coordinator} = createSummaryMocks(task);
|
|
1819
|
+
|
|
1820
|
+
expect(firstPayload).not.toBe(secondPayload);
|
|
1821
|
+
coordinator.requestAndWaitForRtd
|
|
1822
|
+
.mockImplementationOnce(async (requestOptions: any) => {
|
|
1823
|
+
const publishTimestamp = Date.now();
|
|
1824
|
+
await adapter.sendEvent(
|
|
1825
|
+
requestOptions.agentId,
|
|
1826
|
+
requestOptions.interactionId,
|
|
1827
|
+
requestOptions.eventType,
|
|
1828
|
+
requestOptions.eventName,
|
|
1829
|
+
requestOptions.eventMetaData,
|
|
1830
|
+
undefined,
|
|
1831
|
+
undefined,
|
|
1832
|
+
publishTimestamp,
|
|
1833
|
+
requestOptions.timeout
|
|
1834
|
+
);
|
|
1835
|
+
|
|
1836
|
+
return firstPayload;
|
|
1837
|
+
})
|
|
1838
|
+
.mockImplementationOnce(async (requestOptions: any) => {
|
|
1839
|
+
const publishTimestamp = Date.now();
|
|
1840
|
+
await adapter.sendEvent(
|
|
1841
|
+
requestOptions.agentId,
|
|
1842
|
+
requestOptions.interactionId,
|
|
1843
|
+
requestOptions.eventType,
|
|
1844
|
+
requestOptions.eventName,
|
|
1845
|
+
requestOptions.eventMetaData,
|
|
1846
|
+
undefined,
|
|
1847
|
+
undefined,
|
|
1848
|
+
publishTimestamp,
|
|
1849
|
+
requestOptions.timeout
|
|
1850
|
+
);
|
|
1851
|
+
|
|
1852
|
+
return secondPayload;
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
await expect(invoke(task)).resolves.toBe(firstPayload);
|
|
1856
|
+
await expect(invoke(task)).resolves.toBe(secondPayload);
|
|
1857
|
+
|
|
1858
|
+
expect(coordinator.requestAndWaitForRtd).toHaveBeenCalledTimes(2);
|
|
1859
|
+
expect(adapter.sendEvent).toHaveBeenCalledTimes(2);
|
|
1860
|
+
expectSummaryGetEvent(adapter, eventName, 1);
|
|
1861
|
+
expectSummaryGetEvent(adapter, eventName, 2);
|
|
1862
|
+
}
|
|
1863
|
+
);
|
|
1864
|
+
|
|
1865
|
+
it('settles public request promises when coordinator rejects owner cleanup results', async () => {
|
|
1866
|
+
const cleanupTask = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
1867
|
+
const cleanupResult = createDeferred<any>();
|
|
1868
|
+
|
|
1869
|
+
createSummaryMocks(cleanupTask, {registrationResult: cleanupResult.promise});
|
|
1870
|
+
|
|
1871
|
+
const cancelledRequest = cleanupTask.requestMidCallSummary('CONSULT');
|
|
1872
|
+
await flushEventLoopTurn();
|
|
1873
|
+
cleanupResult.reject(createAISummaryError(AI_SUMMARY_REQUEST_CANCELLED));
|
|
1874
|
+
|
|
1875
|
+
await expect(cancelledRequest).rejects.toMatchObject({
|
|
1876
|
+
message: AI_SUMMARY_REQUEST_CANCELLED,
|
|
1877
|
+
data: {errorCode: AI_SUMMARY_REQUEST_CANCELLED},
|
|
1878
|
+
});
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
it('serializes post-call responses after request cleanup', async () => {
|
|
1882
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
1883
|
+
const taskRegistry: Record<string, DummyTask> = {'interaction-1': task};
|
|
1884
|
+
const metrics = spyOnAISummaryMetrics(task);
|
|
1885
|
+
const postCallResult = createPostCallSummaryPayload();
|
|
1886
|
+
const {adapter, coordinator, result} = createRealSummaryMocks(task);
|
|
1887
|
+
|
|
1888
|
+
const postCallRequest = task.requestPostCallSummary();
|
|
1889
|
+
await flushEventLoopTurn();
|
|
1890
|
+
|
|
1891
|
+
expectSummaryGetEvent(adapter, AIAssistantEventName.GET_POST_CALL_SUMMARY);
|
|
1892
|
+
result.resolve(postCallResult);
|
|
1893
|
+
await expect(postCallRequest).resolves.toBe(postCallResult);
|
|
1894
|
+
|
|
1895
|
+
delete taskRegistry['interaction-1'];
|
|
1896
|
+
coordinator.clear('task-owner-1', 'conversation-1');
|
|
1897
|
+
const responsePayload = createPostCallResponsePayload({
|
|
1898
|
+
summary: {humanAuthoredSectionKeySentinel: 'human-authored-section-value-sentinel'} as any,
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
await expect(task.sendPostCallSummaryResponse(responsePayload)).resolves.toBeUndefined();
|
|
1902
|
+
|
|
1903
|
+
expect(taskRegistry['interaction-1']).toBeUndefined();
|
|
1904
|
+
expect(adapter.sendEvent).toHaveBeenCalledTimes(2);
|
|
1905
|
+
expect(getSummaryEventPayload(adapter, 1)).toStrictEqual({
|
|
1906
|
+
agentId: 'agent-1',
|
|
1907
|
+
interactionId: 'interaction-1',
|
|
1908
|
+
conversationId: 'conversation-1',
|
|
1909
|
+
eventName: AIAssistantEventName.POST_CALL_SUMMARY_RESPONSE,
|
|
1910
|
+
summary: {humanAuthoredSectionKeySentinel: 'human-authored-section-value-sentinel'},
|
|
1911
|
+
feedback: 'thumbs_up',
|
|
1912
|
+
wrapUpCode: 'resolved',
|
|
1913
|
+
publishTimestamp: expect.any(Number),
|
|
1914
|
+
numberOfTimesViewed: 1,
|
|
1915
|
+
numberOfTimesEdited: 0,
|
|
1916
|
+
numberOfTimesCopied: 0,
|
|
1917
|
+
state: 'DEFAULT',
|
|
1918
|
+
});
|
|
1919
|
+
expect(metrics.trackEvent).toHaveBeenCalledWith(
|
|
1920
|
+
METRIC_EVENT_NAMES.AI_SUMMARY_POST_CALL_RESPONSE_SUCCESS,
|
|
1921
|
+
expect.objectContaining({
|
|
1922
|
+
operation: METHODS.SEND_POST_CALL_SUMMARY_RESPONSE,
|
|
1923
|
+
conversationId: 'conversation-1',
|
|
1924
|
+
interactionId: 'interaction-1',
|
|
1925
|
+
}),
|
|
1926
|
+
['operational']
|
|
1927
|
+
);
|
|
1928
|
+
expect(JSON.stringify(metrics.trackEvent.mock.calls)).not.toContain(
|
|
1929
|
+
'humanAuthoredSectionKeySentinel'
|
|
1930
|
+
);
|
|
1931
|
+
expect(JSON.stringify(metrics.trackEvent.mock.calls)).not.toContain(
|
|
1932
|
+
'human-authored-section-value-sentinel'
|
|
1933
|
+
);
|
|
1934
|
+
expect(metrics.timeEvent).toHaveBeenCalledWith([
|
|
1935
|
+
METRIC_EVENT_NAMES.AI_SUMMARY_POST_CALL_RESPONSE_SUCCESS,
|
|
1936
|
+
METRIC_EVENT_NAMES.AI_SUMMARY_POST_CALL_RESPONSE_FAILED,
|
|
1937
|
+
]);
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1940
|
+
it('propagates a post-call response telemetry failure', async () => {
|
|
1941
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
1942
|
+
const metrics = spyOnAISummaryMetrics(task);
|
|
1943
|
+
const {adapter} = createSummaryMocks(task);
|
|
1944
|
+
|
|
1945
|
+
metrics.trackEvent.mockImplementationOnce(() => {
|
|
1946
|
+
throw new Error('metrics unavailable');
|
|
1947
|
+
});
|
|
1948
|
+
|
|
1949
|
+
await expect(task.sendPostCallSummaryResponse(createPostCallResponsePayload())).rejects.toThrow(
|
|
1950
|
+
'metrics unavailable'
|
|
1951
|
+
);
|
|
1952
|
+
expect(adapter.sendEvent).toHaveBeenCalledTimes(1);
|
|
1953
|
+
});
|
|
1954
|
+
|
|
1955
|
+
it('uses task IDs for direct post-call responses', async () => {
|
|
1956
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
1957
|
+
const {adapter} = createSummaryMocks(task);
|
|
1958
|
+
|
|
1959
|
+
await expect(
|
|
1960
|
+
task.sendPostCallSummaryResponse(createPostCallResponsePayload())
|
|
1961
|
+
).resolves.toBeUndefined();
|
|
1962
|
+
|
|
1963
|
+
expect(getSummaryEventPayload(adapter)).toMatchObject({
|
|
1964
|
+
interactionId: 'interaction-1',
|
|
1965
|
+
conversationId: 'conversation-1',
|
|
1966
|
+
eventName: AIAssistantEventName.POST_CALL_SUMMARY_RESPONSE,
|
|
1967
|
+
});
|
|
1968
|
+
});
|
|
1969
|
+
|
|
1970
|
+
it('serializes mid-call responses after a summary request', async () => {
|
|
1971
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
1972
|
+
const {adapter, coordinator} = createRealSummaryMocks(task);
|
|
1973
|
+
const midCallRequest = task.requestMidCallSummary('CONSULT');
|
|
1974
|
+
|
|
1975
|
+
await flushEventLoopTurn();
|
|
1976
|
+
expect(
|
|
1977
|
+
coordinator.resolveFromRtdEvent(
|
|
1978
|
+
'MID_CALL_SUMMARY',
|
|
1979
|
+
'conversation-1',
|
|
1980
|
+
createMidCallSummaryPayload()
|
|
1981
|
+
)
|
|
1982
|
+
).toBe('resolved');
|
|
1983
|
+
await expect(midCallRequest).resolves.toEqual(createMidCallSummaryPayload());
|
|
1984
|
+
|
|
1985
|
+
await expect(
|
|
1986
|
+
task.sendMidCallSummaryResponse(createMidCallResponsePayload(), 'CONSULT')
|
|
1987
|
+
).resolves.toBeUndefined();
|
|
1988
|
+
|
|
1989
|
+
expect(getSummaryEventPayload(adapter, 1)).toMatchObject({
|
|
1990
|
+
interactionId: 'interaction-1',
|
|
1991
|
+
conversationId: 'conversation-1',
|
|
1992
|
+
eventName: AIAssistantEventName.MID_CALL_CONSULT_SUMMARY_RESPONSE,
|
|
1993
|
+
});
|
|
1994
|
+
});
|
|
1995
|
+
|
|
1996
|
+
it('uses the same task IDs for mid-call and post-call responses', async () => {
|
|
1997
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
1998
|
+
const {adapter} = createSummaryMocks(task);
|
|
1999
|
+
|
|
2000
|
+
await expect(task.requestMidCallSummary('CONSULT')).resolves.toBeDefined();
|
|
2001
|
+
|
|
2002
|
+
await expect(task.requestPostCallSummary()).resolves.toBeDefined();
|
|
2003
|
+
|
|
2004
|
+
await expect(
|
|
2005
|
+
task.sendMidCallSummaryResponse(createMidCallResponsePayload(), 'CONSULT')
|
|
2006
|
+
).resolves.toBeUndefined();
|
|
2007
|
+
await expect(
|
|
2008
|
+
task.sendPostCallSummaryResponse(createPostCallResponsePayload())
|
|
2009
|
+
).resolves.toBeUndefined();
|
|
2010
|
+
|
|
2011
|
+
expect(getSummaryEventPayload(adapter, 2)).toMatchObject({
|
|
2012
|
+
interactionId: 'interaction-1',
|
|
2013
|
+
conversationId: 'conversation-1',
|
|
2014
|
+
eventName: AIAssistantEventName.MID_CALL_CONSULT_SUMMARY_RESPONSE,
|
|
2015
|
+
});
|
|
2016
|
+
expect(getSummaryEventPayload(adapter, 3)).toMatchObject({
|
|
2017
|
+
interactionId: 'interaction-1',
|
|
2018
|
+
conversationId: 'conversation-1',
|
|
2019
|
+
eventName: AIAssistantEventName.POST_CALL_SUMMARY_RESPONSE,
|
|
2020
|
+
});
|
|
2021
|
+
});
|
|
2022
|
+
|
|
2023
|
+
it('sends a post-call response after a summary request fails', async () => {
|
|
2024
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2025
|
+
const {adapter} = createSummaryMocks(task);
|
|
2026
|
+
const requestError = createAISummaryError(
|
|
2027
|
+
AI_SUMMARY_ERROR_CODES.AI_ASSISTANT_BASE_URL_NOT_AVAILABLE
|
|
2028
|
+
);
|
|
2029
|
+
|
|
2030
|
+
adapter.sendEvent.mockRejectedValueOnce(requestError);
|
|
2031
|
+
|
|
2032
|
+
await expect(task.requestPostCallSummary()).rejects.toBe(requestError);
|
|
2033
|
+
|
|
2034
|
+
await expect(
|
|
2035
|
+
task.sendPostCallSummaryResponse(createPostCallResponsePayload())
|
|
2036
|
+
).resolves.toBeUndefined();
|
|
2037
|
+
expect(getSummaryEventPayload(adapter, 1)).toMatchObject({
|
|
2038
|
+
interactionId: 'interaction-1',
|
|
2039
|
+
conversationId: 'conversation-1',
|
|
2040
|
+
});
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
it.each([
|
|
2044
|
+
{
|
|
2045
|
+
label: 'post-call',
|
|
2046
|
+
invoke: (task: DummyTask) =>
|
|
2047
|
+
task.sendPostCallSummaryResponse(
|
|
2048
|
+
createPostCallResponsePayload({
|
|
2049
|
+
summary: 'Caller reported a billing discrepancy.',
|
|
2050
|
+
numberOfTimesViewed: 2,
|
|
2051
|
+
numberOfTimesEdited: 1,
|
|
2052
|
+
numberOfTimesCopied: 3,
|
|
2053
|
+
})
|
|
2054
|
+
),
|
|
2055
|
+
expected: {
|
|
2056
|
+
agentId: 'agent-1',
|
|
2057
|
+
interactionId: 'interaction-1',
|
|
2058
|
+
conversationId: 'conversation-1',
|
|
2059
|
+
eventName: AIAssistantEventName.POST_CALL_SUMMARY_RESPONSE,
|
|
2060
|
+
summary: 'Caller reported a billing discrepancy.',
|
|
2061
|
+
feedback: 'thumbs_up',
|
|
2062
|
+
wrapUpCode: 'resolved',
|
|
2063
|
+
publishTimestamp: expect.any(Number),
|
|
2064
|
+
numberOfTimesViewed: 2,
|
|
2065
|
+
numberOfTimesEdited: 1,
|
|
2066
|
+
numberOfTimesCopied: 3,
|
|
2067
|
+
state: 'DEFAULT',
|
|
2068
|
+
},
|
|
2069
|
+
},
|
|
2070
|
+
{
|
|
2071
|
+
label: 'mid-call',
|
|
2072
|
+
invoke: (task: DummyTask) =>
|
|
2073
|
+
task.sendMidCallSummaryResponse(
|
|
2074
|
+
createMidCallResponsePayload({
|
|
2075
|
+
summary: 'Caller reported a billing discrepancy.',
|
|
2076
|
+
numberOfTimesViewed: 2,
|
|
2077
|
+
numberOfTimesEdited: 1,
|
|
2078
|
+
numberOfTimesCopied: 3,
|
|
2079
|
+
}),
|
|
2080
|
+
'TRANSFER'
|
|
2081
|
+
),
|
|
2082
|
+
expected: {
|
|
2083
|
+
agentId: 'agent-1',
|
|
2084
|
+
interactionId: 'interaction-1',
|
|
2085
|
+
conversationId: 'conversation-1',
|
|
2086
|
+
eventName: AIAssistantEventName.MID_CALL_TRANSFER_SUMMARY_RESPONSE,
|
|
2087
|
+
summary: 'Caller reported a billing discrepancy.',
|
|
2088
|
+
feedback: 'none',
|
|
2089
|
+
agentName: 'Receiving Agent',
|
|
2090
|
+
publishTimestamp: expect.any(Number),
|
|
2091
|
+
numberOfTimesViewed: 2,
|
|
2092
|
+
numberOfTimesEdited: 1,
|
|
2093
|
+
numberOfTimesCopied: 3,
|
|
2094
|
+
state: 'DEFAULT',
|
|
2095
|
+
},
|
|
2096
|
+
},
|
|
2097
|
+
])('forwards $label plain-text response summaries unchanged', async ({invoke, expected}) => {
|
|
2098
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2099
|
+
const {adapter} = createSummaryMocks(task);
|
|
2100
|
+
|
|
2101
|
+
await expect(invoke(task)).resolves.toBeUndefined();
|
|
2102
|
+
|
|
2103
|
+
expect(getSummaryEventPayload(adapter)).toStrictEqual(expected);
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
it.each([
|
|
2107
|
+
{
|
|
2108
|
+
label: 'post-call',
|
|
2109
|
+
invoke: (task: DummyTask) =>
|
|
2110
|
+
task.sendPostCallSummaryResponse(createPostCallResponsePayload()),
|
|
2111
|
+
expected: {
|
|
2112
|
+
agentId: 'agent-1',
|
|
2113
|
+
interactionId: 'interaction-1',
|
|
2114
|
+
conversationId: 'conversation-1',
|
|
2115
|
+
eventName: AIAssistantEventName.POST_CALL_SUMMARY_RESPONSE,
|
|
2116
|
+
summary: {summarySectionKeySentinel: 'summary-section-value-sentinel'},
|
|
2117
|
+
feedback: 'thumbs_up',
|
|
2118
|
+
wrapUpCode: 'resolved',
|
|
2119
|
+
numberOfTimesViewed: 1,
|
|
2120
|
+
numberOfTimesEdited: 0,
|
|
2121
|
+
numberOfTimesCopied: 0,
|
|
2122
|
+
state: 'DEFAULT',
|
|
2123
|
+
},
|
|
2124
|
+
},
|
|
2125
|
+
{
|
|
2126
|
+
label: 'mid-call',
|
|
2127
|
+
invoke: (task: DummyTask) =>
|
|
2128
|
+
task.sendMidCallSummaryResponse(createMidCallResponsePayload(), 'CONSULT'),
|
|
2129
|
+
expected: {
|
|
2130
|
+
agentId: 'agent-1',
|
|
2131
|
+
interactionId: 'interaction-1',
|
|
2132
|
+
conversationId: 'conversation-1',
|
|
2133
|
+
eventName: AIAssistantEventName.MID_CALL_CONSULT_SUMMARY_RESPONSE,
|
|
2134
|
+
summary: {midCallSectionKeySentinel: 'mid-call-section-value-sentinel'},
|
|
2135
|
+
feedback: 'none',
|
|
2136
|
+
agentName: 'Receiving Agent',
|
|
2137
|
+
numberOfTimesViewed: 1,
|
|
2138
|
+
numberOfTimesEdited: 0,
|
|
2139
|
+
numberOfTimesCopied: 0,
|
|
2140
|
+
state: 'DEFAULT',
|
|
2141
|
+
},
|
|
2142
|
+
},
|
|
2143
|
+
])('generates the $label transport timestamp inside the SDK', async ({invoke, expected}) => {
|
|
2144
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2145
|
+
const {adapter} = createSummaryMocks(task);
|
|
2146
|
+
|
|
2147
|
+
await expect(invoke(task)).resolves.toBeUndefined();
|
|
2148
|
+
|
|
2149
|
+
const transportPayload = getSummaryEventPayload(adapter);
|
|
2150
|
+
|
|
2151
|
+
expect(getSummaryEventPayload(adapter)).toMatchObject({
|
|
2152
|
+
...expected,
|
|
2153
|
+
publishTimestamp: expect.any(Number),
|
|
2154
|
+
});
|
|
2155
|
+
expect(Object.prototype.hasOwnProperty.call(transportPayload, 'actionTimeStamp')).toBe(false);
|
|
2156
|
+
expect(Object.prototype.hasOwnProperty.call(transportPayload, 'publishTimestamp')).toBe(true);
|
|
2157
|
+
});
|
|
2158
|
+
|
|
2159
|
+
it('sends the exact post-call NOT_RECEIVED response transport payload', async () => {
|
|
2160
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2161
|
+
const {adapter} = createSummaryMocks(task);
|
|
2162
|
+
|
|
2163
|
+
await expect(
|
|
2164
|
+
task.sendPostCallSummaryResponse(
|
|
2165
|
+
createPostCallResponsePayload({
|
|
2166
|
+
summary: '',
|
|
2167
|
+
feedback: 'none',
|
|
2168
|
+
state: 'NOT_RECEIVED',
|
|
2169
|
+
wrapUpCode: 'resolved',
|
|
2170
|
+
numberOfTimesViewed: 0,
|
|
2171
|
+
numberOfTimesEdited: 0,
|
|
2172
|
+
numberOfTimesCopied: 0,
|
|
2173
|
+
})
|
|
2174
|
+
)
|
|
2175
|
+
).resolves.toBeUndefined();
|
|
2176
|
+
|
|
2177
|
+
expect(getSummaryEventPayload(adapter)).toStrictEqual({
|
|
2178
|
+
agentId: 'agent-1',
|
|
2179
|
+
interactionId: 'interaction-1',
|
|
2180
|
+
conversationId: 'conversation-1',
|
|
2181
|
+
eventName: AIAssistantEventName.POST_CALL_SUMMARY_RESPONSE,
|
|
2182
|
+
summary: '',
|
|
2183
|
+
feedback: 'none',
|
|
2184
|
+
wrapUpCode: 'resolved',
|
|
2185
|
+
publishTimestamp: expect.any(Number),
|
|
2186
|
+
numberOfTimesViewed: 0,
|
|
2187
|
+
numberOfTimesEdited: 0,
|
|
2188
|
+
numberOfTimesCopied: 0,
|
|
2189
|
+
state: 'NOT_RECEIVED',
|
|
2190
|
+
});
|
|
2191
|
+
});
|
|
2192
|
+
|
|
2193
|
+
it('serializes mid-call response branches without transport-only invalid fields', async () => {
|
|
2194
|
+
const consultTask = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2195
|
+
const consultMetrics = spyOnAISummaryMetrics(consultTask);
|
|
2196
|
+
const consultMocks = createSummaryMocks(consultTask);
|
|
2197
|
+
|
|
2198
|
+
await expect(
|
|
2199
|
+
consultTask.sendMidCallSummaryResponse(
|
|
2200
|
+
createMidCallResponsePayload({
|
|
2201
|
+
state: 'MID_CALL_CANCELLED',
|
|
2202
|
+
numberOfTimesViewed: 0,
|
|
2203
|
+
numberOfTimesEdited: 0,
|
|
2204
|
+
numberOfTimesCopied: 0,
|
|
2205
|
+
}),
|
|
2206
|
+
'CONSULT'
|
|
2207
|
+
)
|
|
2208
|
+
).resolves.toBeUndefined();
|
|
2209
|
+
|
|
2210
|
+
const consultPayload = getSummaryEventPayload(consultMocks.adapter);
|
|
2211
|
+
|
|
2212
|
+
expect(consultPayload).toMatchObject({
|
|
2213
|
+
eventName: AIAssistantEventName.MID_CALL_CONSULT_SUMMARY_RESPONSE,
|
|
2214
|
+
interactionId: 'interaction-1',
|
|
2215
|
+
conversationId: 'conversation-1',
|
|
2216
|
+
agentName: 'Receiving Agent',
|
|
2217
|
+
numberOfTimesViewed: 0,
|
|
2218
|
+
publishTimestamp: expect.any(Number),
|
|
2219
|
+
});
|
|
2220
|
+
expect(consultPayload).not.toHaveProperty('summaryReceived');
|
|
2221
|
+
expect(consultPayload).not.toHaveProperty('wrapUpCode');
|
|
2222
|
+
expect(consultMetrics.trackEvent).toHaveBeenCalledWith(
|
|
2223
|
+
METRIC_EVENT_NAMES.AI_SUMMARY_MID_CALL_RESPONSE_SUCCESS,
|
|
2224
|
+
expect.objectContaining({
|
|
2225
|
+
operation: METHODS.SEND_MID_CALL_SUMMARY_RESPONSE,
|
|
2226
|
+
actionType: 'CONSULT',
|
|
2227
|
+
conversationId: 'conversation-1',
|
|
2228
|
+
interactionId: 'interaction-1',
|
|
2229
|
+
}),
|
|
2230
|
+
['operational']
|
|
2231
|
+
);
|
|
2232
|
+
|
|
2233
|
+
const transferTask = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2234
|
+
const transferMocks = createSummaryMocks(transferTask);
|
|
2235
|
+
|
|
2236
|
+
await expect(
|
|
2237
|
+
transferTask.sendMidCallSummaryResponse(
|
|
2238
|
+
createMidCallResponsePayload({
|
|
2239
|
+
summaryReceived: false,
|
|
2240
|
+
summary: '',
|
|
2241
|
+
feedback: 'none',
|
|
2242
|
+
state: 'NOT_RECEIVED',
|
|
2243
|
+
numberOfTimesViewed: 0,
|
|
2244
|
+
numberOfTimesEdited: 0,
|
|
2245
|
+
numberOfTimesCopied: 0,
|
|
2246
|
+
}),
|
|
2247
|
+
'TRANSFER'
|
|
2248
|
+
)
|
|
2249
|
+
).resolves.toBeUndefined();
|
|
2250
|
+
|
|
2251
|
+
expect(getSummaryEventPayload(transferMocks.adapter)).toMatchObject({
|
|
2252
|
+
eventName: AIAssistantEventName.MID_CALL_TRANSFER_SUMMARY_RESPONSE,
|
|
2253
|
+
interactionId: 'interaction-1',
|
|
2254
|
+
conversationId: 'conversation-1',
|
|
2255
|
+
summary: '',
|
|
2256
|
+
numberOfTimesViewed: 0,
|
|
2257
|
+
});
|
|
2258
|
+
});
|
|
2259
|
+
|
|
2260
|
+
it.each([
|
|
2261
|
+
{
|
|
2262
|
+
label: 'post-call received',
|
|
2263
|
+
invoke: (task: DummyTask) =>
|
|
2264
|
+
task.sendPostCallSummaryResponse(createPostCallResponsePayload()),
|
|
2265
|
+
eventName: AIAssistantEventName.POST_CALL_SUMMARY_RESPONSE,
|
|
2266
|
+
expected: {wrapUpCode: 'resolved', state: 'DEFAULT'},
|
|
2267
|
+
absent: ['agentName', 'summaryReceived'] as const,
|
|
2268
|
+
},
|
|
2269
|
+
{
|
|
2270
|
+
label: 'post-call NOT_RECEIVED',
|
|
2271
|
+
invoke: (task: DummyTask) =>
|
|
2272
|
+
task.sendPostCallSummaryResponse(
|
|
2273
|
+
createPostCallResponsePayload({
|
|
2274
|
+
summary: '',
|
|
2275
|
+
state: 'NOT_RECEIVED',
|
|
2276
|
+
numberOfTimesViewed: 0,
|
|
2277
|
+
numberOfTimesEdited: 0,
|
|
2278
|
+
numberOfTimesCopied: 0,
|
|
2279
|
+
})
|
|
2280
|
+
),
|
|
2281
|
+
eventName: AIAssistantEventName.POST_CALL_SUMMARY_RESPONSE,
|
|
2282
|
+
expected: {wrapUpCode: 'resolved', summary: '', state: 'NOT_RECEIVED'},
|
|
2283
|
+
absent: ['agentName', 'summaryReceived'] as const,
|
|
2284
|
+
},
|
|
2285
|
+
{
|
|
2286
|
+
label: 'mid-call received',
|
|
2287
|
+
invoke: (task: DummyTask) =>
|
|
2288
|
+
task.sendMidCallSummaryResponse(createMidCallResponsePayload(), 'CONSULT'),
|
|
2289
|
+
eventName: AIAssistantEventName.MID_CALL_CONSULT_SUMMARY_RESPONSE,
|
|
2290
|
+
expected: {agentName: 'Receiving Agent', state: 'DEFAULT'},
|
|
2291
|
+
absent: ['wrapUpCode', 'summaryReceived'] as const,
|
|
2292
|
+
},
|
|
2293
|
+
{
|
|
2294
|
+
label: 'mid-call summaryReceived:false',
|
|
2295
|
+
invoke: (task: DummyTask) =>
|
|
2296
|
+
task.sendMidCallSummaryResponse(
|
|
2297
|
+
createMidCallResponsePayload({
|
|
2298
|
+
summaryReceived: false,
|
|
2299
|
+
summary: '',
|
|
2300
|
+
state: 'NOT_RECEIVED',
|
|
2301
|
+
numberOfTimesViewed: 0,
|
|
2302
|
+
numberOfTimesEdited: 0,
|
|
2303
|
+
numberOfTimesCopied: 0,
|
|
2304
|
+
}),
|
|
2305
|
+
'TRANSFER'
|
|
2306
|
+
),
|
|
2307
|
+
eventName: AIAssistantEventName.MID_CALL_TRANSFER_SUMMARY_RESPONSE,
|
|
2308
|
+
expected: {agentName: 'Receiving Agent', summary: '', state: 'NOT_RECEIVED'},
|
|
2309
|
+
absent: ['wrapUpCode', 'summaryReceived'] as const,
|
|
2310
|
+
},
|
|
2311
|
+
])(
|
|
2312
|
+
'populates both identifiers on $label response transport',
|
|
2313
|
+
async ({invoke, eventName, expected, absent}) => {
|
|
2314
|
+
const task = new DummyTask(
|
|
2315
|
+
dummyContact,
|
|
2316
|
+
createAISummaryTaskData({
|
|
2317
|
+
interactionId: 'response-interaction-1',
|
|
2318
|
+
interaction: {mainInteractionId: 'response-conversation-1'} as any,
|
|
2319
|
+
})
|
|
2320
|
+
);
|
|
2321
|
+
const {adapter} = createSummaryMocks(task);
|
|
2322
|
+
|
|
2323
|
+
await expect(invoke(task)).resolves.toBeUndefined();
|
|
2324
|
+
|
|
2325
|
+
expect(adapter.sendEvent).toHaveBeenCalledTimes(1);
|
|
2326
|
+
const transportPayload = getSummaryEventPayload(adapter);
|
|
2327
|
+
|
|
2328
|
+
expect(transportPayload.agentId).toBe('agent-1');
|
|
2329
|
+
expect(transportPayload).toMatchObject({
|
|
2330
|
+
agentId: 'agent-1',
|
|
2331
|
+
interactionId: 'response-interaction-1',
|
|
2332
|
+
conversationId: 'response-conversation-1',
|
|
2333
|
+
eventName,
|
|
2334
|
+
...expected,
|
|
2335
|
+
});
|
|
2336
|
+
absent.forEach((propertyName) => {
|
|
2337
|
+
expect(transportPayload).not.toHaveProperty(propertyName);
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
);
|
|
2341
|
+
|
|
2342
|
+
it.each(['CONSULT', 'TRANSFER'] as const)(
|
|
2343
|
+
'sends a successful summary-not-received MID_CALL_CANCELLED %s response',
|
|
2344
|
+
async (actionType) => {
|
|
2345
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2346
|
+
const {adapter} = createSummaryMocks(task);
|
|
2347
|
+
|
|
2348
|
+
await expect(
|
|
2349
|
+
task.sendMidCallSummaryResponse(
|
|
2350
|
+
createMidCallResponsePayload({
|
|
2351
|
+
summaryReceived: false,
|
|
2352
|
+
summary: '',
|
|
2353
|
+
feedback: 'none',
|
|
2354
|
+
state: 'MID_CALL_CANCELLED',
|
|
2355
|
+
numberOfTimesViewed: 0,
|
|
2356
|
+
numberOfTimesEdited: 0,
|
|
2357
|
+
numberOfTimesCopied: 0,
|
|
2358
|
+
}),
|
|
2359
|
+
actionType
|
|
2360
|
+
)
|
|
2361
|
+
).resolves.toBeUndefined();
|
|
2362
|
+
|
|
2363
|
+
expect(getSummaryEventPayload(adapter)).toMatchObject({
|
|
2364
|
+
eventName:
|
|
2365
|
+
actionType === 'CONSULT'
|
|
2366
|
+
? AIAssistantEventName.MID_CALL_CONSULT_SUMMARY_RESPONSE
|
|
2367
|
+
: AIAssistantEventName.MID_CALL_TRANSFER_SUMMARY_RESPONSE,
|
|
2368
|
+
summary: '',
|
|
2369
|
+
state: 'MID_CALL_CANCELLED',
|
|
2370
|
+
numberOfTimesViewed: 0,
|
|
2371
|
+
numberOfTimesEdited: 0,
|
|
2372
|
+
numberOfTimesCopied: 0,
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
);
|
|
2376
|
+
|
|
2377
|
+
it('preserves a received MID_CALL_CANCELLED summary and viewed counter', async () => {
|
|
2378
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2379
|
+
const {adapter} = createSummaryMocks(task);
|
|
2380
|
+
const summary = {
|
|
2381
|
+
reasonForTransferOrConsult: 'Customer requested a specialist',
|
|
2382
|
+
issueResolution: 'Warm transfer completed',
|
|
2383
|
+
};
|
|
2384
|
+
|
|
2385
|
+
await expect(
|
|
2386
|
+
task.sendMidCallSummaryResponse(
|
|
2387
|
+
createMidCallResponsePayload({
|
|
2388
|
+
summaryReceived: true,
|
|
2389
|
+
summary,
|
|
2390
|
+
state: 'MID_CALL_CANCELLED',
|
|
2391
|
+
numberOfTimesViewed: 1,
|
|
2392
|
+
numberOfTimesEdited: 0,
|
|
2393
|
+
numberOfTimesCopied: 0,
|
|
2394
|
+
}),
|
|
2395
|
+
'CONSULT'
|
|
2396
|
+
)
|
|
2397
|
+
).resolves.toBeUndefined();
|
|
2398
|
+
|
|
2399
|
+
expect(getSummaryEventPayload(adapter)).toMatchObject({
|
|
2400
|
+
summary,
|
|
2401
|
+
state: 'MID_CALL_CANCELLED',
|
|
2402
|
+
numberOfTimesViewed: 1,
|
|
2403
|
+
});
|
|
2404
|
+
});
|
|
2405
|
+
|
|
2406
|
+
it.each([
|
|
2407
|
+
{
|
|
2408
|
+
actionType: 'CONSULT' as AISummaryAction,
|
|
2409
|
+
eventName: AIAssistantEventName.MID_CALL_CONSULT_SUMMARY_RESPONSE,
|
|
2410
|
+
},
|
|
2411
|
+
{
|
|
2412
|
+
actionType: 'TRANSFER' as AISummaryAction,
|
|
2413
|
+
eventName: AIAssistantEventName.MID_CALL_TRANSFER_SUMMARY_RESPONSE,
|
|
2414
|
+
},
|
|
2415
|
+
])(
|
|
2416
|
+
'sends MID_CALL_CANCELLED $actionType responses without invoking a handoff',
|
|
2417
|
+
async ({actionType, eventName}) => {
|
|
2418
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2419
|
+
const {adapter} = createSummaryMocks(task);
|
|
2420
|
+
const consultSpy = jest.spyOn(task, 'consult').mockResolvedValue({} as any);
|
|
2421
|
+
const transferSpy = jest.spyOn(task, 'transfer').mockResolvedValue({} as any);
|
|
2422
|
+
const sendResponseThenHandoff = async (
|
|
2423
|
+
payload: AISummaryResponse,
|
|
2424
|
+
handoffActionType: AISummaryAction
|
|
2425
|
+
) => {
|
|
2426
|
+
await task.sendMidCallSummaryResponse(payload, handoffActionType);
|
|
2427
|
+
|
|
2428
|
+
if (payload.state === 'MID_CALL_CANCELLED') {
|
|
2429
|
+
return;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
if (handoffActionType === 'CONSULT') {
|
|
2433
|
+
await task.consult({} as any);
|
|
2434
|
+
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
await task.transfer({} as any);
|
|
2439
|
+
};
|
|
2440
|
+
|
|
2441
|
+
await expect(
|
|
2442
|
+
sendResponseThenHandoff(
|
|
2443
|
+
createMidCallResponsePayload({
|
|
2444
|
+
state: 'MID_CALL_CANCELLED',
|
|
2445
|
+
numberOfTimesViewed: 0,
|
|
2446
|
+
numberOfTimesEdited: 0,
|
|
2447
|
+
numberOfTimesCopied: 0,
|
|
2448
|
+
}),
|
|
2449
|
+
actionType
|
|
2450
|
+
)
|
|
2451
|
+
).resolves.toBeUndefined();
|
|
2452
|
+
|
|
2453
|
+
expect(getSummaryEventPayload(adapter)).toMatchObject({
|
|
2454
|
+
eventName,
|
|
2455
|
+
state: 'MID_CALL_CANCELLED',
|
|
2456
|
+
});
|
|
2457
|
+
expect(consultSpy).not.toHaveBeenCalled();
|
|
2458
|
+
expect(transferSpy).not.toHaveBeenCalled();
|
|
2459
|
+
}
|
|
2460
|
+
);
|
|
2461
|
+
|
|
2462
|
+
it.each([
|
|
2463
|
+
['post-call IGNORED', 'post-call', 'IGNORED'],
|
|
2464
|
+
['mid-call EXCLUDED', 'mid-call', 'EXCLUDED'],
|
|
2465
|
+
['mid-call IGNORED', 'mid-call', 'IGNORED'],
|
|
2466
|
+
] as const)('accepts %s response states', async (_label, flow, state) => {
|
|
2467
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2468
|
+
const {adapter} = createSummaryMocks(task);
|
|
2469
|
+
|
|
2470
|
+
if (flow === 'post-call') {
|
|
2471
|
+
await expect(
|
|
2472
|
+
task.sendPostCallSummaryResponse(createPostCallResponsePayload({state: state as any}))
|
|
2473
|
+
).resolves.toBeUndefined();
|
|
2474
|
+
} else {
|
|
2475
|
+
await expect(
|
|
2476
|
+
task.sendMidCallSummaryResponse(
|
|
2477
|
+
createMidCallResponsePayload({state: state as any}),
|
|
2478
|
+
'CONSULT'
|
|
2479
|
+
)
|
|
2480
|
+
).resolves.toBeUndefined();
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
expect(getSummaryEventPayload(adapter)).toMatchObject({state});
|
|
2484
|
+
});
|
|
2485
|
+
|
|
2486
|
+
it('accepts mid-call IGNORED on the unavailable branch (summaryReceived:false)', async () => {
|
|
2487
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2488
|
+
const {adapter} = createSummaryMocks(task);
|
|
2489
|
+
|
|
2490
|
+
await expect(
|
|
2491
|
+
task.sendMidCallSummaryResponse(
|
|
2492
|
+
{
|
|
2493
|
+
summaryReceived: false,
|
|
2494
|
+
summary: '',
|
|
2495
|
+
numberOfTimesViewed: 0,
|
|
2496
|
+
numberOfTimesEdited: 0,
|
|
2497
|
+
numberOfTimesCopied: 0,
|
|
2498
|
+
feedback: 'none',
|
|
2499
|
+
state: 'IGNORED',
|
|
2500
|
+
},
|
|
2501
|
+
'CONSULT'
|
|
2502
|
+
)
|
|
2503
|
+
).resolves.toBeUndefined();
|
|
2504
|
+
|
|
2505
|
+
expect(getSummaryEventPayload(adapter)).toMatchObject({state: 'IGNORED'});
|
|
2506
|
+
});
|
|
2507
|
+
|
|
2508
|
+
it.each(['none', 'thumbs_up', 'thumbs_down'] as const)(
|
|
2509
|
+
'forwards valid %s feedback unchanged for both response flows',
|
|
2510
|
+
async (feedback) => {
|
|
2511
|
+
const postTask = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2512
|
+
const postMocks = createSummaryMocks(postTask);
|
|
2513
|
+
const midTask = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2514
|
+
const midMocks = createSummaryMocks(midTask);
|
|
2515
|
+
|
|
2516
|
+
await expect(
|
|
2517
|
+
postTask.sendPostCallSummaryResponse(createPostCallResponsePayload({feedback}))
|
|
2518
|
+
).resolves.toBeUndefined();
|
|
2519
|
+
await expect(
|
|
2520
|
+
midTask.sendMidCallSummaryResponse(createMidCallResponsePayload({feedback}), 'CONSULT')
|
|
2521
|
+
).resolves.toBeUndefined();
|
|
2522
|
+
|
|
2523
|
+
expect(getSummaryEventPayload(postMocks.adapter)).toMatchObject({feedback});
|
|
2524
|
+
expect(getSummaryEventPayload(midMocks.adapter)).toMatchObject({feedback});
|
|
2525
|
+
}
|
|
2526
|
+
);
|
|
2527
|
+
|
|
2528
|
+
it('forwards valid non-zero counters unchanged for both response flows', async () => {
|
|
2529
|
+
const counters = {
|
|
2530
|
+
numberOfTimesViewed: 2,
|
|
2531
|
+
numberOfTimesEdited: 1,
|
|
2532
|
+
numberOfTimesCopied: 3,
|
|
2533
|
+
};
|
|
2534
|
+
const postTask = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2535
|
+
const metrics = spyOnAISummaryMetrics(postTask);
|
|
2536
|
+
const postMocks = createSummaryMocks(postTask);
|
|
2537
|
+
const midTask = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2538
|
+
const midMocks = createSummaryMocks(midTask);
|
|
2539
|
+
|
|
2540
|
+
await expect(
|
|
2541
|
+
postTask.sendPostCallSummaryResponse(createPostCallResponsePayload(counters))
|
|
2542
|
+
).resolves.toBeUndefined();
|
|
2543
|
+
await expect(
|
|
2544
|
+
midTask.sendMidCallSummaryResponse(createMidCallResponsePayload(counters), 'TRANSFER')
|
|
2545
|
+
).resolves.toBeUndefined();
|
|
2546
|
+
|
|
2547
|
+
expect(getSummaryEventPayload(postMocks.adapter)).toMatchObject(counters);
|
|
2548
|
+
expect(getSummaryEventPayload(midMocks.adapter)).toMatchObject(counters);
|
|
2549
|
+
expect(metrics.trackEvent).toHaveBeenCalledWith(
|
|
2550
|
+
METRIC_EVENT_NAMES.AI_SUMMARY_POST_CALL_RESPONSE_SUCCESS,
|
|
2551
|
+
expect.objectContaining({operation: METHODS.SEND_POST_CALL_SUMMARY_RESPONSE}),
|
|
2552
|
+
['operational']
|
|
2553
|
+
);
|
|
2554
|
+
expect(metrics.trackEvent).toHaveBeenCalledWith(
|
|
2555
|
+
METRIC_EVENT_NAMES.AI_SUMMARY_MID_CALL_RESPONSE_SUCCESS,
|
|
2556
|
+
expect.objectContaining({
|
|
2557
|
+
operation: METHODS.SEND_MID_CALL_SUMMARY_RESPONSE,
|
|
2558
|
+
actionType: 'TRANSFER',
|
|
2559
|
+
}),
|
|
2560
|
+
['operational']
|
|
2561
|
+
);
|
|
2562
|
+
});
|
|
2563
|
+
|
|
2564
|
+
it('maps plain adapter errors to a bounded failure code and preserves rejection identity', async () => {
|
|
2565
|
+
const task = new DummyTask(dummyContact, createAISummaryTaskData());
|
|
2566
|
+
const metrics = spyOnAISummaryMetrics(task);
|
|
2567
|
+
const {adapter} = createSummaryMocks(task);
|
|
2568
|
+
const adapterError = new Error('summary-section-value-sentinel');
|
|
2569
|
+
|
|
2570
|
+
adapter.sendEvent.mockRejectedValue(adapterError);
|
|
2571
|
+
|
|
2572
|
+
await expect(
|
|
2573
|
+
task.sendMidCallSummaryResponse(createMidCallResponsePayload(), 'CONSULT')
|
|
2574
|
+
).rejects.toBe(adapterError);
|
|
2575
|
+
|
|
2576
|
+
expect(JSON.stringify(metrics.trackEvent.mock.calls)).not.toContain(
|
|
2577
|
+
'summary-section-value-sentinel'
|
|
2578
|
+
);
|
|
2579
|
+
expect(metrics.trackEvent).toHaveBeenCalledWith(
|
|
2580
|
+
METRIC_EVENT_NAMES.AI_SUMMARY_MID_CALL_RESPONSE_FAILED,
|
|
2581
|
+
expect.objectContaining({
|
|
2582
|
+
operation: METHODS.SEND_MID_CALL_SUMMARY_RESPONSE,
|
|
2583
|
+
actionType: 'CONSULT',
|
|
2584
|
+
failureCode: 'AI_SUMMARY_INVALID_RESPONSE_PAYLOAD',
|
|
2585
|
+
}),
|
|
2586
|
+
['operational']
|
|
2587
|
+
);
|
|
2588
|
+
});
|
|
2589
|
+
|
|
2590
|
+
it.each([
|
|
2591
|
+
{
|
|
2592
|
+
label: 'summary base URL unavailable',
|
|
2593
|
+
expectedCode: AI_SUMMARY_ERROR_CODES.AI_ASSISTANT_BASE_URL_NOT_AVAILABLE,
|
|
2594
|
+
configure: true,
|
|
2595
|
+
payload: createPostCallResponsePayload(),
|
|
2596
|
+
adapterError: createAISummaryError(
|
|
2597
|
+
AI_SUMMARY_ERROR_CODES.AI_ASSISTANT_BASE_URL_NOT_AVAILABLE
|
|
2598
|
+
),
|
|
2599
|
+
expectAdapterCall: true,
|
|
2600
|
+
},
|
|
2601
|
+
...[
|
|
2602
|
+
AI_SUMMARY_TRANSPORT_ERROR_CODES.VALIDATION_FAILED,
|
|
2603
|
+
AI_SUMMARY_TRANSPORT_ERROR_CODES.HTTP_REQUEST_FAILED,
|
|
2604
|
+
AI_SUMMARY_TRANSPORT_ERROR_CODES.TIMEOUT,
|
|
2605
|
+
].map((expectedCode) => ({
|
|
2606
|
+
label: expectedCode,
|
|
2607
|
+
expectedCode,
|
|
2608
|
+
configure: true,
|
|
2609
|
+
payload: createPostCallResponsePayload(),
|
|
2610
|
+
adapterError: createAISummaryError(expectedCode),
|
|
2611
|
+
expectAdapterCall: true,
|
|
2612
|
+
})),
|
|
2613
|
+
])(
|
|
2614
|
+
'records bounded post-call response failure metric for $label',
|
|
2615
|
+
async ({expectedCode, configure, taskData, payload, adapterError, expectAdapterCall}) => {
|
|
2616
|
+
const responseTask = new DummyTask(dummyContact, taskData ?? createAISummaryTaskData());
|
|
2617
|
+
const responseMetrics = spyOnAISummaryMetrics(responseTask);
|
|
2618
|
+
const summaryMocks = configure ? createSummaryMocks(responseTask) : undefined;
|
|
2619
|
+
|
|
2620
|
+
if (adapterError) {
|
|
2621
|
+
summaryMocks?.adapter.sendEvent.mockRejectedValue(adapterError);
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
const result = responseTask.sendPostCallSummaryResponse(payload);
|
|
2625
|
+
|
|
2626
|
+
if (adapterError) {
|
|
2627
|
+
await expect(result).rejects.toBe(adapterError);
|
|
2628
|
+
} else {
|
|
2629
|
+
await expect(result).rejects.toMatchObject(createAISummaryErrorExpectation(expectedCode));
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
if (summaryMocks) {
|
|
2633
|
+
if (expectAdapterCall) {
|
|
2634
|
+
expect(summaryMocks.adapter.sendEvent).toHaveBeenCalledTimes(1);
|
|
2635
|
+
} else {
|
|
2636
|
+
expect(summaryMocks.adapter.sendEvent).not.toHaveBeenCalled();
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
expect(responseMetrics.trackEvent).toHaveBeenCalledWith(
|
|
2640
|
+
METRIC_EVENT_NAMES.AI_SUMMARY_POST_CALL_RESPONSE_FAILED,
|
|
2641
|
+
expect.objectContaining({
|
|
2642
|
+
operation: METHODS.SEND_POST_CALL_SUMMARY_RESPONSE,
|
|
2643
|
+
failureCode: expectedCode,
|
|
2644
|
+
}),
|
|
2645
|
+
['operational']
|
|
2646
|
+
);
|
|
2647
|
+
expect(responseMetrics.timeEvent).toHaveBeenCalled();
|
|
2648
|
+
}
|
|
2649
|
+
);
|
|
2650
|
+
|
|
2651
|
+
it.each([
|
|
2652
|
+
{
|
|
2653
|
+
label: 'summary base URL unavailable',
|
|
2654
|
+
expectedCode: AI_SUMMARY_ERROR_CODES.AI_ASSISTANT_BASE_URL_NOT_AVAILABLE,
|
|
2655
|
+
configure: true,
|
|
2656
|
+
adapterError: createAISummaryError(
|
|
2657
|
+
AI_SUMMARY_ERROR_CODES.AI_ASSISTANT_BASE_URL_NOT_AVAILABLE
|
|
2658
|
+
),
|
|
2659
|
+
taskData: undefined,
|
|
2660
|
+
expectAdapterCall: true,
|
|
2661
|
+
includeActionType: true,
|
|
2662
|
+
},
|
|
2663
|
+
...[
|
|
2664
|
+
AI_SUMMARY_TRANSPORT_ERROR_CODES.VALIDATION_FAILED,
|
|
2665
|
+
AI_SUMMARY_TRANSPORT_ERROR_CODES.HTTP_REQUEST_FAILED,
|
|
2666
|
+
AI_SUMMARY_TRANSPORT_ERROR_CODES.TIMEOUT,
|
|
2667
|
+
].map((expectedCode) => ({
|
|
2668
|
+
label: expectedCode,
|
|
2669
|
+
expectedCode,
|
|
2670
|
+
configure: true,
|
|
2671
|
+
adapterError: createAISummaryError(expectedCode),
|
|
2672
|
+
taskData: undefined,
|
|
2673
|
+
expectAdapterCall: true,
|
|
2674
|
+
includeActionType: true,
|
|
2675
|
+
})),
|
|
2676
|
+
])(
|
|
2677
|
+
'records bounded mid-call response failure metric for $label',
|
|
2678
|
+
async ({
|
|
2679
|
+
expectedCode,
|
|
2680
|
+
configure,
|
|
2681
|
+
taskData,
|
|
2682
|
+
adapterError,
|
|
2683
|
+
expectAdapterCall,
|
|
2684
|
+
}) => {
|
|
2685
|
+
const responseTask = new DummyTask(dummyContact, taskData ?? createAISummaryTaskData());
|
|
2686
|
+
const responseMetrics = spyOnAISummaryMetrics(responseTask);
|
|
2687
|
+
const summaryMocks = configure ? createSummaryMocks(responseTask) : undefined;
|
|
2688
|
+
|
|
2689
|
+
if (adapterError) {
|
|
2690
|
+
summaryMocks?.adapter.sendEvent.mockRejectedValue(adapterError);
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
const result = responseTask.sendMidCallSummaryResponse(
|
|
2694
|
+
createMidCallResponsePayload(),
|
|
2695
|
+
'CONSULT'
|
|
2696
|
+
);
|
|
2697
|
+
|
|
2698
|
+
if (adapterError) {
|
|
2699
|
+
await expect(result).rejects.toBe(adapterError);
|
|
2700
|
+
} else {
|
|
2701
|
+
await expect(result).rejects.toMatchObject(createAISummaryErrorExpectation(expectedCode));
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
if (summaryMocks) {
|
|
2705
|
+
if (expectAdapterCall) {
|
|
2706
|
+
expect(summaryMocks.adapter.sendEvent).toHaveBeenCalledTimes(1);
|
|
2707
|
+
} else {
|
|
2708
|
+
expect(summaryMocks.adapter.sendEvent).not.toHaveBeenCalled();
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
expect(responseMetrics.trackEvent).toHaveBeenCalledWith(
|
|
2712
|
+
METRIC_EVENT_NAMES.AI_SUMMARY_MID_CALL_RESPONSE_FAILED,
|
|
2713
|
+
expect.objectContaining({
|
|
2714
|
+
operation: METHODS.SEND_MID_CALL_SUMMARY_RESPONSE,
|
|
2715
|
+
actionType: 'CONSULT',
|
|
2716
|
+
failureCode: expectedCode,
|
|
2717
|
+
}),
|
|
2718
|
+
['operational']
|
|
2719
|
+
);
|
|
2720
|
+
expect(responseMetrics.timeEvent).toHaveBeenCalled();
|
|
2721
|
+
}
|
|
2722
|
+
);
|
|
2723
|
+
});
|