@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.
Files changed (107) hide show
  1. package/.sdd/manifest.json +6 -5
  2. package/ai-docs/CONTRACTS.md +8 -0
  3. package/ai-docs/SERVICE_STATE.md +4 -0
  4. package/ai-docs/contact-center-spec.md +26 -3
  5. package/ai-docs/features/contact-owner-change-refactor.md +78 -0
  6. package/dist/cc.js +202 -9
  7. package/dist/cc.js.map +1 -1
  8. package/dist/constants.js +5 -1
  9. package/dist/constants.js.map +1 -1
  10. package/dist/index.js +25 -10
  11. package/dist/index.js.map +1 -1
  12. package/dist/metrics/behavioral-events.js +6 -0
  13. package/dist/metrics/behavioral-events.js.map +1 -1
  14. package/dist/metrics/constants.js +6 -0
  15. package/dist/metrics/constants.js.map +1 -1
  16. package/dist/services/AnswerCallOnWebexService.js +4 -4
  17. package/dist/services/AnswerCallOnWebexService.js.map +1 -1
  18. package/dist/services/ApiAiAssistant.js +116 -1
  19. package/dist/services/ApiAiAssistant.js.map +1 -1
  20. package/dist/services/WebexCrossClientService.js +5 -2
  21. package/dist/services/WebexCrossClientService.js.map +1 -1
  22. package/dist/services/WxAppTelephonyMercurySync.js +6 -3
  23. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -1
  24. package/dist/services/config/Util.js +5 -1
  25. package/dist/services/config/Util.js.map +1 -1
  26. package/dist/services/config/constants.js +10 -1
  27. package/dist/services/config/constants.js.map +1 -1
  28. package/dist/services/config/index.js +51 -0
  29. package/dist/services/config/index.js.map +1 -1
  30. package/dist/services/config/types.js +4 -3
  31. package/dist/services/config/types.js.map +1 -1
  32. package/dist/services/core/Utils.js +5 -1
  33. package/dist/services/core/Utils.js.map +1 -1
  34. package/dist/services/task/TaskFactory.js +1 -0
  35. package/dist/services/task/TaskFactory.js.map +1 -1
  36. package/dist/services/task/TaskManager.js +50 -58
  37. package/dist/services/task/TaskManager.js.map +1 -1
  38. package/dist/services/task/types.js.map +1 -1
  39. package/dist/services/task/voice/Voice.js +63 -44
  40. package/dist/services/task/voice/Voice.js.map +1 -1
  41. package/dist/services/task/voice/wxAppOfferObservability.js +145 -0
  42. package/dist/services/task/voice/wxAppOfferObservability.js.map +1 -0
  43. package/dist/services/task/voice/wxAppVoiceMethods.js +52 -11
  44. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -1
  45. package/dist/services/wxAppDiagnosticLogging.js +50 -13
  46. package/dist/services/wxAppDiagnosticLogging.js.map +1 -1
  47. package/dist/types/cc.d.ts +21 -1
  48. package/dist/types/constants.d.ts +4 -0
  49. package/dist/types/index.d.ts +11 -1
  50. package/dist/types/metrics/constants.d.ts +6 -0
  51. package/dist/types/services/ApiAiAssistant.d.ts +35 -2
  52. package/dist/types/services/config/constants.d.ts +5 -0
  53. package/dist/types/services/config/types.d.ts +15 -2
  54. package/dist/types/services/core/Utils.d.ts +2 -0
  55. package/dist/types/services/task/types.d.ts +2 -0
  56. package/dist/types/services/task/voice/Voice.d.ts +15 -2
  57. package/dist/types/services/task/voice/wxAppOfferObservability.d.ts +39 -0
  58. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +19 -6
  59. package/dist/types/services/wxAppDiagnosticLogging.d.ts +12 -0
  60. package/dist/types/types.d.ts +56 -1
  61. package/dist/types.js +39 -2
  62. package/dist/types.js.map +1 -1
  63. package/dist/webex.js +9 -1
  64. package/dist/webex.js.map +1 -1
  65. package/package.json +9 -9
  66. package/src/cc.ts +294 -25
  67. package/src/constants.ts +4 -0
  68. package/src/index.ts +11 -0
  69. package/src/metrics/ai-docs/metrics-spec.md +61 -20
  70. package/src/metrics/behavioral-events.ts +6 -0
  71. package/src/metrics/constants.ts +6 -0
  72. package/src/services/AnswerCallOnWebexService.ts +4 -4
  73. package/src/services/ApiAiAssistant.ts +153 -1
  74. package/src/services/WebexCrossClientService.ts +7 -2
  75. package/src/services/WxAppTelephonyMercurySync.ts +3 -3
  76. package/src/services/ai-docs/services-spec.md +11 -3
  77. package/src/services/config/Util.ts +6 -0
  78. package/src/services/config/ai-docs/config-spec.md +14 -3
  79. package/src/services/config/constants.ts +12 -0
  80. package/src/services/config/index.ts +65 -0
  81. package/src/services/config/types.ts +13 -2
  82. package/src/services/core/Utils.ts +4 -0
  83. package/src/services/task/TaskFactory.ts +1 -0
  84. package/src/services/task/TaskManager.ts +85 -87
  85. package/src/services/task/ai-docs/task-spec.md +7 -3
  86. package/src/services/task/types.ts +16 -0
  87. package/src/services/task/voice/Voice.ts +74 -52
  88. package/src/services/task/voice/wxAppOfferObservability.ts +204 -0
  89. package/src/services/task/voice/wxAppVoiceMethods.ts +83 -24
  90. package/src/services/wxAppDiagnosticLogging.ts +135 -23
  91. package/src/types.ts +65 -0
  92. package/src/webex.js +8 -0
  93. package/test/unit/spec/cc.ts +469 -19
  94. package/test/unit/spec/services/ApiAiAssistant.ts +134 -2
  95. package/test/unit/spec/services/WebexCrossClientService.ts +45 -3
  96. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +1 -1
  97. package/test/unit/spec/services/config/Util.ts +44 -0
  98. package/test/unit/spec/services/config/index.ts +68 -0
  99. package/test/unit/spec/services/task/TaskManager.ts +214 -3
  100. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +33 -0
  101. package/test/unit/spec/services/task/voice/Voice.ts +10 -51
  102. package/test/unit/spec/services/task/voice/wxAppOfferObservability.ts +361 -0
  103. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +121 -5
  104. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +44 -8
  105. package/test/unit/spec/wellnessSampleUtils.ts +288 -0
  106. package/umd/contact-center.min.js +2 -2
  107. package/umd/contact-center.min.js.map +1 -1
@@ -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
- isEndTaskEnabled: mockAgentProfile.isEndTaskEnabled,
409
- isEndConsultEnabled: mockAgentProfile.isEndConsultEnabled,
410
- webRtcEnabled: mockAgentProfile.webRtcEnabled,
411
- autoWrapup: mockAgentProfile.wrapUpData.wrapUpProps.autoWrapup ?? false,
412
- aiFeature: mockAgentProfile.aiFeature,
413
- consultTransfer: {
414
- allowConsultToQueue: mockAgentProfile.allowConsultToQueue,
415
- accessQueue: mockAgentProfile.accessQueue,
416
- accessEntryPoint: mockAgentProfile.accessEntryPoint,
417
- accessBuddyTeam: mockAgentProfile.accessBuddyTeam,
418
- },
419
- enableWxBetterTogether: false,
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 not connect RTD websocket when realtime transcripts feature is disabled', async () => {
636
- mockAgentProfile.aiFeature = {realtimeTranscripts: {enable: false}} as any;
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 () => {