@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
package/test/unit/spec/cc.ts
CHANGED
|
@@ -130,6 +130,7 @@ describe('webex.cc', () => {
|
|
|
130
130
|
config: {
|
|
131
131
|
getAgentConfig: jest.fn(),
|
|
132
132
|
getOutdialAniEntries: jest.fn(),
|
|
133
|
+
getWellbeingBreakIdleCode: jest.fn(),
|
|
133
134
|
},
|
|
134
135
|
webSocketManager: mockWebSocketManager,
|
|
135
136
|
rtdWebSocketManager: {
|
|
@@ -170,12 +171,14 @@ describe('webex.cc', () => {
|
|
|
170
171
|
setAgentId: jest.fn(),
|
|
171
172
|
setWebRtcEnabled: jest.fn(),
|
|
172
173
|
setAnswerCallOnWebexService: jest.fn(),
|
|
174
|
+
handleRealtimeWebsocketEvent: jest.fn(),
|
|
173
175
|
registerIncomingCallEvent: jest.fn(),
|
|
174
176
|
registerTaskListeners: jest.fn(),
|
|
175
177
|
getTask: jest.fn(),
|
|
176
178
|
getActiveTasks: jest.fn(),
|
|
177
179
|
applyEnableWxBetterTogether: jest.fn(),
|
|
178
180
|
syncWxAppMuteFromCallDetailsForAllTasks: jest.fn(),
|
|
181
|
+
refreshWxAppOfferObservabilityForAllTasks: jest.fn(),
|
|
179
182
|
on: jest.fn(),
|
|
180
183
|
off: jest.fn(),
|
|
181
184
|
emit: jest.fn(),
|
|
@@ -321,6 +324,7 @@ describe('webex.cc', () => {
|
|
|
321
324
|
isTimeoutDesktopInactivityEnabled: false,
|
|
322
325
|
webRtcEnabled: true,
|
|
323
326
|
lostConnectionRecoveryTimeout: 0,
|
|
327
|
+
isWellnessBreakEnabled: false,
|
|
324
328
|
};
|
|
325
329
|
|
|
326
330
|
it('should register successfully and return agent profile', async () => {
|
|
@@ -332,6 +336,7 @@ describe('webex.cc', () => {
|
|
|
332
336
|
data: {
|
|
333
337
|
auxCodeId: 'auxCodeId',
|
|
334
338
|
agentId: 'agentId',
|
|
339
|
+
agentSessionId: 'session-after-refresh',
|
|
335
340
|
deviceType: LoginOption.EXTENSION,
|
|
336
341
|
dn: '12345',
|
|
337
342
|
},
|
|
@@ -404,22 +409,25 @@ describe('webex.cc', () => {
|
|
|
404
409
|
module: CC_FILE,
|
|
405
410
|
method: 'connectWebsocket',
|
|
406
411
|
});
|
|
407
|
-
expect(mockTaskManager.setConfigFlags).toHaveBeenCalledWith(
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
412
|
+
expect(mockTaskManager.setConfigFlags).toHaveBeenCalledWith(
|
|
413
|
+
expect.objectContaining({
|
|
414
|
+
isEndTaskEnabled: mockAgentProfile.isEndTaskEnabled,
|
|
415
|
+
isEndConsultEnabled: mockAgentProfile.isEndConsultEnabled,
|
|
416
|
+
webRtcEnabled: mockAgentProfile.webRtcEnabled,
|
|
417
|
+
autoWrapup: mockAgentProfile.wrapUpData.wrapUpProps.autoWrapup ?? false,
|
|
418
|
+
aiFeature: mockAgentProfile.aiFeature,
|
|
419
|
+
consultTransfer: {
|
|
420
|
+
allowConsultToQueue: mockAgentProfile.allowConsultToQueue,
|
|
421
|
+
accessQueue: mockAgentProfile.accessQueue,
|
|
422
|
+
accessEntryPoint: mockAgentProfile.accessEntryPoint,
|
|
423
|
+
accessBuddyTeam: mockAgentProfile.accessBuddyTeam,
|
|
424
|
+
},
|
|
425
|
+
enableWxBetterTogether: false,
|
|
426
|
+
getWxAppUsersubPublished: expect.any(Function),
|
|
427
|
+
})
|
|
428
|
+
);
|
|
421
429
|
expect(reloadSpy).toHaveBeenCalled();
|
|
422
|
-
expect(result).toEqual(mockAgentProfile);
|
|
430
|
+
expect(result).toEqual({...mockAgentProfile, agentSessionId: 'session-after-refresh'});
|
|
423
431
|
expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
|
|
424
432
|
METRIC_EVENT_NAMES.WEBSOCKET_REGISTER_SUCCESS,
|
|
425
433
|
METRIC_EVENT_NAMES.WEBSOCKET_REGISTER_FAILED,
|
|
@@ -496,6 +504,25 @@ describe('webex.cc', () => {
|
|
|
496
504
|
);
|
|
497
505
|
});
|
|
498
506
|
|
|
507
|
+
it('clears wellness session context when registration fails after relogin', async () => {
|
|
508
|
+
const mockError = new Error('Error after silent relogin');
|
|
509
|
+
webex.cc.$config = {...webex.cc.$config, allowAutomatedRelogin: true};
|
|
510
|
+
jest.spyOn(webex.cc.services.config, 'getAgentConfig').mockResolvedValue({
|
|
511
|
+
...mockAgentProfile,
|
|
512
|
+
webRtcEnabled: false,
|
|
513
|
+
aiFeature: undefined,
|
|
514
|
+
} as Profile);
|
|
515
|
+
mockWebSocketManager.initWebSocket.mockResolvedValue({agentId: 'agent123'});
|
|
516
|
+
jest.spyOn(webex.cc as any, 'silentRelogin').mockImplementation(async () => {
|
|
517
|
+
webex.cc['updateWellnessSession']('partial-session');
|
|
518
|
+
throw mockError;
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
await expect(webex.cc.register()).rejects.toThrow(mockError);
|
|
522
|
+
|
|
523
|
+
expect(webex.cc['currentAgentSessionId']).toBeUndefined();
|
|
524
|
+
});
|
|
525
|
+
|
|
499
526
|
it('should log error if mercury connect fails but cc.register() should not fail', async () => {
|
|
500
527
|
const mockError = new Error('Error while performing mercury connect');
|
|
501
528
|
jest.spyOn(webex.internal.mercury, 'connect').mockRejectedValue(mockError);
|
|
@@ -632,8 +659,47 @@ describe('webex.cc', () => {
|
|
|
632
659
|
expect(result).toEqual(mockAgentProfile);
|
|
633
660
|
});
|
|
634
661
|
|
|
635
|
-
it('should
|
|
636
|
-
mockAgentProfile.aiFeature = {
|
|
662
|
+
it('should connect RTD websocket when only wellness breaks are enabled', async () => {
|
|
663
|
+
mockAgentProfile.aiFeature = {
|
|
664
|
+
realtimeTranscripts: {enable: false},
|
|
665
|
+
suggestedResponses: {enable: false},
|
|
666
|
+
} as any;
|
|
667
|
+
mockAgentProfile.isWellnessBreakEnabled = true;
|
|
668
|
+
jest.spyOn(webex.internal.mercury, 'connect').mockResolvedValue(true);
|
|
669
|
+
jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
|
|
670
|
+
data: {
|
|
671
|
+
auxCodeId: 'auxCodeId',
|
|
672
|
+
agentId: 'agentId',
|
|
673
|
+
deviceType: LoginOption.EXTENSION,
|
|
674
|
+
dn: '12345',
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
jest.spyOn(webex.cc.services.config, 'getAgentConfig').mockResolvedValue(mockAgentProfile);
|
|
678
|
+
mockWebSocketManager.initWebSocket.mockResolvedValue({agentId: 'agent123'});
|
|
679
|
+
|
|
680
|
+
await webex.cc.register();
|
|
681
|
+
|
|
682
|
+
expect(webex.cc.services.rtdWebSocketManager.initWebSocket).toHaveBeenCalledWith({
|
|
683
|
+
body: {
|
|
684
|
+
force: true,
|
|
685
|
+
isKeepAliveEnabled: false,
|
|
686
|
+
clientType: 'WebexCCSDK',
|
|
687
|
+
allowMultiLogin: false,
|
|
688
|
+
},
|
|
689
|
+
resource: 'v1/realtime/subscribe',
|
|
690
|
+
});
|
|
691
|
+
expect(webex.cc.services.rtdWebSocketManager.on).toHaveBeenCalledWith(
|
|
692
|
+
'message',
|
|
693
|
+
expect.any(Function)
|
|
694
|
+
);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('should not connect RTD websocket when transcripts, suggestions, and wellness are disabled', async () => {
|
|
698
|
+
mockAgentProfile.aiFeature = {
|
|
699
|
+
realtimeTranscripts: {enable: false},
|
|
700
|
+
suggestedResponses: {enable: false},
|
|
701
|
+
} as any;
|
|
702
|
+
mockAgentProfile.isWellnessBreakEnabled = false;
|
|
637
703
|
jest.spyOn(webex.internal.mercury, 'connect').mockResolvedValue(true);
|
|
638
704
|
jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
|
|
639
705
|
data: {
|
|
@@ -1076,6 +1142,7 @@ describe('webex.cc', () => {
|
|
|
1076
1142
|
|
|
1077
1143
|
expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
|
|
1078
1144
|
expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
|
|
1145
|
+
expect(mockTaskManager.refreshWxAppOfferObservabilityForAllTasks).toHaveBeenCalled();
|
|
1079
1146
|
});
|
|
1080
1147
|
|
|
1081
1148
|
it('should rollback wxApp config when post-station-login init fails', async () => {
|
|
@@ -1206,7 +1273,57 @@ describe('webex.cc', () => {
|
|
|
1206
1273
|
|
|
1207
1274
|
expect(metricSpy).toHaveBeenCalledWith(
|
|
1208
1275
|
METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
|
|
1209
|
-
expect.objectContaining({skipReason: 'publish_failed'}),
|
|
1276
|
+
expect.objectContaining({skipReason: 'publish_failed', usersubPublished: false}),
|
|
1277
|
+
['operational', 'behavioral']
|
|
1278
|
+
);
|
|
1279
|
+
expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
it('should report retained usersub state when publish fails during re-init', async () => {
|
|
1283
|
+
webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
|
|
1284
|
+
webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
|
|
1285
|
+
webex.cc.agentConfig = {
|
|
1286
|
+
agentId: 'agentId',
|
|
1287
|
+
webRtcEnabled: false,
|
|
1288
|
+
loginVoiceOptions: ['EXTENSION'],
|
|
1289
|
+
};
|
|
1290
|
+
webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
|
|
1291
|
+
mockTaskManager.applyEnableWxBetterTogether(true);
|
|
1292
|
+
|
|
1293
|
+
jest.spyOn(webex.cc as any, 'ensureWxAppMercuryConnected').mockResolvedValue(undefined);
|
|
1294
|
+
jest
|
|
1295
|
+
.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
|
|
1296
|
+
.mockImplementation(() => {});
|
|
1297
|
+
jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
|
|
1298
|
+
jest
|
|
1299
|
+
.spyOn(webex.cc['webexCrossClientService'], 'isAnswerCallsStateActive')
|
|
1300
|
+
.mockReturnValue(true);
|
|
1301
|
+
jest
|
|
1302
|
+
.spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
|
|
1303
|
+
.mockRejectedValue(new Error('publish failed'));
|
|
1304
|
+
jest
|
|
1305
|
+
.spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
|
|
1306
|
+
.mockResolvedValue(undefined);
|
|
1307
|
+
const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
|
|
1308
|
+
|
|
1309
|
+
jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
|
|
1310
|
+
data: {
|
|
1311
|
+
agentId: 'agentId',
|
|
1312
|
+
teamId: 'teamId',
|
|
1313
|
+
channelsMap: {chat: [], email: [], social: [], telephony: []},
|
|
1314
|
+
},
|
|
1315
|
+
trackingId: 'track-1',
|
|
1316
|
+
} as StationLoginSuccess);
|
|
1317
|
+
|
|
1318
|
+
await webex.cc.stationLogin({
|
|
1319
|
+
teamId: 'teamId',
|
|
1320
|
+
loginOption: LoginOption.EXTENSION,
|
|
1321
|
+
dialNumber: '1001',
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
expect(metricSpy).toHaveBeenCalledWith(
|
|
1325
|
+
METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
|
|
1326
|
+
expect.objectContaining({skipReason: 'publish_failed', usersubPublished: true}),
|
|
1210
1327
|
['operational', 'behavioral']
|
|
1211
1328
|
);
|
|
1212
1329
|
expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
|
|
@@ -1618,7 +1735,61 @@ describe('webex.cc', () => {
|
|
|
1618
1735
|
);
|
|
1619
1736
|
expect(metricSpy).toHaveBeenCalledWith(
|
|
1620
1737
|
METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
|
|
1621
|
-
expect.objectContaining({skipReason: 'user_id_unavailable'}),
|
|
1738
|
+
expect.objectContaining({skipReason: 'user_id_unavailable', usersubPublished: false}),
|
|
1739
|
+
['operational', 'behavioral']
|
|
1740
|
+
);
|
|
1741
|
+
expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
|
|
1742
|
+
});
|
|
1743
|
+
|
|
1744
|
+
it('should report retained usersub state when userId is missing during re-init', async () => {
|
|
1745
|
+
webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
|
|
1746
|
+
webex.internal.device = {
|
|
1747
|
+
url: 'https://wdm.example.com/devices/dev-1',
|
|
1748
|
+
registered: true,
|
|
1749
|
+
register: jest.fn().mockResolvedValue(undefined),
|
|
1750
|
+
};
|
|
1751
|
+
webex.internal.mercury = {
|
|
1752
|
+
connected: true,
|
|
1753
|
+
connect: jest.fn().mockResolvedValue(undefined),
|
|
1754
|
+
on: jest.fn(),
|
|
1755
|
+
off: jest.fn(),
|
|
1756
|
+
};
|
|
1757
|
+
webex.cc.agentConfig = {
|
|
1758
|
+
agentId: 'agentId',
|
|
1759
|
+
webRtcEnabled: false,
|
|
1760
|
+
loginVoiceOptions: ['EXTENSION'],
|
|
1761
|
+
};
|
|
1762
|
+
webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
|
|
1763
|
+
mockTaskManager.applyEnableWxBetterTogether(true);
|
|
1764
|
+
|
|
1765
|
+
jest
|
|
1766
|
+
.spyOn(webex.cc['webexCrossClientService'], 'isAnswerCallsStateActive')
|
|
1767
|
+
.mockReturnValue(true);
|
|
1768
|
+
const publishSpy = jest
|
|
1769
|
+
.spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
|
|
1770
|
+
.mockResolvedValue(undefined);
|
|
1771
|
+
jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
|
|
1772
|
+
const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
|
|
1773
|
+
|
|
1774
|
+
jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
|
|
1775
|
+
data: {
|
|
1776
|
+
agentId: 'agentId',
|
|
1777
|
+
teamId: 'teamId',
|
|
1778
|
+
channelsMap: {chat: [], email: [], social: [], telephony: []},
|
|
1779
|
+
},
|
|
1780
|
+
trackingId: 'track-1',
|
|
1781
|
+
} as StationLoginSuccess);
|
|
1782
|
+
|
|
1783
|
+
await webex.cc.stationLogin({
|
|
1784
|
+
teamId: 'teamId',
|
|
1785
|
+
loginOption: LoginOption.EXTENSION,
|
|
1786
|
+
dialNumber: '1001',
|
|
1787
|
+
});
|
|
1788
|
+
|
|
1789
|
+
expect(publishSpy).not.toHaveBeenCalledWith(true, expect.anything());
|
|
1790
|
+
expect(metricSpy).toHaveBeenCalledWith(
|
|
1791
|
+
METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
|
|
1792
|
+
expect.objectContaining({skipReason: 'user_id_unavailable', usersubPublished: true}),
|
|
1622
1793
|
['operational', 'behavioral']
|
|
1623
1794
|
);
|
|
1624
1795
|
expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
|
|
@@ -2070,6 +2241,257 @@ describe('webex.cc', () => {
|
|
|
2070
2241
|
});
|
|
2071
2242
|
});
|
|
2072
2243
|
|
|
2244
|
+
describe('Agent Wellness Break SDK contracts', () => {
|
|
2245
|
+
const wellbeingCode = {
|
|
2246
|
+
id: 'wellbeing-code',
|
|
2247
|
+
name: 'WellbeingBreak',
|
|
2248
|
+
isSystem: true,
|
|
2249
|
+
isDefault: false,
|
|
2250
|
+
};
|
|
2251
|
+
|
|
2252
|
+
beforeEach(() => {
|
|
2253
|
+
webex.cc.agentConfig = {
|
|
2254
|
+
agentId: 'agent-1',
|
|
2255
|
+
isWellnessBreakEnabled: true,
|
|
2256
|
+
} as Profile;
|
|
2257
|
+
webex.cc['updateWellnessSession']('session-1');
|
|
2258
|
+
});
|
|
2259
|
+
|
|
2260
|
+
it('fetches the system idle code once per registration', async () => {
|
|
2261
|
+
const lookup = jest
|
|
2262
|
+
.spyOn(webex.cc.services.config, 'getWellbeingBreakIdleCode')
|
|
2263
|
+
.mockResolvedValue(wellbeingCode);
|
|
2264
|
+
|
|
2265
|
+
await expect(webex.cc.getWellbeingBreakIdleCode()).resolves.toEqual(wellbeingCode);
|
|
2266
|
+
await expect(webex.cc.getWellbeingBreakIdleCode()).resolves.toEqual(wellbeingCode);
|
|
2267
|
+
|
|
2268
|
+
expect(lookup).toHaveBeenCalledTimes(1);
|
|
2269
|
+
expect(lookup).toHaveBeenCalledWith('mockOrgId');
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2272
|
+
it('does not cache an idle-code lookup that completes after a new registration starts', async () => {
|
|
2273
|
+
let resolveLookup: (idleCode: typeof wellbeingCode) => void = () => undefined;
|
|
2274
|
+
jest
|
|
2275
|
+
.spyOn(webex.cc.services.config, 'getWellbeingBreakIdleCode')
|
|
2276
|
+
.mockImplementation(
|
|
2277
|
+
() =>
|
|
2278
|
+
new Promise((resolve) => {
|
|
2279
|
+
resolveLookup = resolve;
|
|
2280
|
+
})
|
|
2281
|
+
);
|
|
2282
|
+
|
|
2283
|
+
const lookupPromise = webex.cc.getWellbeingBreakIdleCode();
|
|
2284
|
+
const staleLookup = expect(lookupPromise).rejects.toThrow(
|
|
2285
|
+
'WELLNESS_BREAK_REGISTRATION_CHANGED'
|
|
2286
|
+
);
|
|
2287
|
+
jest
|
|
2288
|
+
.spyOn(webex.cc, 'connectWebsocket')
|
|
2289
|
+
.mockRejectedValue(new Error('replacement registration failed'));
|
|
2290
|
+
|
|
2291
|
+
await expect(webex.cc.register()).rejects.toThrow('replacement registration failed');
|
|
2292
|
+
resolveLookup(wellbeingCode);
|
|
2293
|
+
|
|
2294
|
+
await staleLookup;
|
|
2295
|
+
expect(webex.cc['wellbeingBreakIdleCode']).toBeUndefined();
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
it('rejects the system idle-code lookup when wellness is disabled', async () => {
|
|
2299
|
+
webex.cc.agentConfig.isWellnessBreakEnabled = false;
|
|
2300
|
+
|
|
2301
|
+
await expect(webex.cc.getWellbeingBreakIdleCode()).rejects.toThrow(
|
|
2302
|
+
'WELLNESS_BREAK_NOT_ENABLED'
|
|
2303
|
+
);
|
|
2304
|
+
expect(webex.cc.services.config.getWellbeingBreakIdleCode).not.toHaveBeenCalled();
|
|
2305
|
+
});
|
|
2306
|
+
|
|
2307
|
+
it('invalidates the wellness session only for a current-session multi-login close', () => {
|
|
2308
|
+
webex.cc['handleWebsocketMessage'](
|
|
2309
|
+
JSON.stringify({
|
|
2310
|
+
type: CC_EVENTS.AGENT_MULTI_LOGIN,
|
|
2311
|
+
data: {
|
|
2312
|
+
type: 'AgentMultiLoginCloseSession',
|
|
2313
|
+
agentSessionId: 'another-session',
|
|
2314
|
+
},
|
|
2315
|
+
})
|
|
2316
|
+
);
|
|
2317
|
+
expect(webex.cc['currentAgentSessionId']).toBe('session-1');
|
|
2318
|
+
|
|
2319
|
+
webex.cc['handleWebsocketMessage'](
|
|
2320
|
+
JSON.stringify({
|
|
2321
|
+
type: CC_EVENTS.AGENT_MULTI_LOGIN,
|
|
2322
|
+
data: {
|
|
2323
|
+
type: 'AgentMultiLoginCloseSession',
|
|
2324
|
+
agentSessionId: 'session-1',
|
|
2325
|
+
},
|
|
2326
|
+
})
|
|
2327
|
+
);
|
|
2328
|
+
expect(webex.cc['currentAgentSessionId']).toBeUndefined();
|
|
2329
|
+
});
|
|
2330
|
+
|
|
2331
|
+
it('emits primary wellness notifications regardless of notification session metadata', () => {
|
|
2332
|
+
const emitSpy = jest.spyOn(webex.cc, 'emit');
|
|
2333
|
+
const payload = {
|
|
2334
|
+
type: 'Wellness_Break_Handler',
|
|
2335
|
+
orgId: 'mockOrgId',
|
|
2336
|
+
trackingId: 'data-notification-tracking',
|
|
2337
|
+
data: {
|
|
2338
|
+
agentId: 'agent-1',
|
|
2339
|
+
orgId: 'mockOrgId',
|
|
2340
|
+
notifType: 'Wellness_Break_Handler',
|
|
2341
|
+
notifDetails: {
|
|
2342
|
+
actionEvent: 'SUGGEST_WELLNESS_BREAK',
|
|
2343
|
+
actionText: 'How about a break?',
|
|
2344
|
+
},
|
|
2345
|
+
data: {
|
|
2346
|
+
orgId: 'mockOrgId',
|
|
2347
|
+
agentSessionId: 'notification-session',
|
|
2348
|
+
interactionId: 'interaction-1',
|
|
2349
|
+
},
|
|
2350
|
+
},
|
|
2351
|
+
};
|
|
2352
|
+
|
|
2353
|
+
webex.cc['handleWebsocketMessage'](JSON.stringify(payload));
|
|
2354
|
+
|
|
2355
|
+
expect(emitSpy).toHaveBeenCalledWith(CC_EVENTS.WELLNESS_BREAK, {
|
|
2356
|
+
agentId: 'agent-1',
|
|
2357
|
+
orgId: 'mockOrgId',
|
|
2358
|
+
agentSessionId: 'notification-session',
|
|
2359
|
+
actionEvent: 'SUGGEST_WELLNESS_BREAK',
|
|
2360
|
+
actionText: 'How about a break?',
|
|
2361
|
+
interactionId: 'interaction-1',
|
|
2362
|
+
trackingId: 'data-notification-tracking',
|
|
2363
|
+
});
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
it('routes data-socket wellness notifications without sending them to TaskManager', () => {
|
|
2367
|
+
const emitSpy = jest.spyOn(webex.cc, 'emit');
|
|
2368
|
+
const taskEventSpy = mockTaskManager.handleRealtimeWebsocketEvent;
|
|
2369
|
+
const wellnessMessage = JSON.stringify({
|
|
2370
|
+
type: 'Wellness_Break_Handler',
|
|
2371
|
+
orgId: 'mockOrgId',
|
|
2372
|
+
data: {
|
|
2373
|
+
agentId: 'agent-1',
|
|
2374
|
+
orgId: 'mockOrgId',
|
|
2375
|
+
notifType: 'Wellness_Break_Handler',
|
|
2376
|
+
notifDetails: {actionEvent: 'PROVIDE_WELLNESS_BREAK'},
|
|
2377
|
+
data: {orgId: 'mockOrgId', agentSessionId: 'notification-session'},
|
|
2378
|
+
},
|
|
2379
|
+
});
|
|
2380
|
+
|
|
2381
|
+
webex.cc['handleRTDWebsocketMessage'](wellnessMessage);
|
|
2382
|
+
|
|
2383
|
+
expect(emitSpy).toHaveBeenCalledWith(CC_EVENTS.WELLNESS_BREAK, {
|
|
2384
|
+
agentId: 'agent-1',
|
|
2385
|
+
orgId: 'mockOrgId',
|
|
2386
|
+
agentSessionId: 'notification-session',
|
|
2387
|
+
actionEvent: 'PROVIDE_WELLNESS_BREAK',
|
|
2388
|
+
});
|
|
2389
|
+
expect(taskEventSpy).not.toHaveBeenCalled();
|
|
2390
|
+
|
|
2391
|
+
const transcriptMessage = JSON.stringify({type: CC_EVENTS.REAL_TIME_TRANSCRIPTION});
|
|
2392
|
+
webex.cc['handleRTDWebsocketMessage'](transcriptMessage);
|
|
2393
|
+
expect(taskEventSpy).toHaveBeenCalledWith(transcriptMessage);
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2396
|
+
it.each([
|
|
2397
|
+
['SUGGEST_WELLNESS_BREAK', 'interactionId'],
|
|
2398
|
+
['WELLNESS_BREAK_NOT_ALLOWED', 'InteractionId'],
|
|
2399
|
+
])('normalizes the %s action and either interaction-id spelling', (actionEvent, key) => {
|
|
2400
|
+
const emitSpy = jest.spyOn(webex.cc, 'emit');
|
|
2401
|
+
webex.cc['handleWebsocketMessage'](
|
|
2402
|
+
JSON.stringify({
|
|
2403
|
+
type: 'Wellness_Break_Handler',
|
|
2404
|
+
orgId: 'mockOrgId',
|
|
2405
|
+
data: {
|
|
2406
|
+
agentId: 'agent-1',
|
|
2407
|
+
orgId: 'mockOrgId',
|
|
2408
|
+
notifType: 'Wellness_Break_Handler',
|
|
2409
|
+
notifDetails: {actionEvent},
|
|
2410
|
+
data: {
|
|
2411
|
+
orgId: 'mockOrgId',
|
|
2412
|
+
agentSessionId: 'session-1',
|
|
2413
|
+
[key]: 'interaction-2',
|
|
2414
|
+
},
|
|
2415
|
+
},
|
|
2416
|
+
})
|
|
2417
|
+
);
|
|
2418
|
+
|
|
2419
|
+
expect(emitSpy).toHaveBeenCalledWith(
|
|
2420
|
+
CC_EVENTS.WELLNESS_BREAK,
|
|
2421
|
+
expect.objectContaining({actionEvent, interactionId: 'interaction-2'})
|
|
2422
|
+
);
|
|
2423
|
+
});
|
|
2424
|
+
|
|
2425
|
+
it('does not promote uncontracted notification text fields', () => {
|
|
2426
|
+
const emitSpy = jest.spyOn(webex.cc, 'emit');
|
|
2427
|
+
webex.cc['handleWebsocketMessage'](
|
|
2428
|
+
JSON.stringify({
|
|
2429
|
+
type: 'Wellness_Break_Handler',
|
|
2430
|
+
orgId: 'mockOrgId',
|
|
2431
|
+
trackingId: 'notification-tracking',
|
|
2432
|
+
data: {
|
|
2433
|
+
agentId: 'agent-1',
|
|
2434
|
+
orgId: 'mockOrgId',
|
|
2435
|
+
notifType: 'Wellness_Break_Handler',
|
|
2436
|
+
notifDetails: {
|
|
2437
|
+
actionEvent: 'SUGGEST_WELLNESS_BREAK',
|
|
2438
|
+
actionText2: 'Uncontracted text',
|
|
2439
|
+
},
|
|
2440
|
+
data: {
|
|
2441
|
+
orgId: 'mockOrgId',
|
|
2442
|
+
agentSessionId: 'session-1',
|
|
2443
|
+
InteractionId: 'interaction-qa',
|
|
2444
|
+
},
|
|
2445
|
+
},
|
|
2446
|
+
})
|
|
2447
|
+
);
|
|
2448
|
+
|
|
2449
|
+
expect(emitSpy).toHaveBeenCalledWith(CC_EVENTS.WELLNESS_BREAK, {
|
|
2450
|
+
agentId: 'agent-1',
|
|
2451
|
+
orgId: 'mockOrgId',
|
|
2452
|
+
agentSessionId: 'session-1',
|
|
2453
|
+
actionEvent: 'SUGGEST_WELLNESS_BREAK',
|
|
2454
|
+
interactionId: 'interaction-qa',
|
|
2455
|
+
trackingId: 'notification-tracking',
|
|
2456
|
+
});
|
|
2457
|
+
});
|
|
2458
|
+
|
|
2459
|
+
it('ignores wellness events when the effective flag is disabled or identity is missing', () => {
|
|
2460
|
+
const emitSpy = jest.spyOn(webex.cc, 'emit');
|
|
2461
|
+
const payload = {
|
|
2462
|
+
type: 'Wellness_Break_Handler',
|
|
2463
|
+
orgId: 'mockOrgId',
|
|
2464
|
+
data: {
|
|
2465
|
+
agentId: 'agent-1',
|
|
2466
|
+
orgId: 'mockOrgId',
|
|
2467
|
+
notifType: 'Wellness_Break_Handler',
|
|
2468
|
+
notifDetails: {actionEvent: 'SUGGEST_WELLNESS_BREAK'},
|
|
2469
|
+
data: {orgId: 'mockOrgId', agentSessionId: 'session-1'},
|
|
2470
|
+
},
|
|
2471
|
+
};
|
|
2472
|
+
|
|
2473
|
+
webex.cc.agentConfig.isWellnessBreakEnabled = false;
|
|
2474
|
+
webex.cc['handleWebsocketMessage'](JSON.stringify(payload));
|
|
2475
|
+
webex.cc.agentConfig.isWellnessBreakEnabled = true;
|
|
2476
|
+
const {orgId: omittedOrgId, ...payloadWithoutEnvelopeOrg} = payload;
|
|
2477
|
+
expect(omittedOrgId).toBe('mockOrgId');
|
|
2478
|
+
webex.cc['handleWebsocketMessage'](JSON.stringify(payloadWithoutEnvelopeOrg));
|
|
2479
|
+
|
|
2480
|
+
expect(emitSpy).not.toHaveBeenCalledWith(CC_EVENTS.WELLNESS_BREAK, expect.anything());
|
|
2481
|
+
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
|
|
2482
|
+
METRIC_EVENT_NAMES.AI_ASSISTANT_WELLNESS_EVENT_INVALID,
|
|
2483
|
+
{reason: 'wellness_disabled'},
|
|
2484
|
+
['operational']
|
|
2485
|
+
);
|
|
2486
|
+
expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
|
|
2487
|
+
METRIC_EVENT_NAMES.AI_ASSISTANT_WELLNESS_EVENT_INVALID,
|
|
2488
|
+
{reason: 'missing_wellness_identity'},
|
|
2489
|
+
['operational']
|
|
2490
|
+
);
|
|
2491
|
+
});
|
|
2492
|
+
|
|
2493
|
+
});
|
|
2494
|
+
|
|
2073
2495
|
describe('getBuddyAgents', () => {
|
|
2074
2496
|
it('should return buddy agents response when successful', async () => {
|
|
2075
2497
|
const data: BuddyAgents = {state: 'Available', mediaType: 'telephony'};
|
|
@@ -2436,6 +2858,7 @@ describe('webex.cc', () => {
|
|
|
2436
2858
|
|
|
2437
2859
|
expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
|
|
2438
2860
|
expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
|
|
2861
|
+
expect(mockTaskManager.refreshWxAppOfferObservabilityForAllTasks).toHaveBeenCalled();
|
|
2439
2862
|
});
|
|
2440
2863
|
|
|
2441
2864
|
it('should force-publish usersub false on silent relogin when enableWxBetterTogether is false at init', async () => {
|
|
@@ -2701,6 +3124,32 @@ describe('webex.cc', () => {
|
|
|
2701
3124
|
deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
|
|
2702
3125
|
});
|
|
2703
3126
|
|
|
3127
|
+
it('invalidates wellness context before deregistration cleanup can fail', async () => {
|
|
3128
|
+
const cleanupError = new Error('cleanup failed');
|
|
3129
|
+
let rejectCleanup: (error: Error) => void = () => undefined;
|
|
3130
|
+
const cleanup = new Promise((resolve, reject) => {
|
|
3131
|
+
rejectCleanup = reject;
|
|
3132
|
+
});
|
|
3133
|
+
webex.cc.agentConfig = {
|
|
3134
|
+
...webex.cc.agentConfig,
|
|
3135
|
+
isWellnessBreakEnabled: true,
|
|
3136
|
+
};
|
|
3137
|
+
webex.cc['updateWellnessSession']('session-1');
|
|
3138
|
+
jest.spyOn(webex.cc as any, 'teardownWxAppLocalState').mockReturnValue(cleanup);
|
|
3139
|
+
|
|
3140
|
+
const deregistration = webex.cc.deregister();
|
|
3141
|
+
const rejection = expect(deregistration).rejects.toThrow(cleanupError);
|
|
3142
|
+
|
|
3143
|
+
expect(webex.cc['currentAgentSessionId']).toBeUndefined();
|
|
3144
|
+
await expect(webex.cc.apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
|
|
3145
|
+
'WELLNESS_BREAK_AGENT_SESSION_REQUIRED'
|
|
3146
|
+
);
|
|
3147
|
+
|
|
3148
|
+
rejectCleanup(cleanupError);
|
|
3149
|
+
await rejection;
|
|
3150
|
+
expect(webex.cc['currentAgentSessionId']).toBeUndefined();
|
|
3151
|
+
});
|
|
3152
|
+
|
|
2704
3153
|
it('should unregister successfully and clean up all resources when webrtc is enabled', async () => {
|
|
2705
3154
|
webex.cc.services.rtdWebSocketManager = {
|
|
2706
3155
|
isSocketClosed: false,
|
|
@@ -3872,6 +4321,7 @@ describe('webex.cc', () => {
|
|
|
3872
4321
|
expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
|
|
3873
4322
|
expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(true);
|
|
3874
4323
|
expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
|
|
4324
|
+
expect(mockTaskManager.refreshWxAppOfferObservabilityForAllTasks).toHaveBeenCalled();
|
|
3875
4325
|
});
|
|
3876
4326
|
|
|
3877
4327
|
it('should publish false and update config when disabled after wxApp was enabled', async () => {
|