@webex/contact-center 3.12.0-llmrefactor.12 → 3.12.0-llmrefactor.14
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 +6 -5
- package/ai-docs/CONTRACTS.md +8 -0
- package/ai-docs/SERVICE_STATE.md +4 -0
- package/ai-docs/contact-center-spec.md +26 -3
- package/ai-docs/features/contact-owner-change-refactor.md +78 -0
- package/dist/cc.js +202 -9
- package/dist/cc.js.map +1 -1
- package/dist/constants.js +5 -1
- package/dist/constants.js.map +1 -1
- package/dist/index.js +25 -10
- package/dist/index.js.map +1 -1
- package/dist/metrics/behavioral-events.js +6 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +6 -0
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/AnswerCallOnWebexService.js +4 -4
- package/dist/services/AnswerCallOnWebexService.js.map +1 -1
- package/dist/services/ApiAiAssistant.js +116 -1
- package/dist/services/ApiAiAssistant.js.map +1 -1
- package/dist/services/WebexCrossClientService.js +5 -2
- package/dist/services/WebexCrossClientService.js.map +1 -1
- package/dist/services/WxAppTelephonyMercurySync.js +6 -3
- package/dist/services/WxAppTelephonyMercurySync.js.map +1 -1
- package/dist/services/config/Util.js +5 -1
- package/dist/services/config/Util.js.map +1 -1
- package/dist/services/config/constants.js +10 -1
- package/dist/services/config/constants.js.map +1 -1
- package/dist/services/config/index.js +51 -0
- package/dist/services/config/index.js.map +1 -1
- package/dist/services/config/types.js +4 -3
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/core/Utils.js +5 -1
- package/dist/services/core/Utils.js.map +1 -1
- package/dist/services/task/TaskFactory.js +1 -0
- package/dist/services/task/TaskFactory.js.map +1 -1
- package/dist/services/task/TaskManager.js +50 -58
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +63 -44
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/services/task/voice/wxAppOfferObservability.js +145 -0
- package/dist/services/task/voice/wxAppOfferObservability.js.map +1 -0
- package/dist/services/task/voice/wxAppVoiceMethods.js +52 -11
- package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -1
- package/dist/services/wxAppDiagnosticLogging.js +50 -13
- package/dist/services/wxAppDiagnosticLogging.js.map +1 -1
- package/dist/types/cc.d.ts +21 -1
- package/dist/types/constants.d.ts +4 -0
- package/dist/types/index.d.ts +11 -1
- package/dist/types/metrics/constants.d.ts +6 -0
- package/dist/types/services/ApiAiAssistant.d.ts +35 -2
- package/dist/types/services/config/constants.d.ts +5 -0
- package/dist/types/services/config/types.d.ts +15 -2
- package/dist/types/services/core/Utils.d.ts +2 -0
- package/dist/types/services/task/types.d.ts +2 -0
- package/dist/types/services/task/voice/Voice.d.ts +15 -2
- package/dist/types/services/task/voice/wxAppOfferObservability.d.ts +39 -0
- package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +19 -6
- package/dist/types/services/wxAppDiagnosticLogging.d.ts +12 -0
- package/dist/types/types.d.ts +56 -1
- package/dist/types.js +39 -2
- package/dist/types.js.map +1 -1
- package/dist/webex.js +9 -1
- package/dist/webex.js.map +1 -1
- package/package.json +9 -9
- package/src/cc.ts +294 -25
- package/src/constants.ts +4 -0
- package/src/index.ts +11 -0
- package/src/metrics/ai-docs/metrics-spec.md +61 -20
- package/src/metrics/behavioral-events.ts +6 -0
- package/src/metrics/constants.ts +6 -0
- package/src/services/AnswerCallOnWebexService.ts +4 -4
- package/src/services/ApiAiAssistant.ts +153 -1
- package/src/services/WebexCrossClientService.ts +7 -2
- package/src/services/WxAppTelephonyMercurySync.ts +3 -3
- package/src/services/ai-docs/services-spec.md +11 -3
- package/src/services/config/Util.ts +6 -0
- package/src/services/config/ai-docs/config-spec.md +14 -3
- package/src/services/config/constants.ts +12 -0
- package/src/services/config/index.ts +65 -0
- package/src/services/config/types.ts +13 -2
- package/src/services/core/Utils.ts +4 -0
- package/src/services/task/TaskFactory.ts +1 -0
- package/src/services/task/TaskManager.ts +85 -87
- package/src/services/task/ai-docs/task-spec.md +7 -3
- package/src/services/task/types.ts +16 -0
- package/src/services/task/voice/Voice.ts +74 -52
- package/src/services/task/voice/wxAppOfferObservability.ts +204 -0
- package/src/services/task/voice/wxAppVoiceMethods.ts +83 -24
- package/src/services/wxAppDiagnosticLogging.ts +135 -23
- package/src/types.ts +65 -0
- package/src/webex.js +8 -0
- package/test/unit/spec/cc.ts +469 -19
- package/test/unit/spec/services/ApiAiAssistant.ts +134 -2
- package/test/unit/spec/services/WebexCrossClientService.ts +45 -3
- package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +1 -1
- package/test/unit/spec/services/config/Util.ts +44 -0
- package/test/unit/spec/services/config/index.ts +68 -0
- package/test/unit/spec/services/task/TaskManager.ts +214 -3
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +33 -0
- package/test/unit/spec/services/task/voice/Voice.ts +10 -51
- package/test/unit/spec/services/task/voice/wxAppOfferObservability.ts +361 -0
- package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +121 -5
- package/test/unit/spec/services/wxAppDiagnosticLogging.ts +44 -8
- package/test/unit/spec/wellnessSampleUtils.ts +288 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -2,7 +2,12 @@ import ApiAIAssistant from '../../../../src/services/ApiAiAssistant';
|
|
|
2
2
|
import MetricsManager from '../../../../src/metrics/MetricsManager';
|
|
3
3
|
import LoggerProxy from '../../../../src/logger-proxy';
|
|
4
4
|
import WebexRequest from '../../../../src/services/core/WebexRequest';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
HTTP_METHODS,
|
|
7
|
+
RealTimeAssistanceUserActionId,
|
|
8
|
+
WELLNESS_BREAK_USER_ACTIONS,
|
|
9
|
+
WebexSDK,
|
|
10
|
+
} from '../../../../src/types';
|
|
6
11
|
|
|
7
12
|
jest.mock('../../../../src/metrics/MetricsManager');
|
|
8
13
|
jest.mock('../../../../src/logger-proxy');
|
|
@@ -11,6 +16,11 @@ describe('ApiAIAssistant', () => {
|
|
|
11
16
|
let apiAIAssistant: ApiAIAssistant;
|
|
12
17
|
let mockWebex: WebexSDK;
|
|
13
18
|
let mockMetricsManager: jest.Mocked<MetricsManager>;
|
|
19
|
+
let wellnessContext: {
|
|
20
|
+
isWellnessBreakEnabled: boolean;
|
|
21
|
+
agentId?: string;
|
|
22
|
+
agentSessionId?: string;
|
|
23
|
+
};
|
|
14
24
|
|
|
15
25
|
beforeEach(() => {
|
|
16
26
|
jest.clearAllMocks();
|
|
@@ -43,7 +53,8 @@ describe('ApiAIAssistant', () => {
|
|
|
43
53
|
} as unknown as jest.Mocked<MetricsManager>;
|
|
44
54
|
(MetricsManager.getInstance as jest.Mock).mockReturnValue(mockMetricsManager);
|
|
45
55
|
|
|
46
|
-
|
|
56
|
+
wellnessContext = {isWellnessBreakEnabled: false};
|
|
57
|
+
apiAIAssistant = new ApiAIAssistant(mockWebex, () => wellnessContext);
|
|
47
58
|
});
|
|
48
59
|
|
|
49
60
|
it('should send transcript start event successfully', async () => {
|
|
@@ -270,4 +281,125 @@ describe('ApiAIAssistant', () => {
|
|
|
270
281
|
|
|
271
282
|
expect(errorMessage).toBe('Error while performing sendRealTimeAssistanceUserAction');
|
|
272
283
|
});
|
|
284
|
+
|
|
285
|
+
describe('Agent Wellness Break actions', () => {
|
|
286
|
+
beforeEach(() => {
|
|
287
|
+
wellnessContext = {
|
|
288
|
+
isWellnessBreakEnabled: true,
|
|
289
|
+
agentId: 'test-agent-id',
|
|
290
|
+
agentSessionId: 'session-1',
|
|
291
|
+
};
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('does not expose wellness context mutation on the public assistant instance', () => {
|
|
295
|
+
expect(apiAIAssistant).not.toHaveProperty('setWellnessContext');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('sends the exact REQUESTED body and resolves only for HTTP 202', async () => {
|
|
299
|
+
jest.spyOn(Date, 'now').mockReturnValue(1787640000000);
|
|
300
|
+
(mockWebex.request as jest.Mock).mockResolvedValue({statusCode: 202});
|
|
301
|
+
|
|
302
|
+
await expect(apiAIAssistant.requestWellnessBreak()).resolves.toBeUndefined();
|
|
303
|
+
|
|
304
|
+
expect(mockWebex.request).toHaveBeenCalledWith({
|
|
305
|
+
uri: 'https://api-ai-assistant.produs1.ciscoccservice.com/event',
|
|
306
|
+
method: HTTP_METHODS.POST,
|
|
307
|
+
addAuthHeader: true,
|
|
308
|
+
body: {
|
|
309
|
+
agentId: 'test-agent-id',
|
|
310
|
+
orgId: 'test-org-id',
|
|
311
|
+
eventType: 'CUSTOM_EVENT',
|
|
312
|
+
eventName: 'WellnessBreakAction',
|
|
313
|
+
eventDetails: {
|
|
314
|
+
data: {
|
|
315
|
+
action: 'REQUESTED',
|
|
316
|
+
agentSessionId: 'session-1',
|
|
317
|
+
actionTimeStamp: 1787640000000,
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it.each([
|
|
325
|
+
WELLNESS_BREAK_USER_ACTIONS.ACCEPTED,
|
|
326
|
+
WELLNESS_BREAK_USER_ACTIONS.REJECTED,
|
|
327
|
+
WELLNESS_BREAK_USER_ACTIONS.NO_RESPONSE,
|
|
328
|
+
] as const)(
|
|
329
|
+
'sends the %s offer action',
|
|
330
|
+
async (action) => {
|
|
331
|
+
(mockWebex.request as jest.Mock).mockResolvedValue({statusCode: 202});
|
|
332
|
+
|
|
333
|
+
await apiAIAssistant.respondToWellnessBreak({action});
|
|
334
|
+
|
|
335
|
+
expect(
|
|
336
|
+
(mockWebex.request as jest.Mock).mock.calls[0][0].body.eventDetails.data.action
|
|
337
|
+
).toBe(action);
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
it('rejects disabled or incomplete internal session context before HTTP', async () => {
|
|
342
|
+
wellnessContext = {
|
|
343
|
+
isWellnessBreakEnabled: false,
|
|
344
|
+
agentId: 'test-agent-id',
|
|
345
|
+
agentSessionId: 'session-1',
|
|
346
|
+
};
|
|
347
|
+
await expect(apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
|
|
348
|
+
'WELLNESS_BREAK_NOT_ENABLED'
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
wellnessContext = {
|
|
352
|
+
isWellnessBreakEnabled: true,
|
|
353
|
+
};
|
|
354
|
+
await expect(apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
|
|
355
|
+
'WELLNESS_BREAK_AGENT_ID_REQUIRED'
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
wellnessContext = {
|
|
359
|
+
isWellnessBreakEnabled: true,
|
|
360
|
+
agentId: 'test-agent-id',
|
|
361
|
+
};
|
|
362
|
+
await expect(apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
|
|
363
|
+
'WELLNESS_BREAK_AGENT_SESSION_REQUIRED'
|
|
364
|
+
);
|
|
365
|
+
expect(mockWebex.request).not.toHaveBeenCalled();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
it('rejects a missing organization before HTTP', async () => {
|
|
369
|
+
(mockWebex.credentials.getOrgId as jest.Mock).mockReturnValue(' ');
|
|
370
|
+
|
|
371
|
+
await expect(apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
|
|
372
|
+
'WELLNESS_BREAK_ORG_ID_REQUIRED'
|
|
373
|
+
);
|
|
374
|
+
expect(mockWebex.request).not.toHaveBeenCalled();
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it.each([200, 204, 400, 401, 403, 404, 409, 429, 500])(
|
|
378
|
+
'rejects the non-202 HTTP status %i',
|
|
379
|
+
async (statusCode) => {
|
|
380
|
+
(mockWebex.request as jest.Mock).mockResolvedValue({statusCode});
|
|
381
|
+
|
|
382
|
+
await expect(apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
|
|
383
|
+
`WELLNESS_BREAK_ACTION_UNEXPECTED_STATUS_${statusCode}`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
it('rejects network failures through the structured error path', async () => {
|
|
389
|
+
(mockWebex.request as jest.Mock).mockRejectedValue(new Error('Network unavailable'));
|
|
390
|
+
|
|
391
|
+
await expect(apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
|
|
392
|
+
'Error while performing requestWellnessBreak'
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('rejects unsupported offer actions before HTTP', async () => {
|
|
397
|
+
await expect(
|
|
398
|
+
apiAIAssistant.respondToWellnessBreak({
|
|
399
|
+
action: 'REQUESTED',
|
|
400
|
+
} as unknown as Parameters<ApiAIAssistant['respondToWellnessBreak']>[0])
|
|
401
|
+
).rejects.toThrow('WELLNESS_BREAK_ACTION_INVALID');
|
|
402
|
+
expect(mockWebex.request).not.toHaveBeenCalled();
|
|
403
|
+
});
|
|
404
|
+
});
|
|
273
405
|
});
|
|
@@ -160,11 +160,53 @@ describe('WebexCrossClientService', () => {
|
|
|
160
160
|
|
|
161
161
|
expect(trackEvent).toHaveBeenCalledWith(
|
|
162
162
|
METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
|
|
163
|
-
expect.objectContaining({enableWxBetterTogether: true}),
|
|
163
|
+
expect.objectContaining({enableWxBetterTogether: true, usersubPublished: false}),
|
|
164
164
|
['operational', 'behavioral']
|
|
165
165
|
);
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
+
it('reports retained usersub state when disable publish fails after successful enable', async () => {
|
|
169
|
+
await service.setManageWebexCallingInWxcc(true, {trackPublishMetrics: true});
|
|
170
|
+
trackEvent.mockClear();
|
|
171
|
+
|
|
172
|
+
webex.request = jest.fn().mockRejectedValue(new Error('disable failed'));
|
|
173
|
+
|
|
174
|
+
await expect(
|
|
175
|
+
service.setManageWebexCallingInWxcc(false, {trackPublishMetrics: true})
|
|
176
|
+
).rejects.toThrow('disable failed');
|
|
177
|
+
|
|
178
|
+
expect(trackEvent).toHaveBeenCalledWith(
|
|
179
|
+
METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
|
|
180
|
+
expect.objectContaining({
|
|
181
|
+
enableWxBetterTogether: false,
|
|
182
|
+
usersubPublished: true,
|
|
183
|
+
}),
|
|
184
|
+
['operational', 'behavioral']
|
|
185
|
+
);
|
|
186
|
+
expect(service.isAnswerCallsStateActive()).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('reports retained usersub state when re-enable publish fails after successful enable', async () => {
|
|
190
|
+
await service.setManageWebexCallingInWxcc(true);
|
|
191
|
+
trackEvent.mockClear();
|
|
192
|
+
|
|
193
|
+
webex.request = jest.fn().mockRejectedValue(new Error('refresh failed'));
|
|
194
|
+
|
|
195
|
+
await expect(
|
|
196
|
+
service.setManageWebexCallingInWxcc(true, {trackPublishMetrics: true})
|
|
197
|
+
).rejects.toThrow('refresh failed');
|
|
198
|
+
|
|
199
|
+
expect(trackEvent).toHaveBeenCalledWith(
|
|
200
|
+
METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
|
|
201
|
+
expect.objectContaining({
|
|
202
|
+
enableWxBetterTogether: true,
|
|
203
|
+
usersubPublished: true,
|
|
204
|
+
}),
|
|
205
|
+
['operational', 'behavioral']
|
|
206
|
+
);
|
|
207
|
+
expect(service.isAnswerCallsStateActive()).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
168
210
|
it('tracks usersub publish success when trackPublishMetrics is enabled', async () => {
|
|
169
211
|
await service.setManageWebexCallingInWxcc(true, {trackPublishMetrics: true});
|
|
170
212
|
|
|
@@ -174,7 +216,7 @@ describe('WebexCrossClientService', () => {
|
|
|
174
216
|
]);
|
|
175
217
|
expect(trackEvent).toHaveBeenCalledWith(
|
|
176
218
|
METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS,
|
|
177
|
-
{enableWxBetterTogether: true},
|
|
219
|
+
{enableWxBetterTogether: true, usersubPublished: true},
|
|
178
220
|
['operational', 'behavioral']
|
|
179
221
|
);
|
|
180
222
|
});
|
|
@@ -243,7 +285,7 @@ describe('WebexCrossClientService', () => {
|
|
|
243
285
|
expect(cancelTimedEvent).not.toHaveBeenCalled();
|
|
244
286
|
expect(trackEvent).toHaveBeenCalledWith(
|
|
245
287
|
METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS,
|
|
246
|
-
{enableWxBetterTogether: false},
|
|
288
|
+
{enableWxBetterTogether: false, usersubPublished: false},
|
|
247
289
|
['operational', 'behavioral']
|
|
248
290
|
);
|
|
249
291
|
});
|
|
@@ -42,7 +42,7 @@ describe('WxAppTelephonyMercurySync', () => {
|
|
|
42
42
|
expect(sync.isSubscribed()).toBe(true);
|
|
43
43
|
expect(trackEvent).toHaveBeenCalledWith(
|
|
44
44
|
METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_SUCCESS,
|
|
45
|
-
{},
|
|
45
|
+
{mercurySubscribed: true},
|
|
46
46
|
['operational', 'behavioral']
|
|
47
47
|
);
|
|
48
48
|
});
|
|
@@ -82,4 +82,48 @@ describe('parseAgentConfigs collaboration access fields', () => {
|
|
|
82
82
|
expect(profile.accessEntryPoint).toBe('NONE');
|
|
83
83
|
expect(profile.accessBuddyTeam).toBe('ALL');
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
it('enables wellness only when organization config, reminders, and license are present', () => {
|
|
87
|
+
const profile = parseAgentConfigs({
|
|
88
|
+
...baseProfileData,
|
|
89
|
+
orgSettingsData: {...baseProfileData.orgSettingsData, aiAssistantQuantity: 1},
|
|
90
|
+
aiFeatureFlags: {
|
|
91
|
+
data: [
|
|
92
|
+
{
|
|
93
|
+
id: 'ai-feature-1',
|
|
94
|
+
agentWellbeing: {
|
|
95
|
+
enable: true,
|
|
96
|
+
wellnessBreakReminders: 'ENABLED',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(profile.isWellnessBreakEnabled).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it.each([
|
|
107
|
+
{quantity: 0, enable: true, reminders: 'ENABLED'},
|
|
108
|
+
{quantity: 1, enable: false, reminders: 'ENABLED'},
|
|
109
|
+
{quantity: 1, enable: true, reminders: 'DISABLED'},
|
|
110
|
+
] as const)(
|
|
111
|
+
'disables wellness when an effective gate is missing',
|
|
112
|
+
({quantity, enable, reminders}) => {
|
|
113
|
+
const profile = parseAgentConfigs({
|
|
114
|
+
...baseProfileData,
|
|
115
|
+
orgSettingsData: {...baseProfileData.orgSettingsData, aiAssistantQuantity: quantity},
|
|
116
|
+
aiFeatureFlags: {
|
|
117
|
+
data: [
|
|
118
|
+
{
|
|
119
|
+
id: 'ai-feature-1',
|
|
120
|
+
agentWellbeing: {enable, wellnessBreakReminders: reminders},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(profile.isWellnessBreakEnabled).toBe(false);
|
|
127
|
+
}
|
|
128
|
+
);
|
|
85
129
|
});
|
|
@@ -705,6 +705,74 @@ describe('AgentConfigService', () => {
|
|
|
705
705
|
});
|
|
706
706
|
});
|
|
707
707
|
|
|
708
|
+
describe('getWellbeingBreakIdleCode', () => {
|
|
709
|
+
it('uses the system-code query and finds the exact code across pages', async () => {
|
|
710
|
+
(mockWebexRequest.request as jest.Mock)
|
|
711
|
+
.mockResolvedValueOnce({
|
|
712
|
+
statusCode: 200,
|
|
713
|
+
body: {
|
|
714
|
+
data: [
|
|
715
|
+
{id: 'other', name: 'Other', active: true, isSystemCode: true, defaultCode: false},
|
|
716
|
+
],
|
|
717
|
+
meta: {totalPages: 2},
|
|
718
|
+
},
|
|
719
|
+
})
|
|
720
|
+
.mockResolvedValueOnce({
|
|
721
|
+
statusCode: 200,
|
|
722
|
+
body: {
|
|
723
|
+
data: [
|
|
724
|
+
{
|
|
725
|
+
id: 'wellbeing-code',
|
|
726
|
+
name: 'WellbeingBreak',
|
|
727
|
+
active: true,
|
|
728
|
+
isSystemCode: true,
|
|
729
|
+
defaultCode: false,
|
|
730
|
+
},
|
|
731
|
+
],
|
|
732
|
+
meta: {totalPages: 2},
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
await expect(agentConfigService.getWellbeingBreakIdleCode(mockOrgId)).resolves.toEqual({
|
|
737
|
+
id: 'wellbeing-code',
|
|
738
|
+
name: 'WellbeingBreak',
|
|
739
|
+
isSystem: true,
|
|
740
|
+
isDefault: false,
|
|
741
|
+
});
|
|
742
|
+
expect(mockWebexRequest.request).toHaveBeenNthCalledWith(1, {
|
|
743
|
+
service: mockWccAPIURL,
|
|
744
|
+
resource:
|
|
745
|
+
`organization/${mockOrgId}/v2/auxiliary-code?page=0&pageSize=100` +
|
|
746
|
+
'&workType=IDLE_CODE&customFilter=isSystemCode==true&desktopProfileFilter=false',
|
|
747
|
+
method: 'GET',
|
|
748
|
+
});
|
|
749
|
+
expect(mockWebexRequest.request).toHaveBeenNthCalledWith(2, {
|
|
750
|
+
service: mockWccAPIURL,
|
|
751
|
+
resource:
|
|
752
|
+
`organization/${mockOrgId}/v2/auxiliary-code?page=1&pageSize=100` +
|
|
753
|
+
'&workType=IDLE_CODE&customFilter=isSystemCode==true&desktopProfileFilter=false',
|
|
754
|
+
method: 'GET',
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('rejects when the exact system code is unavailable', async () => {
|
|
759
|
+
(mockWebexRequest.request as jest.Mock).mockResolvedValue({
|
|
760
|
+
statusCode: 200,
|
|
761
|
+
body: {
|
|
762
|
+
data: [
|
|
763
|
+
{id: 'inactive', name: 'WellbeingBreak', active: false, isSystemCode: true},
|
|
764
|
+
{id: 'lookalike', name: 'wellbeingbreak', active: true, isSystemCode: true},
|
|
765
|
+
],
|
|
766
|
+
meta: {totalPages: 1},
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
await expect(agentConfigService.getWellbeingBreakIdleCode(mockOrgId)).rejects.toThrow(
|
|
771
|
+
'WELLBEING_BREAK_IDLE_CODE_NOT_FOUND'
|
|
772
|
+
);
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
|
|
708
776
|
describe('getAgentConfig', () => {
|
|
709
777
|
const mockTeamData = [
|
|
710
778
|
{id: 'team1', name: 'Support Team'},
|
|
@@ -2927,7 +2927,7 @@ describe('TaskManager', () => {
|
|
|
2927
2927
|
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
2928
2928
|
});
|
|
2929
2929
|
|
|
2930
|
-
it('reuses a task
|
|
2930
|
+
it('reuses a task identified by the incoming main-call media key', () => {
|
|
2931
2931
|
const mainMediaInteractionId = 'main-media-interaction-id';
|
|
2932
2932
|
const promotedChildId = 'promoted-child-without-main-id';
|
|
2933
2933
|
const task = installTask(
|
|
@@ -2958,6 +2958,41 @@ describe('TaskManager', () => {
|
|
|
2958
2958
|
expect(hydrateHandler).toHaveBeenCalledWith(task);
|
|
2959
2959
|
});
|
|
2960
2960
|
|
|
2961
|
+
it('reuses a stale task through its collection key and the incoming main-call media key', () => {
|
|
2962
|
+
const mainMediaInteractionId = 'collection-main-interaction-id';
|
|
2963
|
+
const staleChildId = 'stale-collection-child-id';
|
|
2964
|
+
const promotedChildId = 'promoted-collection-child-id';
|
|
2965
|
+
const staleTaskData = createConferenceTaskData(
|
|
2966
|
+
staleChildId,
|
|
2967
|
+
previousOwnerId,
|
|
2968
|
+
staleChildId
|
|
2969
|
+
);
|
|
2970
|
+
staleTaskData.interaction.media[staleChildId].mType = 'consult';
|
|
2971
|
+
const task = createMockTask(staleTaskData);
|
|
2972
|
+
taskManager.taskCollection = {[mainMediaInteractionId]: task};
|
|
2973
|
+
(taskManager as any).setupTaskListeners(task);
|
|
2974
|
+
const hydrateHandler = jest.fn();
|
|
2975
|
+
taskManager.on(TASK_EVENTS.TASK_HYDRATE, hydrateHandler);
|
|
2976
|
+
const ownerChangeData = createPromotedAgentOwnerChangeData(
|
|
2977
|
+
promotedChildId,
|
|
2978
|
+
mainMediaInteractionId
|
|
2979
|
+
);
|
|
2980
|
+
delete ownerChangeData.interaction.mainInteractionId;
|
|
2981
|
+
|
|
2982
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
|
|
2983
|
+
|
|
2984
|
+
expectLastStateMachineEvent(task.sendStateMachineEvent, TaskEvent.CONTACT_OWNER_CHANGED);
|
|
2985
|
+
expect(task.data.interactionId).toBe(mainMediaInteractionId);
|
|
2986
|
+
expect(task.data.interaction.owner).toBe(currentAgentId);
|
|
2987
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
2988
|
+
expect(taskManager.taskCollection[mainMediaInteractionId]).toBe(task);
|
|
2989
|
+
expect(taskManager.taskCollection[staleChildId]).toBeUndefined();
|
|
2990
|
+
expect(taskManager.taskCollection[promotedChildId]).toBeUndefined();
|
|
2991
|
+
expect(Object.values(taskManager.taskCollection)).toEqual([task]);
|
|
2992
|
+
expect(task.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
|
|
2993
|
+
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
2994
|
+
});
|
|
2995
|
+
|
|
2961
2996
|
it('prefers the exact task for ContactOwnerChanged over related child tasks', () => {
|
|
2962
2997
|
const exactTask = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
2963
2998
|
const childTaskId = 'related-child-id';
|
|
@@ -2980,6 +3015,102 @@ describe('TaskManager', () => {
|
|
|
2980
3015
|
expect(childTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
2981
3016
|
});
|
|
2982
3017
|
|
|
3018
|
+
it('does not replace a distinct main task when an exact child task also exists', () => {
|
|
3019
|
+
const childTaskId = 'exact-child-id';
|
|
3020
|
+
const mainTask = installTask(createConferenceTaskData(taskId, previousOwnerId));
|
|
3021
|
+
const childTask = createMockTask(
|
|
3022
|
+
createConferenceTaskData(childTaskId, previousOwnerId, taskId)
|
|
3023
|
+
);
|
|
3024
|
+
taskManager.taskCollection[childTaskId] = childTask;
|
|
3025
|
+
|
|
3026
|
+
webSocketManagerMock.emit(
|
|
3027
|
+
'message',
|
|
3028
|
+
JSON.stringify({
|
|
3029
|
+
data: {
|
|
3030
|
+
...createConferenceTaskData(childTaskId, promotedOwnerId, taskId),
|
|
3031
|
+
type: CC_EVENTS.CONTACT_OWNER_CHANGED,
|
|
3032
|
+
},
|
|
3033
|
+
})
|
|
3034
|
+
);
|
|
3035
|
+
|
|
3036
|
+
expect(childTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3037
|
+
expect(mainTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3038
|
+
expect(taskManager.taskCollection[taskId]).toBe(mainTask);
|
|
3039
|
+
expect(taskManager.taskCollection[childTaskId]).toBe(childTask);
|
|
3040
|
+
});
|
|
3041
|
+
|
|
3042
|
+
it('ignores a related owner change with conflicting explicit main interaction IDs', () => {
|
|
3043
|
+
const childTaskId = 'conflicting-main-child-id';
|
|
3044
|
+
const existingMainId = 'existing-main-id';
|
|
3045
|
+
const incomingMainId = 'incoming-main-id';
|
|
3046
|
+
const sharedParentId = 'shared-parent-id';
|
|
3047
|
+
const taskData = createConferenceTaskData(
|
|
3048
|
+
childTaskId,
|
|
3049
|
+
previousOwnerId,
|
|
3050
|
+
existingMainId
|
|
3051
|
+
);
|
|
3052
|
+
taskData.interaction.parentInteractionId = sharedParentId;
|
|
3053
|
+
const task = installTask(taskData);
|
|
3054
|
+
const ownerChangeData = createConferenceTaskData(
|
|
3055
|
+
'different-child-id',
|
|
3056
|
+
promotedOwnerId,
|
|
3057
|
+
incomingMainId
|
|
3058
|
+
);
|
|
3059
|
+
ownerChangeData.interaction.parentInteractionId = sharedParentId;
|
|
3060
|
+
|
|
3061
|
+
webSocketManagerMock.emit(
|
|
3062
|
+
'message',
|
|
3063
|
+
JSON.stringify({
|
|
3064
|
+
data: {...ownerChangeData, type: CC_EVENTS.CONTACT_OWNER_CHANGED},
|
|
3065
|
+
})
|
|
3066
|
+
);
|
|
3067
|
+
|
|
3068
|
+
expect(task.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3069
|
+
expect(taskManager.taskCollection[childTaskId]).toBe(task);
|
|
3070
|
+
expect(taskManager.taskCollection[existingMainId]).toBeUndefined();
|
|
3071
|
+
expect(taskManager.taskCollection[incomingMainId]).toBeUndefined();
|
|
3072
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3073
|
+
});
|
|
3074
|
+
|
|
3075
|
+
it('does not replace a distinct task already stored at the canonical main key', () => {
|
|
3076
|
+
const childTaskId = 'related-active-child-id';
|
|
3077
|
+
const canonicalMainId = 'occupied-canonical-main-id';
|
|
3078
|
+
const candidate = createMockTask(
|
|
3079
|
+
createConferenceTaskData(childTaskId, previousOwnerId, canonicalMainId)
|
|
3080
|
+
);
|
|
3081
|
+
const occupiedData = createConferenceTaskData(
|
|
3082
|
+
'occupied-task-child-id',
|
|
3083
|
+
previousOwnerId,
|
|
3084
|
+
canonicalMainId
|
|
3085
|
+
);
|
|
3086
|
+
occupiedData.interaction.media[canonicalMainId].mType = 'consult';
|
|
3087
|
+
const occupiedTask = createMockTask(occupiedData);
|
|
3088
|
+
taskManager.taskCollection = {
|
|
3089
|
+
[childTaskId]: candidate,
|
|
3090
|
+
[canonicalMainId]: occupiedTask,
|
|
3091
|
+
};
|
|
3092
|
+
|
|
3093
|
+
webSocketManagerMock.emit(
|
|
3094
|
+
'message',
|
|
3095
|
+
JSON.stringify({
|
|
3096
|
+
data: {
|
|
3097
|
+
...createConferenceTaskData(
|
|
3098
|
+
'incoming-related-child-id',
|
|
3099
|
+
promotedOwnerId,
|
|
3100
|
+
canonicalMainId
|
|
3101
|
+
),
|
|
3102
|
+
type: CC_EVENTS.CONTACT_OWNER_CHANGED,
|
|
3103
|
+
},
|
|
3104
|
+
})
|
|
3105
|
+
);
|
|
3106
|
+
|
|
3107
|
+
expect(candidate.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3108
|
+
expect(occupiedTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3109
|
+
expect(taskManager.taskCollection[childTaskId]).toBe(candidate);
|
|
3110
|
+
expect(taskManager.taskCollection[canonicalMainId]).toBe(occupiedTask);
|
|
3111
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3112
|
+
});
|
|
3113
|
+
|
|
2983
3114
|
it('ignores a related ContactOwnerChanged task whose current agent is consult-only', () => {
|
|
2984
3115
|
const childTaskId = 'consult-only-child-id';
|
|
2985
3116
|
const data = createConferenceTaskData(childTaskId, previousOwnerId, taskId);
|
|
@@ -3066,7 +3197,43 @@ describe('TaskManager', () => {
|
|
|
3066
3197
|
expect(secondTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3067
3198
|
});
|
|
3068
3199
|
|
|
3069
|
-
it('
|
|
3200
|
+
it('ignores ambiguous main-call media-key matches without creating a task', () => {
|
|
3201
|
+
const mainMediaInteractionId = 'ambiguous-main-media-id';
|
|
3202
|
+
const createMediaRelatedTask = (interactionId: string) => {
|
|
3203
|
+
const data = createConferenceTaskData(interactionId, previousOwnerId, interactionId);
|
|
3204
|
+
data.interaction.media = {
|
|
3205
|
+
[mainMediaInteractionId]: {
|
|
3206
|
+
mediaResourceId: mainMediaInteractionId,
|
|
3207
|
+
mediaType: 'telephony',
|
|
3208
|
+
mType: 'mainCall',
|
|
3209
|
+
participants: [currentAgentId, previousOwnerId, promotedOwnerId],
|
|
3210
|
+
},
|
|
3211
|
+
};
|
|
3212
|
+
|
|
3213
|
+
return createMockTask(data);
|
|
3214
|
+
};
|
|
3215
|
+
const firstTask = createMediaRelatedTask('first-media-child-id');
|
|
3216
|
+
const secondTask = createMediaRelatedTask('second-media-child-id');
|
|
3217
|
+
taskManager.taskCollection = {
|
|
3218
|
+
'first-media-child-id': firstTask,
|
|
3219
|
+
'second-media-child-id': secondTask,
|
|
3220
|
+
};
|
|
3221
|
+
(TaskFactory.createTask as jest.Mock).mockClear();
|
|
3222
|
+
const ownerChangeData = createPromotedAgentOwnerChangeData(
|
|
3223
|
+
'promoted-media-child-id',
|
|
3224
|
+
mainMediaInteractionId
|
|
3225
|
+
);
|
|
3226
|
+
delete ownerChangeData.interaction.mainInteractionId;
|
|
3227
|
+
|
|
3228
|
+
webSocketManagerMock.emit('message', JSON.stringify({data: ownerChangeData}));
|
|
3229
|
+
|
|
3230
|
+
expect(firstTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3231
|
+
expect(secondTask.sendStateMachineEvent).not.toHaveBeenCalled();
|
|
3232
|
+
expect(TaskFactory.createTask).not.toHaveBeenCalled();
|
|
3233
|
+
expect(Object.values(taskManager.taskCollection)).toEqual([firstTask, secondTask]);
|
|
3234
|
+
});
|
|
3235
|
+
|
|
3236
|
+
it('does not recover a new task when multiple stale related tasks exist', () => {
|
|
3070
3237
|
const createStaleRelatedTask = (interactionId: string) => {
|
|
3071
3238
|
const data = createConferenceTaskData(interactionId, previousOwnerId, taskId);
|
|
3072
3239
|
data.interaction.media[taskId].participants = [previousOwnerId, promotedOwnerId];
|
|
@@ -3178,7 +3345,10 @@ describe('TaskManager', () => {
|
|
|
3178
3345
|
expect.objectContaining({
|
|
3179
3346
|
type: TaskEvent.HYDRATE,
|
|
3180
3347
|
agentId: currentAgentId,
|
|
3181
|
-
taskData: expect.objectContaining({
|
|
3348
|
+
taskData: expect.objectContaining({
|
|
3349
|
+
interactionId: taskId,
|
|
3350
|
+
interaction: expect.objectContaining({state: 'conference'}),
|
|
3351
|
+
}),
|
|
3182
3352
|
})
|
|
3183
3353
|
);
|
|
3184
3354
|
expect(recoveredTask.sendStateMachineEvent).toHaveBeenNthCalledWith(
|
|
@@ -3192,6 +3362,7 @@ describe('TaskManager', () => {
|
|
|
3192
3362
|
expect(taskManager.taskCollection['promoted-child-interaction-id']).toBeUndefined();
|
|
3193
3363
|
expect(Object.values(taskManager.taskCollection)).toEqual([recoveredTask]);
|
|
3194
3364
|
expect(recoveredTask.data.interaction.owner).toBe(currentAgentId);
|
|
3365
|
+
expect(recoveredTask.listenerCount(TASK_EVENTS.TASK_HYDRATE)).toBe(1);
|
|
3195
3366
|
expect(hydrateHandler).toHaveBeenCalledTimes(1);
|
|
3196
3367
|
expect(hydrateHandler).toHaveBeenCalledWith(recoveredTask);
|
|
3197
3368
|
expect(incomingHandler).not.toHaveBeenCalled();
|
|
@@ -3217,6 +3388,21 @@ describe('TaskManager', () => {
|
|
|
3217
3388
|
data.interaction.isTerminated = true;
|
|
3218
3389
|
},
|
|
3219
3390
|
],
|
|
3391
|
+
[
|
|
3392
|
+
'the interaction is not telephony',
|
|
3393
|
+
(data) => {
|
|
3394
|
+
data.interaction.mediaType = 'chat';
|
|
3395
|
+
},
|
|
3396
|
+
],
|
|
3397
|
+
[
|
|
3398
|
+
'the payload has no stable interaction identifier',
|
|
3399
|
+
(data) => {
|
|
3400
|
+
data.interactionId = undefined;
|
|
3401
|
+
data.interaction.interactionId = undefined;
|
|
3402
|
+
delete data.interaction.mainInteractionId;
|
|
3403
|
+
data.interaction.media = {};
|
|
3404
|
+
},
|
|
3405
|
+
],
|
|
3220
3406
|
])('does not create a missing task when %s', (_description, mutatePayload) => {
|
|
3221
3407
|
clearTasksAndFactoryHistory();
|
|
3222
3408
|
const ownerChangeData = createPromotedAgentOwnerChangeData();
|
|
@@ -3626,6 +3812,15 @@ describe('TaskManager', () => {
|
|
|
3626
3812
|
expect(taskManager.taskCollection['task-alias']).toBeUndefined();
|
|
3627
3813
|
});
|
|
3628
3814
|
|
|
3815
|
+
it('disposes wxApp offer observability when removing a task from the collection', () => {
|
|
3816
|
+
const disposeWxAppOfferObservability = jest.fn();
|
|
3817
|
+
task.disposeWxAppOfferObservability = disposeWxAppOfferObservability;
|
|
3818
|
+
|
|
3819
|
+
(taskManager as any).removeTaskFromCollection(task);
|
|
3820
|
+
|
|
3821
|
+
expect(disposeWxAppOfferObservability).toHaveBeenCalledTimes(1);
|
|
3822
|
+
});
|
|
3823
|
+
|
|
3629
3824
|
it('replaces an EP-DN child task with a hydrated main task on CONTACT_MERGED', () => {
|
|
3630
3825
|
const childTaskId = 'ep-dn-child-interaction-id';
|
|
3631
3826
|
const childTask = createMockTask({
|
|
@@ -4186,4 +4381,20 @@ describe('TaskManager', () => {
|
|
|
4186
4381
|
expect(taskTwo.applyWxAppMuteStateFromSync).toHaveBeenCalledWith('call-1', true);
|
|
4187
4382
|
});
|
|
4188
4383
|
});
|
|
4384
|
+
|
|
4385
|
+
describe('refreshWxAppOfferObservabilityForAllTasks', () => {
|
|
4386
|
+
it('invokes refresh on collected tasks that implement it', () => {
|
|
4387
|
+
const taskOne = {refreshWxAppOfferObservability: jest.fn()};
|
|
4388
|
+
const taskTwo = {};
|
|
4389
|
+
|
|
4390
|
+
taskManager['taskCollection'] = {
|
|
4391
|
+
[taskId]: taskOne,
|
|
4392
|
+
'task-2': taskTwo,
|
|
4393
|
+
};
|
|
4394
|
+
|
|
4395
|
+
taskManager.refreshWxAppOfferObservabilityForAllTasks();
|
|
4396
|
+
|
|
4397
|
+
expect(taskOne.refreshWxAppOfferObservability).toHaveBeenCalledTimes(1);
|
|
4398
|
+
});
|
|
4399
|
+
});
|
|
4189
4400
|
});
|
|
@@ -539,6 +539,39 @@ describe('Task state machine', () => {
|
|
|
539
539
|
} as any,
|
|
540
540
|
});
|
|
541
541
|
|
|
542
|
+
it('hydrates a recovered active conference from IDLE to CONFERENCING', () => {
|
|
543
|
+
const service = startMachine();
|
|
544
|
+
const taskData = createTaskData({
|
|
545
|
+
agentId: 'agent-1',
|
|
546
|
+
interactionId: 'interaction-1',
|
|
547
|
+
mediaResourceId: 'interaction-1',
|
|
548
|
+
interaction: {
|
|
549
|
+
state: 'conference',
|
|
550
|
+
interactionId: 'interaction-1',
|
|
551
|
+
mainInteractionId: 'interaction-1',
|
|
552
|
+
owner: 'agent-1',
|
|
553
|
+
participants: {
|
|
554
|
+
'agent-1': {id: 'agent-1', pType: 'Agent', hasLeft: false},
|
|
555
|
+
'agent-2': {id: 'agent-2', pType: 'Agent', hasLeft: false},
|
|
556
|
+
'customer-1': {id: 'customer-1', pType: 'Customer', hasLeft: false},
|
|
557
|
+
},
|
|
558
|
+
media: {
|
|
559
|
+
'interaction-1': {
|
|
560
|
+
mediaResourceId: 'interaction-1',
|
|
561
|
+
mType: 'mainCall',
|
|
562
|
+
participants: ['agent-1', 'agent-2', 'customer-1'],
|
|
563
|
+
isHold: false,
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
} as any,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
service.send({type: TaskEvent.HYDRATE, taskData});
|
|
570
|
+
|
|
571
|
+
expect(service.getSnapshot().value).toBe(TaskState.CONFERENCING);
|
|
572
|
+
expect(service.getSnapshot().context.taskData).toEqual(taskData);
|
|
573
|
+
});
|
|
574
|
+
|
|
542
575
|
it('boots from IDLE to CONSULTING on CONSULTING_ACTIVE for split-leg ordering', () => {
|
|
543
576
|
const service = startMachine();
|
|
544
577
|
const taskData = createTaskData({
|