@webex/contact-center 3.12.0-llmrefactor.13 → 3.12.0-llmrefactor.15

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 (68) hide show
  1. package/.sdd/manifest.json +3 -2
  2. package/ai-docs/CONTRACTS.md +7 -1
  3. package/ai-docs/contact-center-spec.md +7 -7
  4. package/ai-docs/features/contact-owner-change-refactor.md +78 -0
  5. package/dist/cc.js +39 -7
  6. package/dist/cc.js.map +1 -1
  7. package/dist/metrics/behavioral-events.js +6 -0
  8. package/dist/metrics/behavioral-events.js.map +1 -1
  9. package/dist/metrics/constants.js +1 -0
  10. package/dist/metrics/constants.js.map +1 -1
  11. package/dist/services/AnswerCallOnWebexService.js +4 -4
  12. package/dist/services/AnswerCallOnWebexService.js.map +1 -1
  13. package/dist/services/WebexCrossClientService.js +5 -2
  14. package/dist/services/WebexCrossClientService.js.map +1 -1
  15. package/dist/services/WxAppTelephonyMercurySync.js +6 -3
  16. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -1
  17. package/dist/services/config/types.js.map +1 -1
  18. package/dist/services/task/TaskFactory.js +1 -0
  19. package/dist/services/task/TaskFactory.js.map +1 -1
  20. package/dist/services/task/TaskManager.js +50 -58
  21. package/dist/services/task/TaskManager.js.map +1 -1
  22. package/dist/services/task/types.js.map +1 -1
  23. package/dist/services/task/voice/Voice.js +63 -44
  24. package/dist/services/task/voice/Voice.js.map +1 -1
  25. package/dist/services/task/voice/wxAppOfferObservability.js +145 -0
  26. package/dist/services/task/voice/wxAppOfferObservability.js.map +1 -0
  27. package/dist/services/task/voice/wxAppVoiceMethods.js +52 -11
  28. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -1
  29. package/dist/services/wxAppDiagnosticLogging.js +50 -13
  30. package/dist/services/wxAppDiagnosticLogging.js.map +1 -1
  31. package/dist/types/metrics/constants.d.ts +1 -0
  32. package/dist/types/services/config/types.d.ts +2 -0
  33. package/dist/types/services/task/types.d.ts +2 -0
  34. package/dist/types/services/task/voice/Voice.d.ts +15 -2
  35. package/dist/types/services/task/voice/wxAppOfferObservability.d.ts +39 -0
  36. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +19 -6
  37. package/dist/types/services/wxAppDiagnosticLogging.d.ts +12 -0
  38. package/dist/types.js.map +1 -1
  39. package/dist/webex.js +1 -1
  40. package/package.json +9 -9
  41. package/src/cc.ts +43 -7
  42. package/src/metrics/ai-docs/metrics-spec.md +44 -2
  43. package/src/metrics/behavioral-events.ts +6 -0
  44. package/src/metrics/constants.ts +1 -0
  45. package/src/services/AnswerCallOnWebexService.ts +4 -4
  46. package/src/services/WebexCrossClientService.ts +7 -2
  47. package/src/services/WxAppTelephonyMercurySync.ts +3 -3
  48. package/src/services/config/types.ts +2 -0
  49. package/src/services/task/TaskFactory.ts +1 -0
  50. package/src/services/task/TaskManager.ts +85 -87
  51. package/src/services/task/ai-docs/task-spec.md +7 -3
  52. package/src/services/task/types.ts +16 -0
  53. package/src/services/task/voice/Voice.ts +74 -52
  54. package/src/services/task/voice/wxAppOfferObservability.ts +204 -0
  55. package/src/services/task/voice/wxAppVoiceMethods.ts +83 -24
  56. package/src/services/wxAppDiagnosticLogging.ts +135 -23
  57. package/src/types.ts +2 -0
  58. package/test/unit/spec/cc.ts +201 -19
  59. package/test/unit/spec/services/WebexCrossClientService.ts +45 -3
  60. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +1 -1
  61. package/test/unit/spec/services/task/TaskManager.ts +214 -3
  62. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +33 -0
  63. package/test/unit/spec/services/task/voice/Voice.ts +10 -51
  64. package/test/unit/spec/services/task/voice/wxAppOfferObservability.ts +361 -0
  65. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +121 -5
  66. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +44 -8
  67. package/umd/contact-center.min.js +2 -2
  68. package/umd/contact-center.min.js.map +1 -1
@@ -31,16 +31,54 @@ export function callIdSuffix(callId: string | null | undefined): string | undefi
31
31
  return callId.length <= 8 ? callId : callId.slice(-8);
32
32
  }
33
33
 
34
- function logWxApp(message: string, method: string, data: WxAppLogData): void {
35
- LoggerProxy.info(`${WXAPP_LOG_PREFIX} ${message}`, {
34
+ function formatLogSuffix(data: WxAppLogData, keys?: string[]): string {
35
+ const entries = keys
36
+ ? keys.map((key) => [key, data[key]] as const)
37
+ : Object.entries(data).filter(([key]) => key !== 'feature' && key !== 'event');
38
+
39
+ const parts = entries
40
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
41
+ .map(([key, value]) => `${key}=${String(value)}`);
42
+
43
+ return parts.length > 0 ? ` ${parts.join(' ')}` : '';
44
+ }
45
+
46
+ function logWxApp(
47
+ message: string,
48
+ method: string,
49
+ data: WxAppLogData,
50
+ suffixKeys?: string[]
51
+ ): void {
52
+ const suffix = formatLogSuffix(data, suffixKeys);
53
+ LoggerProxy.log(`${WXAPP_LOG_PREFIX} ${message}${suffix}`, {
54
+ module: 'wxAppDiagnosticLogging',
55
+ method,
56
+ data: {feature: WXAPP_LOG_FEATURE, ...data},
57
+ });
58
+ }
59
+
60
+ function logWxAppWarn(
61
+ message: string,
62
+ method: string,
63
+ data: WxAppLogData,
64
+ suffixKeys?: string[]
65
+ ): void {
66
+ const suffix = formatLogSuffix(data, suffixKeys);
67
+ LoggerProxy.warn(`${WXAPP_LOG_PREFIX} ${message}${suffix}`, {
36
68
  module: 'wxAppDiagnosticLogging',
37
69
  method,
38
70
  data: {feature: WXAPP_LOG_FEATURE, ...data},
39
71
  });
40
72
  }
41
73
 
42
- function logWxAppError(message: string, method: string, data: WxAppLogData): void {
43
- LoggerProxy.error(`${WXAPP_LOG_PREFIX} ${message}`, {
74
+ function logWxAppError(
75
+ message: string,
76
+ method: string,
77
+ data: WxAppLogData,
78
+ suffixKeys?: string[]
79
+ ): void {
80
+ const suffix = formatLogSuffix(data, suffixKeys);
81
+ LoggerProxy.error(`${WXAPP_LOG_PREFIX} ${message}${suffix}`, {
44
82
  module: 'wxAppDiagnosticLogging',
45
83
  method,
46
84
  data: {feature: WXAPP_LOG_FEATURE, ...data},
@@ -56,7 +94,7 @@ export function logWxAppSessionReadiness(params: {
56
94
  telephonyTaskType: 'Voice' | 'WebRTC' | 'unknown';
57
95
  skipReason?: WxAppSessionSkipReason;
58
96
  }): void {
59
- logWxApp('session readiness', 'logWxAppSessionReadiness', {
97
+ const data: WxAppLogData = {
60
98
  event: 'session_readiness',
61
99
  enableWxBetterTogether: params.enableWxBetterTogether,
62
100
  loginOption: params.loginOption,
@@ -65,7 +103,16 @@ export function logWxAppSessionReadiness(params: {
65
103
  mercurySubscribed: params.mercurySubscribed,
66
104
  telephonyTaskType: params.telephonyTaskType,
67
105
  skipReason: params.skipReason,
68
- });
106
+ };
107
+
108
+ logWxApp('session readiness', 'logWxAppSessionReadiness', data, [
109
+ 'usersubPublished',
110
+ 'mercurySubscribed',
111
+ 'loginOption',
112
+ 'enableWxBetterTogether',
113
+ 'telephonyTaskType',
114
+ 'skipReason',
115
+ ]);
69
116
  }
70
117
 
71
118
  export function deriveWxAppAcceptReason(params: {
@@ -111,8 +158,10 @@ export function logWxAppOfferDecision(params: {
111
158
  acceptReason: WxAppAcceptReason;
112
159
  wxAppParticipantDeviceType?: string;
113
160
  hasDeviceCallId: boolean;
161
+ hasDeviceId?: boolean;
162
+ usersubPublished?: boolean;
114
163
  }): void {
115
- logWxApp('offer decision', 'logWxAppOfferDecision', {
164
+ const data: WxAppLogData = {
116
165
  event: 'offer_decision',
117
166
  interactionId: params.interactionId,
118
167
  acceptVisible: params.acceptVisible,
@@ -120,7 +169,51 @@ export function logWxAppOfferDecision(params: {
120
169
  acceptReason: params.acceptReason,
121
170
  wxAppParticipantDeviceType: params.wxAppParticipantDeviceType,
122
171
  hasDeviceCallId: params.hasDeviceCallId,
123
- });
172
+ hasDeviceId: params.hasDeviceId,
173
+ usersubPublished: params.usersubPublished,
174
+ };
175
+
176
+ logWxApp('offer decision', 'logWxAppOfferDecision', data, [
177
+ 'acceptReason',
178
+ 'hasDeviceCallId',
179
+ 'hasDeviceId',
180
+ 'usersubPublished',
181
+ 'interactionId',
182
+ 'acceptVisible',
183
+ 'acceptEnabled',
184
+ ]);
185
+ }
186
+
187
+ export function logWxAppOfferParticipantMismatch(params: {
188
+ interactionId: string;
189
+ usersubPublished: boolean;
190
+ hasDeviceCallId: boolean;
191
+ hasDeviceId: boolean;
192
+ participantDeviceType?: string;
193
+ acceptVisible: boolean;
194
+ acceptEnabled: boolean;
195
+ acceptReason: WxAppAcceptReason;
196
+ }): void {
197
+ const data: WxAppLogData = {
198
+ event: 'participant_fields_mismatch',
199
+ interactionId: params.interactionId,
200
+ usersubPublished: params.usersubPublished,
201
+ hasDeviceCallId: params.hasDeviceCallId,
202
+ hasDeviceId: params.hasDeviceId,
203
+ participantDeviceType: params.participantDeviceType ?? 'missing',
204
+ acceptVisible: params.acceptVisible,
205
+ acceptEnabled: params.acceptEnabled,
206
+ acceptReason: params.acceptReason,
207
+ };
208
+
209
+ logWxAppWarn('participant fields mismatch', 'logWxAppOfferParticipantMismatch', data, [
210
+ 'usersubPublished',
211
+ 'hasDeviceCallId',
212
+ 'hasDeviceId',
213
+ 'acceptReason',
214
+ 'interactionId',
215
+ 'participantDeviceType',
216
+ ]);
124
217
  }
125
218
 
126
219
  export function logWxAppTelephonyAction(params: {
@@ -142,18 +235,32 @@ export function logWxAppTelephonyAction(params: {
142
235
  };
143
236
 
144
237
  if (params.phase === 'failed') {
145
- logWxAppError(`telephony ${params.action} ${params.phase}`, 'logWxAppTelephonyAction', payload);
238
+ logWxAppError(
239
+ `telephony ${params.action} ${params.phase}`,
240
+ 'logWxAppTelephonyAction',
241
+ payload,
242
+ ['interactionId', 'trackingId', 'httpStatus', 'failureReason']
243
+ );
146
244
  } else {
147
- logWxApp(`telephony ${params.action} ${params.phase}`, 'logWxAppTelephonyAction', payload);
245
+ logWxApp(`telephony ${params.action} ${params.phase}`, 'logWxAppTelephonyAction', payload, [
246
+ 'interactionId',
247
+ 'action',
248
+ 'phase',
249
+ ]);
148
250
  }
149
251
  }
150
252
 
151
253
  export function logWxAppValidationFailure(reason: string, interactionId?: string): void {
152
- logWxAppError('validation failed', 'logWxAppValidationFailure', {
153
- event: 'validation_failed',
154
- reason,
155
- interactionId,
156
- });
254
+ logWxAppError(
255
+ 'validation failed',
256
+ 'logWxAppValidationFailure',
257
+ {
258
+ event: 'validation_failed',
259
+ reason,
260
+ interactionId,
261
+ },
262
+ ['reason', 'interactionId']
263
+ );
157
264
  }
158
265
 
159
266
  export function logWxAppMercuryMuteSync(params: {
@@ -163,12 +270,17 @@ export function logWxAppMercuryMuteSync(params: {
163
270
  interactionId?: string;
164
271
  dropReason?: string;
165
272
  }): void {
166
- logWxApp(`mercury mute sync ${params.phase}`, 'logWxAppMercuryMuteSync', {
167
- event: 'mercury_mute_sync',
168
- phase: params.phase,
169
- muted: params.muted,
170
- callIdSuffix: params.callIdSuffix,
171
- interactionId: params.interactionId,
172
- dropReason: params.dropReason,
173
- });
273
+ logWxApp(
274
+ `mercury mute sync ${params.phase}`,
275
+ 'logWxAppMercuryMuteSync',
276
+ {
277
+ event: 'mercury_mute_sync',
278
+ phase: params.phase,
279
+ muted: params.muted,
280
+ callIdSuffix: params.callIdSuffix,
281
+ interactionId: params.interactionId,
282
+ dropReason: params.dropReason,
283
+ },
284
+ ['phase', 'muted', 'callIdSuffix', 'interactionId', 'dropReason']
285
+ );
174
286
  }
package/src/types.ts CHANGED
@@ -618,6 +618,8 @@ export type ConfigFlags = {
618
618
  isRecordingEnabled?: boolean;
619
619
  /** Whether wxApp thick-client answer controls/APIs are enabled (WXCC-6026) */
620
620
  enableWxBetterTogether?: boolean;
621
+ /** Read-only observability: live usersub answer-calls-on-wxcc state for wxApp offer diagnostics. */
622
+ getWxAppUsersubPublished?: () => boolean;
621
623
  };
622
624
 
623
625
  /**
@@ -171,12 +171,14 @@ describe('webex.cc', () => {
171
171
  setAgentId: jest.fn(),
172
172
  setWebRtcEnabled: jest.fn(),
173
173
  setAnswerCallOnWebexService: jest.fn(),
174
+ handleRealtimeWebsocketEvent: jest.fn(),
174
175
  registerIncomingCallEvent: jest.fn(),
175
176
  registerTaskListeners: jest.fn(),
176
177
  getTask: jest.fn(),
177
178
  getActiveTasks: jest.fn(),
178
179
  applyEnableWxBetterTogether: jest.fn(),
179
180
  syncWxAppMuteFromCallDetailsForAllTasks: jest.fn(),
181
+ refreshWxAppOfferObservabilityForAllTasks: jest.fn(),
180
182
  on: jest.fn(),
181
183
  off: jest.fn(),
182
184
  emit: jest.fn(),
@@ -334,6 +336,7 @@ describe('webex.cc', () => {
334
336
  data: {
335
337
  auxCodeId: 'auxCodeId',
336
338
  agentId: 'agentId',
339
+ agentSessionId: 'session-after-refresh',
337
340
  deviceType: LoginOption.EXTENSION,
338
341
  dn: '12345',
339
342
  },
@@ -406,22 +409,25 @@ describe('webex.cc', () => {
406
409
  module: CC_FILE,
407
410
  method: 'connectWebsocket',
408
411
  });
409
- expect(mockTaskManager.setConfigFlags).toHaveBeenCalledWith({
410
- isEndTaskEnabled: mockAgentProfile.isEndTaskEnabled,
411
- isEndConsultEnabled: mockAgentProfile.isEndConsultEnabled,
412
- webRtcEnabled: mockAgentProfile.webRtcEnabled,
413
- autoWrapup: mockAgentProfile.wrapUpData.wrapUpProps.autoWrapup ?? false,
414
- aiFeature: mockAgentProfile.aiFeature,
415
- consultTransfer: {
416
- allowConsultToQueue: mockAgentProfile.allowConsultToQueue,
417
- accessQueue: mockAgentProfile.accessQueue,
418
- accessEntryPoint: mockAgentProfile.accessEntryPoint,
419
- accessBuddyTeam: mockAgentProfile.accessBuddyTeam,
420
- },
421
- enableWxBetterTogether: false,
422
- });
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
+ );
423
429
  expect(reloadSpy).toHaveBeenCalled();
424
- expect(result).toEqual(mockAgentProfile);
430
+ expect(result).toEqual({...mockAgentProfile, agentSessionId: 'session-after-refresh'});
425
431
  expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
426
432
  METRIC_EVENT_NAMES.WEBSOCKET_REGISTER_SUCCESS,
427
433
  METRIC_EVENT_NAMES.WEBSOCKET_REGISTER_FAILED,
@@ -653,8 +659,47 @@ describe('webex.cc', () => {
653
659
  expect(result).toEqual(mockAgentProfile);
654
660
  });
655
661
 
656
- it('should not connect RTD websocket when realtime transcripts feature is disabled', async () => {
657
- 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;
658
703
  jest.spyOn(webex.internal.mercury, 'connect').mockResolvedValue(true);
659
704
  jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
660
705
  data: {
@@ -1097,6 +1142,7 @@ describe('webex.cc', () => {
1097
1142
 
1098
1143
  expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
1099
1144
  expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
1145
+ expect(mockTaskManager.refreshWxAppOfferObservabilityForAllTasks).toHaveBeenCalled();
1100
1146
  });
1101
1147
 
1102
1148
  it('should rollback wxApp config when post-station-login init fails', async () => {
@@ -1227,7 +1273,57 @@ describe('webex.cc', () => {
1227
1273
 
1228
1274
  expect(metricSpy).toHaveBeenCalledWith(
1229
1275
  METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1230
- 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}),
1231
1327
  ['operational', 'behavioral']
1232
1328
  );
1233
1329
  expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
@@ -1639,7 +1735,61 @@ describe('webex.cc', () => {
1639
1735
  );
1640
1736
  expect(metricSpy).toHaveBeenCalledWith(
1641
1737
  METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
1642
- 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}),
1643
1793
  ['operational', 'behavioral']
1644
1794
  );
1645
1795
  expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
@@ -2213,6 +2363,36 @@ describe('webex.cc', () => {
2213
2363
  });
2214
2364
  });
2215
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
+
2216
2396
  it.each([
2217
2397
  ['SUGGEST_WELLNESS_BREAK', 'interactionId'],
2218
2398
  ['WELLNESS_BREAK_NOT_ALLOWED', 'InteractionId'],
@@ -2678,6 +2858,7 @@ describe('webex.cc', () => {
2678
2858
 
2679
2859
  expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
2680
2860
  expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
2861
+ expect(mockTaskManager.refreshWxAppOfferObservabilityForAllTasks).toHaveBeenCalled();
2681
2862
  });
2682
2863
 
2683
2864
  it('should force-publish usersub false on silent relogin when enableWxBetterTogether is false at init', async () => {
@@ -4140,6 +4321,7 @@ describe('webex.cc', () => {
4140
4321
  expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
4141
4322
  expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(true);
4142
4323
  expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
4324
+ expect(mockTaskManager.refreshWxAppOfferObservabilityForAllTasks).toHaveBeenCalled();
4143
4325
  });
4144
4326
 
4145
4327
  it('should publish false and update config when disabled after wxApp was enabled', async () => {
@@ -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
  });