@webex/contact-center 3.12.0-llmrefactor.1 → 3.12.0-llmrefactor.11

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 (198) hide show
  1. package/.sdd/manifest.json +7 -5
  2. package/ai-docs/ARCHITECTURE.md +1 -1
  3. package/ai-docs/CONTRACTS.md +8 -3
  4. package/ai-docs/SECURITY.md +1 -1
  5. package/ai-docs/contact-center-spec.md +22 -4
  6. package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
  7. package/dist/cc.js +544 -12
  8. package/dist/cc.js.map +1 -1
  9. package/dist/config.js +7 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +10 -2
  12. package/dist/constants.js.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/metrics/MetricsManager.js +16 -0
  15. package/dist/metrics/MetricsManager.js.map +1 -1
  16. package/dist/metrics/behavioral-events.js +104 -0
  17. package/dist/metrics/behavioral-events.js.map +1 -1
  18. package/dist/metrics/constants.js +18 -0
  19. package/dist/metrics/constants.js.map +1 -1
  20. package/dist/services/AddressBook.js +18 -15
  21. package/dist/services/AddressBook.js.map +1 -1
  22. package/dist/services/AnswerCallOnWebexService.js +202 -0
  23. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  24. package/dist/services/EntryPoint.js +46 -68
  25. package/dist/services/EntryPoint.js.map +1 -1
  26. package/dist/services/Queue.js +27 -22
  27. package/dist/services/Queue.js.map +1 -1
  28. package/dist/services/WebexCrossClientService.js +222 -0
  29. package/dist/services/WebexCrossClientService.js.map +1 -0
  30. package/dist/services/WxAppTelephonyMercurySync.js +108 -0
  31. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  32. package/dist/services/config/Util.js +3 -0
  33. package/dist/services/config/Util.js.map +1 -1
  34. package/dist/services/config/constants.js +10 -6
  35. package/dist/services/config/constants.js.map +1 -1
  36. package/dist/services/config/types.js +4 -0
  37. package/dist/services/config/types.js.map +1 -1
  38. package/dist/services/core/Err.js.map +1 -1
  39. package/dist/services/core/WebexRequest.js +6 -2
  40. package/dist/services/core/WebexRequest.js.map +1 -1
  41. package/dist/services/core/aqm-reqs.js +28 -14
  42. package/dist/services/core/aqm-reqs.js.map +1 -1
  43. package/dist/services/core/types.js.map +1 -1
  44. package/dist/services/task/Task.js +73 -7
  45. package/dist/services/task/Task.js.map +1 -1
  46. package/dist/services/task/TaskFactory.js +8 -4
  47. package/dist/services/task/TaskFactory.js.map +1 -1
  48. package/dist/services/task/TaskManager.js +490 -21
  49. package/dist/services/task/TaskManager.js.map +1 -1
  50. package/dist/services/task/WebexCallingUtils.js +70 -0
  51. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  52. package/dist/services/task/constants.js +4 -1
  53. package/dist/services/task/constants.js.map +1 -1
  54. package/dist/services/task/contact.js +29 -0
  55. package/dist/services/task/contact.js.map +1 -1
  56. package/dist/services/task/digital/Digital.js +3 -2
  57. package/dist/services/task/digital/Digital.js.map +1 -1
  58. package/dist/services/task/state-machine/TaskStateMachine.js +162 -53
  59. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  60. package/dist/services/task/state-machine/actions.js +6 -1
  61. package/dist/services/task/state-machine/actions.js.map +1 -1
  62. package/dist/services/task/state-machine/guards.js +7 -10
  63. package/dist/services/task/state-machine/guards.js.map +1 -1
  64. package/dist/services/task/state-machine/types.js.map +1 -1
  65. package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
  66. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  67. package/dist/services/task/types.js +16 -1
  68. package/dist/services/task/types.js.map +1 -1
  69. package/dist/services/task/voice/Voice.js +456 -15
  70. package/dist/services/task/voice/Voice.js.map +1 -1
  71. package/dist/services/task/voice/WebRTC.js +4 -1
  72. package/dist/services/task/voice/WebRTC.js.map +1 -1
  73. package/dist/services/task/voice/wxAppVoiceMethods.js +354 -0
  74. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  75. package/dist/services/wxAppDiagnosticLogging.js +122 -0
  76. package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
  77. package/dist/services/wxAppTelephonyUtils.js +19 -0
  78. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  79. package/dist/types/cc.d.ts +78 -4
  80. package/dist/types/config.d.ts +7 -0
  81. package/dist/types/constants.d.ts +8 -0
  82. package/dist/types/index.d.ts +3 -1
  83. package/dist/types/metrics/MetricsManager.d.ts +6 -0
  84. package/dist/types/metrics/constants.d.ts +17 -0
  85. package/dist/types/services/AddressBook.d.ts +2 -1
  86. package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -0
  87. package/dist/types/services/EntryPoint.d.ts +7 -5
  88. package/dist/types/services/Queue.d.ts +5 -3
  89. package/dist/types/services/WebexCrossClientService.d.ts +32 -0
  90. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  91. package/dist/types/services/config/constants.d.ts +8 -5
  92. package/dist/types/services/config/types.d.ts +17 -4
  93. package/dist/types/services/core/Err.d.ts +2 -0
  94. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  95. package/dist/types/services/core/types.d.ts +2 -0
  96. package/dist/types/services/task/Task.d.ts +20 -2
  97. package/dist/types/services/task/TaskFactory.d.ts +2 -1
  98. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  99. package/dist/types/services/task/constants.d.ts +3 -0
  100. package/dist/types/services/task/contact.d.ts +4 -0
  101. package/dist/types/services/task/digital/Digital.d.ts +2 -2
  102. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +304 -56
  103. package/dist/types/services/task/state-machine/guards.d.ts +6 -5
  104. package/dist/types/services/task/state-machine/types.d.ts +9 -1
  105. package/dist/types/services/task/types.d.ts +81 -4
  106. package/dist/types/services/task/voice/Voice.d.ts +52 -9
  107. package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
  108. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +55 -0
  109. package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
  110. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  111. package/dist/types/types.d.ts +14 -8
  112. package/dist/types/utils/PageCache.d.ts +20 -3
  113. package/dist/types.js +7 -2
  114. package/dist/types.js.map +1 -1
  115. package/dist/utils/PageCache.js +19 -5
  116. package/dist/utils/PageCache.js.map +1 -1
  117. package/dist/webex.js +1 -1
  118. package/package.json +8 -8
  119. package/src/cc.ts +657 -12
  120. package/src/config.ts +7 -0
  121. package/src/constants.ts +8 -0
  122. package/src/index.ts +2 -0
  123. package/src/metrics/MetricsManager.ts +16 -0
  124. package/src/metrics/ai-docs/metrics-spec.md +78 -10
  125. package/src/metrics/behavioral-events.ts +106 -0
  126. package/src/metrics/constants.ts +20 -0
  127. package/src/services/AddressBook.ts +17 -6
  128. package/src/services/AnswerCallOnWebexService.ts +239 -0
  129. package/src/services/EntryPoint.ts +59 -60
  130. package/src/services/Queue.ts +29 -12
  131. package/src/services/WebexCrossClientService.ts +293 -0
  132. package/src/services/WxAppTelephonyMercurySync.ts +142 -0
  133. package/src/services/agent/ai-docs/agent-spec.md +1 -0
  134. package/src/services/ai-docs/AGENTS.md +10 -10
  135. package/src/services/ai-docs/services-spec.md +6 -1
  136. package/src/services/config/Util.ts +3 -0
  137. package/src/services/config/ai-docs/AGENTS.md +1 -1
  138. package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
  139. package/src/services/config/ai-docs/config-spec.md +6 -0
  140. package/src/services/config/constants.ts +10 -6
  141. package/src/services/config/types.ts +16 -4
  142. package/src/services/core/Err.ts +1 -0
  143. package/src/services/core/WebexRequest.ts +3 -1
  144. package/src/services/core/ai-docs/core-spec.md +5 -1
  145. package/src/services/core/aqm-reqs.ts +30 -15
  146. package/src/services/core/types.ts +2 -0
  147. package/src/services/task/Task.ts +79 -8
  148. package/src/services/task/TaskFactory.ts +8 -3
  149. package/src/services/task/TaskManager.ts +716 -17
  150. package/src/services/task/WebexCallingUtils.ts +136 -0
  151. package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
  152. package/src/services/task/ai-docs/task-spec.md +156 -3
  153. package/src/services/task/constants.ts +3 -0
  154. package/src/services/task/contact.ts +30 -0
  155. package/src/services/task/digital/Digital.ts +4 -2
  156. package/src/services/task/state-machine/TaskStateMachine.ts +235 -81
  157. package/src/services/task/state-machine/actions.ts +5 -2
  158. package/src/services/task/state-machine/ai-docs/AGENTS.md +6 -4
  159. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +35 -16
  160. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +103 -33
  161. package/src/services/task/state-machine/guards.ts +7 -10
  162. package/src/services/task/state-machine/types.ts +16 -1
  163. package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
  164. package/src/services/task/types.ts +114 -4
  165. package/src/services/task/voice/Voice.ts +595 -14
  166. package/src/services/task/voice/WebRTC.ts +5 -1
  167. package/src/services/task/voice/wxAppVoiceMethods.ts +516 -0
  168. package/src/services/wxAppDiagnosticLogging.ts +174 -0
  169. package/src/services/wxAppTelephonyUtils.ts +14 -0
  170. package/src/types.ts +33 -9
  171. package/src/utils/AGENTS.md +21 -10
  172. package/src/utils/PageCache.ts +38 -5
  173. package/src/utils/ai-docs/utils-spec.md +21 -11
  174. package/test/unit/spec/cc.ts +1522 -19
  175. package/test/unit/spec/metrics/MetricsManager.ts +25 -0
  176. package/test/unit/spec/metrics/behavioral-events.ts +60 -0
  177. package/test/unit/spec/services/AddressBook.ts +37 -6
  178. package/test/unit/spec/services/AnswerCallOnWebexService.ts +264 -0
  179. package/test/unit/spec/services/EntryPoint.ts +87 -40
  180. package/test/unit/spec/services/Queue.ts +123 -12
  181. package/test/unit/spec/services/WebexCrossClientService.ts +467 -0
  182. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +136 -0
  183. package/test/unit/spec/services/config/Util.ts +85 -0
  184. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  185. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  186. package/test/unit/spec/services/task/Task.ts +274 -6
  187. package/test/unit/spec/services/task/TaskFactory.ts +39 -2
  188. package/test/unit/spec/services/task/TaskManager.ts +1717 -9
  189. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  190. package/test/unit/spec/services/task/contact.ts +33 -0
  191. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +1485 -109
  192. package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
  193. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
  194. package/test/unit/spec/services/task/voice/Voice.ts +1412 -0
  195. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +603 -0
  196. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
  197. package/umd/contact-center.min.js +2 -2
  198. package/umd/contact-center.min.js.map +1 -1
@@ -169,10 +169,13 @@ describe('webex.cc', () => {
169
169
  setWrapupData: jest.fn(),
170
170
  setAgentId: jest.fn(),
171
171
  setWebRtcEnabled: jest.fn(),
172
+ setAnswerCallOnWebexService: jest.fn(),
172
173
  registerIncomingCallEvent: jest.fn(),
173
174
  registerTaskListeners: jest.fn(),
174
175
  getTask: jest.fn(),
175
176
  getActiveTasks: jest.fn(),
177
+ applyEnableWxBetterTogether: jest.fn(),
178
+ syncWxAppMuteFromCallDetailsForAllTasks: jest.fn(),
176
179
  on: jest.fn(),
177
180
  off: jest.fn(),
178
181
  emit: jest.fn(),
@@ -311,6 +314,9 @@ describe('webex.cc', () => {
311
314
  isEndConsultEnabled: false,
312
315
  agentDbId: '',
313
316
  allowConsultToQueue: false,
317
+ accessQueue: 'SPECIFIC',
318
+ accessEntryPoint: 'ALL',
319
+ accessBuddyTeam: 'ALL',
314
320
  agentPersonalStatsEnabled: false,
315
321
  isTimeoutDesktopInactivityEnabled: false,
316
322
  webRtcEnabled: true,
@@ -404,6 +410,13 @@ describe('webex.cc', () => {
404
410
  webRtcEnabled: mockAgentProfile.webRtcEnabled,
405
411
  autoWrapup: mockAgentProfile.wrapUpData.wrapUpProps.autoWrapup ?? false,
406
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,
407
420
  });
408
421
  expect(reloadSpy).toHaveBeenCalled();
409
422
  expect(result).toEqual(mockAgentProfile);
@@ -716,7 +729,7 @@ describe('webex.cc', () => {
716
729
  expect(registerWebCallingLineSpy).toHaveBeenCalled();
717
730
  expect(stationLoginMock).toHaveBeenCalledWith({
718
731
  data: {
719
- dialNumber: 'agentId',
732
+ dialNumber: `${WEB_RTC_PREFIX}agentId`,
720
733
  teamId: 'teamId',
721
734
  deviceType: LoginOption.BROWSER,
722
735
  isExtension: false,
@@ -734,6 +747,7 @@ describe('webex.cc', () => {
734
747
  METRIC_EVENT_NAMES.STATION_LOGIN_FAILED,
735
748
  ]);
736
749
  expect(result).toEqual(responseMock);
750
+ expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.BROWSER);
737
751
 
738
752
  const onSpy = jest.spyOn(mockTaskManager, 'on');
739
753
  const emitSpy = jest.spyOn(webex.cc, 'trigger');
@@ -875,7 +889,7 @@ describe('webex.cc', () => {
875
889
  expect(registerWebCallingLineSpy).not.toHaveBeenCalled();
876
890
  expect(stationLoginSpy).toHaveBeenCalledWith({
877
891
  data: {
878
- dialNumber: 'agentId',
892
+ dialNumber: `${WEB_RTC_PREFIX}agentId`,
879
893
  teamId: 'teamId',
880
894
  deviceType: LoginOption.BROWSER,
881
895
  isExtension: false,
@@ -1019,6 +1033,639 @@ describe('webex.cc', () => {
1019
1033
  {module: CC_FILE, method: 'stationLogin', trackingId: error.details.trackingId}
1020
1034
  );
1021
1035
  });
1036
+
1037
+ it('should publish usersub and subscribe Mercury on station login when enableWxBetterTogether is true at init', async () => {
1038
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1039
+ webex.internal.device = {
1040
+ userId: 'user-123',
1041
+ url: 'https://wdm.example.com/devices/dev-1',
1042
+ registered: true,
1043
+ register: jest.fn().mockResolvedValue(undefined),
1044
+ };
1045
+ webex.internal.mercury.connected = true;
1046
+ webex.internal.mercury.connect = jest.fn().mockResolvedValue(undefined);
1047
+ webex.cc.agentConfig = {
1048
+ agentId: 'agentId',
1049
+ webRtcEnabled: false,
1050
+ loginVoiceOptions: ['EXTENSION'],
1051
+ };
1052
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1053
+
1054
+ const publishSpy = jest
1055
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1056
+ .mockResolvedValue(undefined);
1057
+ const subscribeSpy = jest
1058
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
1059
+ .mockImplementation(() => {});
1060
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1061
+
1062
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1063
+ data: {
1064
+ agentId: 'agentId',
1065
+ teamId: 'teamId',
1066
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1067
+ },
1068
+ trackingId: 'track-1',
1069
+ } as StationLoginSuccess);
1070
+
1071
+ await webex.cc.stationLogin({
1072
+ teamId: 'teamId',
1073
+ loginOption: LoginOption.EXTENSION,
1074
+ dialNumber: '1001',
1075
+ });
1076
+
1077
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
1078
+ expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
1079
+ });
1080
+
1081
+ it('should rollback wxApp config when post-station-login init fails', async () => {
1082
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1083
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1084
+ webex.cc.agentConfig = {
1085
+ agentId: 'agentId',
1086
+ webRtcEnabled: false,
1087
+ loginVoiceOptions: ['EXTENSION'],
1088
+ };
1089
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1090
+ mockTaskManager.applyEnableWxBetterTogether(true);
1091
+
1092
+ jest
1093
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
1094
+ .mockRejectedValue(new Error('usersub publish failed'));
1095
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1096
+
1097
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1098
+ data: {
1099
+ agentId: 'agentId',
1100
+ teamId: 'teamId',
1101
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1102
+ },
1103
+ trackingId: 'track-1',
1104
+ } as StationLoginSuccess);
1105
+
1106
+ await webex.cc.stationLogin({
1107
+ teamId: 'teamId',
1108
+ loginOption: LoginOption.EXTENSION,
1109
+ dialNumber: '1001',
1110
+ });
1111
+
1112
+ expect(unsubscribeSpy).toHaveBeenCalled();
1113
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1114
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
1115
+ });
1116
+
1117
+ it('should reset wxApp config when Mercury fails before usersub publish on station login', async () => {
1118
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1119
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1120
+ webex.cc.agentConfig = {
1121
+ agentId: 'agentId',
1122
+ webRtcEnabled: false,
1123
+ loginVoiceOptions: ['EXTENSION'],
1124
+ };
1125
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1126
+ mockTaskManager.applyEnableWxBetterTogether(true);
1127
+
1128
+ const publishSpy = jest
1129
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1130
+ .mockResolvedValue(undefined);
1131
+ jest
1132
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
1133
+ .mockRejectedValue(new Error('mercury connect failed'));
1134
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1135
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1136
+ jest
1137
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1138
+ .mockResolvedValue(undefined);
1139
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1140
+
1141
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1142
+ data: {
1143
+ agentId: 'agentId',
1144
+ teamId: 'teamId',
1145
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1146
+ },
1147
+ trackingId: 'track-1',
1148
+ } as StationLoginSuccess);
1149
+
1150
+ await webex.cc.stationLogin({
1151
+ teamId: 'teamId',
1152
+ loginOption: LoginOption.EXTENSION,
1153
+ dialNumber: '1001',
1154
+ });
1155
+
1156
+ expect(publishSpy).not.toHaveBeenCalled();
1157
+ expect(teardownSpy).not.toHaveBeenCalled();
1158
+ expect(unsubscribeSpy).toHaveBeenCalled();
1159
+ expect(metricSpy).toHaveBeenCalledWith(
1160
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1161
+ expect.objectContaining({skipReason: 'mercury_subscribe_failed'}),
1162
+ ['operational', 'behavioral']
1163
+ );
1164
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1165
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
1166
+ });
1167
+
1168
+ it('should record publish_failed when usersub publish fails after Mercury init', async () => {
1169
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1170
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1171
+ webex.cc.agentConfig = {
1172
+ agentId: 'agentId',
1173
+ webRtcEnabled: false,
1174
+ loginVoiceOptions: ['EXTENSION'],
1175
+ };
1176
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1177
+ mockTaskManager.applyEnableWxBetterTogether(true);
1178
+
1179
+ jest.spyOn(webex.cc as any, 'ensureWxAppMercuryConnected').mockResolvedValue(undefined);
1180
+ jest
1181
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
1182
+ .mockImplementation(() => {});
1183
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1184
+ jest
1185
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1186
+ .mockRejectedValue(new Error('publish failed'));
1187
+ jest
1188
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1189
+ .mockResolvedValue(undefined);
1190
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1191
+
1192
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1193
+ data: {
1194
+ agentId: 'agentId',
1195
+ teamId: 'teamId',
1196
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1197
+ },
1198
+ trackingId: 'track-1',
1199
+ } as StationLoginSuccess);
1200
+
1201
+ await webex.cc.stationLogin({
1202
+ teamId: 'teamId',
1203
+ loginOption: LoginOption.EXTENSION,
1204
+ dialNumber: '1001',
1205
+ });
1206
+
1207
+ expect(metricSpy).toHaveBeenCalledWith(
1208
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1209
+ expect.objectContaining({skipReason: 'publish_failed'}),
1210
+ ['operational', 'behavioral']
1211
+ );
1212
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1213
+ });
1214
+
1215
+ it('should register device before publishing usersub true on extension station login', async () => {
1216
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1217
+ webex.internal.device = {
1218
+ userId: 'user-123',
1219
+ registered: false,
1220
+ register: jest.fn().mockImplementation(async function (this: {
1221
+ url?: string;
1222
+ registered?: boolean;
1223
+ }) {
1224
+ this.url = 'https://wdm.example.com/devices/dev-1';
1225
+ this.registered = true;
1226
+ }),
1227
+ };
1228
+ webex.internal.mercury = {
1229
+ connected: false,
1230
+ connect: jest.fn().mockResolvedValue(undefined),
1231
+ };
1232
+ webex.cc.agentConfig = {
1233
+ agentId: 'agentId',
1234
+ webRtcEnabled: false,
1235
+ loginVoiceOptions: ['EXTENSION'],
1236
+ };
1237
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1238
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1239
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1240
+
1241
+ const callOrder: string[] = [];
1242
+ jest.spyOn(webex.internal.device, 'register').mockImplementation(async function (this: {
1243
+ url?: string;
1244
+ registered?: boolean;
1245
+ }) {
1246
+ callOrder.push('device.register');
1247
+ this.url = 'https://wdm.example.com/devices/dev-1';
1248
+ this.registered = true;
1249
+ });
1250
+ jest
1251
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1252
+ .mockImplementation(async (enable: boolean) => {
1253
+ if (enable === true && !webex.internal.device.url) {
1254
+ throw new Error('Device URL is unavailable for cross-client publish');
1255
+ }
1256
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
1257
+ });
1258
+
1259
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1260
+ data: {
1261
+ agentId: 'agentId',
1262
+ teamId: 'teamId',
1263
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1264
+ },
1265
+ trackingId: 'track-1',
1266
+ } as StationLoginSuccess);
1267
+
1268
+ await webex.cc.stationLogin({
1269
+ teamId: 'teamId',
1270
+ loginOption: LoginOption.EXTENSION,
1271
+ dialNumber: '1001',
1272
+ });
1273
+
1274
+ expect(callOrder).toEqual(['device.register', 'publish:true']);
1275
+ });
1276
+
1277
+ it('should register device before publishing forced usersub false when wxApp is disabled at station login', async () => {
1278
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1279
+ webex.internal.device = {
1280
+ userId: 'user-123',
1281
+ registered: false,
1282
+ register: jest.fn().mockImplementation(async function (this: {
1283
+ url?: string;
1284
+ registered?: boolean;
1285
+ }) {
1286
+ this.url = 'https://wdm.example.com/devices/dev-1';
1287
+ this.registered = true;
1288
+ }),
1289
+ };
1290
+ webex.internal.mercury = {
1291
+ connected: false,
1292
+ connect: jest.fn().mockResolvedValue(undefined),
1293
+ };
1294
+ webex.cc.agentConfig = {
1295
+ agentId: 'agentId',
1296
+ webRtcEnabled: false,
1297
+ loginVoiceOptions: ['EXTENSION'],
1298
+ };
1299
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1300
+ jest
1301
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1302
+ .mockResolvedValue(undefined);
1303
+
1304
+ const callOrder: string[] = [];
1305
+ jest.spyOn(webex.internal.device, 'register').mockImplementation(async function (this: {
1306
+ url?: string;
1307
+ registered?: boolean;
1308
+ }) {
1309
+ callOrder.push('device.register');
1310
+ this.url = 'https://wdm.example.com/devices/dev-1';
1311
+ this.registered = true;
1312
+ });
1313
+ jest
1314
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1315
+ .mockImplementation(async (enable: boolean) => {
1316
+ if (!webex.internal.device.url) {
1317
+ throw new Error('Device URL is unavailable for cross-client publish');
1318
+ }
1319
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
1320
+ });
1321
+
1322
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1323
+ data: {
1324
+ agentId: 'agentId',
1325
+ teamId: 'teamId',
1326
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1327
+ },
1328
+ trackingId: 'track-1',
1329
+ } as StationLoginSuccess);
1330
+
1331
+ await webex.cc.stationLogin({
1332
+ teamId: 'teamId',
1333
+ loginOption: LoginOption.EXTENSION,
1334
+ dialNumber: '1001',
1335
+ });
1336
+
1337
+ expect(callOrder).toEqual(['device.register', 'publish:false']);
1338
+ });
1339
+
1340
+ it('should retry forced false publish when wxApp is disabled at station login', async () => {
1341
+ jest.useFakeTimers();
1342
+ try {
1343
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1344
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1345
+ webex.cc.agentConfig = {
1346
+ agentId: 'agentId',
1347
+ webRtcEnabled: false,
1348
+ loginVoiceOptions: ['EXTENSION'],
1349
+ };
1350
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1351
+
1352
+ const publishSpy = jest
1353
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1354
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
1355
+ .mockResolvedValue(undefined);
1356
+ const scheduleRetrySpy = jest.spyOn(
1357
+ webex.cc as any,
1358
+ 'scheduleForcedFalsePublishRetry'
1359
+ );
1360
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1361
+ jest
1362
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1363
+ .mockResolvedValue(undefined);
1364
+
1365
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1366
+ data: {
1367
+ agentId: 'agentId',
1368
+ teamId: 'teamId',
1369
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1370
+ },
1371
+ trackingId: 'track-1',
1372
+ } as StationLoginSuccess);
1373
+
1374
+ await webex.cc.stationLogin({
1375
+ teamId: 'teamId',
1376
+ loginOption: LoginOption.EXTENSION,
1377
+ dialNumber: '1001',
1378
+ });
1379
+
1380
+ expect(publishSpy).toHaveBeenCalledTimes(1);
1381
+ expect(scheduleRetrySpy).toHaveBeenCalled();
1382
+ expect(teardownSpy).not.toHaveBeenCalled();
1383
+
1384
+ jest.advanceTimersByTime(30_000);
1385
+ await publishSpy.mock.results[1]?.value;
1386
+
1387
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1388
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1389
+ } finally {
1390
+ jest.useRealTimers();
1391
+ }
1392
+ });
1393
+
1394
+ it('should not publish usersub on BROWSER station login even when enableWxBetterTogether is true at init', async () => {
1395
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1396
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1397
+ webex.cc.agentConfig = {
1398
+ agentId: 'agentId',
1399
+ webRtcEnabled: true,
1400
+ loginVoiceOptions: ['BROWSER'],
1401
+ };
1402
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
1403
+
1404
+ const publishSpy = jest.spyOn(
1405
+ webex.cc['webexCrossClientService'],
1406
+ 'setManageWebexCallingInWxcc'
1407
+ );
1408
+
1409
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1410
+ data: {
1411
+ agentId: 'agentId',
1412
+ teamId: 'teamId',
1413
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1414
+ },
1415
+ trackingId: 'track-1',
1416
+ } as StationLoginSuccess);
1417
+
1418
+ await webex.cc.stationLogin({
1419
+ teamId: 'teamId',
1420
+ loginOption: LoginOption.BROWSER,
1421
+ });
1422
+
1423
+ expect(publishSpy).not.toHaveBeenCalled();
1424
+ });
1425
+
1426
+ it('should force-publish usersub false on station login when enableWxBetterTogether is false at init', async () => {
1427
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1428
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1429
+ webex.cc.agentConfig = {
1430
+ agentId: 'agentId',
1431
+ webRtcEnabled: false,
1432
+ loginVoiceOptions: ['EXTENSION'],
1433
+ };
1434
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1435
+
1436
+ const publishSpy = jest
1437
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1438
+ .mockResolvedValue(undefined);
1439
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1440
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1441
+
1442
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1443
+ data: {
1444
+ agentId: 'agentId',
1445
+ teamId: 'teamId',
1446
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1447
+ },
1448
+ trackingId: 'track-1',
1449
+ } as StationLoginSuccess);
1450
+
1451
+ await webex.cc.stationLogin({
1452
+ teamId: 'teamId',
1453
+ loginOption: LoginOption.EXTENSION,
1454
+ dialNumber: '1001',
1455
+ });
1456
+
1457
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1458
+ expect(teardownSpy).toHaveBeenCalled();
1459
+ expect(unsubscribeSpy).toHaveBeenCalled();
1460
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
1461
+ METRIC_EVENT_NAMES.WXAPP_SESSION_SKIPPED,
1462
+ expect.objectContaining({
1463
+ enableWxBetterTogether: false,
1464
+ skipReason: 'flag_disabled',
1465
+ }),
1466
+ ['operational', 'behavioral']
1467
+ );
1468
+ });
1469
+
1470
+ it('should not emit WXAPP_SESSION_INIT_FAILED when flag-disabled cleanup fails', async () => {
1471
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1472
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1473
+ webex.cc.agentConfig = {
1474
+ agentId: 'agentId',
1475
+ webRtcEnabled: false,
1476
+ loginVoiceOptions: ['EXTENSION'],
1477
+ };
1478
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1479
+
1480
+ jest
1481
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1482
+ .mockRejectedValue(new Error('forced false publish failed'));
1483
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1484
+
1485
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1486
+ data: {
1487
+ agentId: 'agentId',
1488
+ teamId: 'teamId',
1489
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1490
+ },
1491
+ trackingId: 'track-1',
1492
+ } as StationLoginSuccess);
1493
+
1494
+ await webex.cc.stationLogin({
1495
+ teamId: 'teamId',
1496
+ loginOption: LoginOption.EXTENSION,
1497
+ dialNumber: '1001',
1498
+ });
1499
+
1500
+ expect(metricSpy).not.toHaveBeenCalledWith(
1501
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1502
+ expect.anything(),
1503
+ expect.anything()
1504
+ );
1505
+ });
1506
+
1507
+ it('should still publish usersub but record session init failed when Mercury is unavailable', async () => {
1508
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1509
+ webex.internal.device = {
1510
+ userId: 'user-123',
1511
+ url: 'https://wdm.example.com/devices/dev-1',
1512
+ registered: true,
1513
+ register: jest.fn().mockResolvedValue(undefined),
1514
+ };
1515
+ delete webex.internal.mercury;
1516
+ webex.cc.agentConfig = {
1517
+ agentId: 'agentId',
1518
+ webRtcEnabled: false,
1519
+ loginVoiceOptions: ['EXTENSION'],
1520
+ };
1521
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1522
+ mockTaskManager.applyEnableWxBetterTogether(true);
1523
+
1524
+ const publishSpy = jest
1525
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1526
+ .mockResolvedValue(undefined);
1527
+ jest
1528
+ .spyOn(webex.cc['webexCrossClientService'], 'isAnswerCallsStateActive')
1529
+ .mockReturnValue(true);
1530
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1531
+
1532
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1533
+ data: {
1534
+ agentId: 'agentId',
1535
+ teamId: 'teamId',
1536
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1537
+ },
1538
+ trackingId: 'track-1',
1539
+ } as StationLoginSuccess);
1540
+
1541
+ await webex.cc.stationLogin({
1542
+ teamId: 'teamId',
1543
+ loginOption: LoginOption.EXTENSION,
1544
+ dialNumber: '1001',
1545
+ });
1546
+
1547
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
1548
+ expect(metricSpy).toHaveBeenCalledWith(
1549
+ METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_FAILED,
1550
+ expect.objectContaining({error: 'Mercury is unavailable for wxApp mute sync'}),
1551
+ ['operational', 'behavioral']
1552
+ );
1553
+ expect(metricSpy).not.toHaveBeenCalledWith(
1554
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_SUCCESS,
1555
+ expect.anything(),
1556
+ expect.anything()
1557
+ );
1558
+ expect(metricSpy).toHaveBeenCalledWith(
1559
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1560
+ expect.objectContaining({skipReason: 'mercury_not_subscribed'}),
1561
+ ['operational', 'behavioral']
1562
+ );
1563
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
1564
+ });
1565
+
1566
+ it('should record session init failed when userId is missing but not emit success', async () => {
1567
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1568
+ webex.internal.device = {
1569
+ url: 'https://wdm.example.com/devices/dev-1',
1570
+ registered: true,
1571
+ register: jest.fn().mockResolvedValue(undefined),
1572
+ };
1573
+ webex.internal.mercury = {
1574
+ connected: true,
1575
+ connect: jest.fn().mockResolvedValue(undefined),
1576
+ on: jest.fn(),
1577
+ off: jest.fn(),
1578
+ };
1579
+ webex.cc.agentConfig = {
1580
+ agentId: 'agentId',
1581
+ webRtcEnabled: false,
1582
+ loginVoiceOptions: ['EXTENSION'],
1583
+ };
1584
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1585
+ mockTaskManager.applyEnableWxBetterTogether(true);
1586
+
1587
+ const publishSpy = jest
1588
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1589
+ .mockResolvedValue(undefined);
1590
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1591
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1592
+
1593
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1594
+ data: {
1595
+ agentId: 'agentId',
1596
+ teamId: 'teamId',
1597
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1598
+ },
1599
+ trackingId: 'track-1',
1600
+ } as StationLoginSuccess);
1601
+
1602
+ await webex.cc.stationLogin({
1603
+ teamId: 'teamId',
1604
+ loginOption: LoginOption.EXTENSION,
1605
+ dialNumber: '1001',
1606
+ });
1607
+
1608
+ expect(publishSpy).not.toHaveBeenCalledWith(true, expect.anything());
1609
+ expect(metricSpy).not.toHaveBeenCalledWith(
1610
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_SUCCESS,
1611
+ expect.anything(),
1612
+ expect.anything()
1613
+ );
1614
+ expect(metricSpy).toHaveBeenCalledWith(
1615
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1616
+ expect.objectContaining({skipReason: 'usersub_not_published'}),
1617
+ ['operational', 'behavioral']
1618
+ );
1619
+ expect(metricSpy).toHaveBeenCalledWith(
1620
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
1621
+ expect.objectContaining({skipReason: 'user_id_unavailable'}),
1622
+ ['operational', 'behavioral']
1623
+ );
1624
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
1625
+ });
1626
+
1627
+ it('should clear stale usersub on login after prior session enabled wxApp then hard refresh', async () => {
1628
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1629
+ webex.cc.agentConfig = {
1630
+ agentId: 'agentId',
1631
+ webRtcEnabled: false,
1632
+ loginVoiceOptions: ['EXTENSION'],
1633
+ };
1634
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1635
+ webex.request = jest.fn().mockResolvedValue({body: {}});
1636
+
1637
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1638
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1639
+ data: {
1640
+ agentId: 'agentId',
1641
+ teamId: 'teamId',
1642
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1643
+ },
1644
+ trackingId: 'track-1',
1645
+ } as StationLoginSuccess);
1646
+
1647
+ await webex.cc.stationLogin({
1648
+ teamId: 'teamId',
1649
+ loginOption: LoginOption.EXTENSION,
1650
+ dialNumber: '1001',
1651
+ });
1652
+
1653
+ webex.cc['webexCrossClientService'].teardown();
1654
+ webex.cc['wxAppTelephonyMercurySync'].unsubscribe();
1655
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1656
+
1657
+ const publishSpy = jest
1658
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1659
+ .mockResolvedValue(undefined);
1660
+
1661
+ await webex.cc.stationLogin({
1662
+ teamId: 'teamId',
1663
+ loginOption: LoginOption.EXTENSION,
1664
+ dialNumber: '1001',
1665
+ });
1666
+
1667
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1668
+ });
1022
1669
  });
1023
1670
 
1024
1671
  describe('stationLogout', () => {
@@ -1042,23 +1689,233 @@ describe('webex.cc', () => {
1042
1689
  method: 'stationLogout',
1043
1690
  });
1044
1691
 
1045
- expect(stationLogoutMock).toHaveBeenCalledWith({data: data});
1046
- // TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
1047
- // expect(mockTaskManager.unregisterIncomingCallEvent).toHaveBeenCalledWith();
1048
- // expect(mockTaskManager.off).toHaveBeenCalledWith(
1049
- // TASK_EVENTS.TASK_INCOMING,
1050
- // expect.any(Function)
1051
- // );
1052
- // expect(mockTaskManager.off).toHaveBeenCalledWith(
1053
- // TASK_EVENTS.TASK_HYDRATE,
1054
- // expect.any(Function)
1055
- // );
1056
- // expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1057
- expect(result).toEqual(response);
1058
- expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
1059
- METRIC_EVENT_NAMES.STATION_LOGOUT_SUCCESS,
1060
- METRIC_EVENT_NAMES.STATION_LOGOUT_FAILED,
1061
- ]);
1692
+ expect(stationLogoutMock).toHaveBeenCalledWith({data: data});
1693
+ // TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
1694
+ // expect(mockTaskManager.unregisterIncomingCallEvent).toHaveBeenCalledWith();
1695
+ // expect(mockTaskManager.off).toHaveBeenCalledWith(
1696
+ // TASK_EVENTS.TASK_INCOMING,
1697
+ // expect.any(Function)
1698
+ // );
1699
+ // expect(mockTaskManager.off).toHaveBeenCalledWith(
1700
+ // TASK_EVENTS.TASK_HYDRATE,
1701
+ // expect.any(Function)
1702
+ // );
1703
+ // expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1704
+ expect(result).toEqual(response);
1705
+ expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
1706
+ METRIC_EVENT_NAMES.STATION_LOGOUT_SUCCESS,
1707
+ METRIC_EVENT_NAMES.STATION_LOGOUT_FAILED,
1708
+ ]);
1709
+ });
1710
+
1711
+ it('should not publish usersub false on logout when wxApp was never enabled', async () => {
1712
+ webex.internal.device = {userId: 'user-123'};
1713
+ const publishSpy = jest
1714
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1715
+ .mockResolvedValue(undefined);
1716
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1717
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1718
+
1719
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1720
+ trackingId: 'track-1',
1721
+ } as StationLogoutResponse);
1722
+
1723
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1724
+
1725
+ expect(publishSpy).not.toHaveBeenCalled();
1726
+ expect(teardownSpy).toHaveBeenCalled();
1727
+ expect(unsubscribeSpy).toHaveBeenCalled();
1728
+ });
1729
+
1730
+ it('should publish usersub false and teardown wxApp sync on logout when wxApp was enabled', async () => {
1731
+ webex.internal.device = {
1732
+ userId: 'user-123',
1733
+ url: 'https://wdm.example.com/devices/dev-1',
1734
+ registered: false,
1735
+ register: jest.fn().mockResolvedValue(undefined),
1736
+ unregister: jest.fn().mockResolvedValue(undefined),
1737
+ };
1738
+ webex.internal.mercury = {
1739
+ connected: false,
1740
+ connect: jest.fn().mockResolvedValue(undefined),
1741
+ disconnect: jest.fn().mockResolvedValue(undefined),
1742
+ };
1743
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1744
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1745
+ webex.request = jest.fn().mockResolvedValue({body: {}});
1746
+ webex.cc['agentConfig'] = {
1747
+ agentId: 'agent-123',
1748
+ deviceType: LoginOption.EXTENSION,
1749
+ } as Profile;
1750
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1751
+
1752
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
1753
+
1754
+ const publishSpy = jest
1755
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1756
+ .mockResolvedValue(undefined);
1757
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1758
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1759
+
1760
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1761
+ trackingId: 'track-1',
1762
+ } as StationLogoutResponse);
1763
+
1764
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1765
+
1766
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1767
+ expect(teardownSpy).toHaveBeenCalled();
1768
+ expect(unsubscribeSpy).toHaveBeenCalled();
1769
+ });
1770
+
1771
+ it('should teardown wxApp state on logout even when usersub publish fails', async () => {
1772
+ webex.internal.device = {userId: 'user-123'};
1773
+ webex.cc['agentConfig'] = {
1774
+ agentId: 'agent-123',
1775
+ deviceType: LoginOption.EXTENSION,
1776
+ } as Profile;
1777
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1778
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1779
+ mockTaskManager.applyEnableWxBetterTogether(true);
1780
+
1781
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1782
+ trackingId: 'track-1',
1783
+ } as StationLogoutResponse);
1784
+ jest
1785
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
1786
+ .mockRejectedValue(new Error('usersub publish failed'));
1787
+ const scheduleRetrySpy = jest.spyOn(
1788
+ webex.cc as any,
1789
+ 'scheduleCompensatingFalsePublishRetry'
1790
+ );
1791
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1792
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1793
+
1794
+ const result = await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1795
+
1796
+ expect(result).toEqual({trackingId: 'track-1'});
1797
+ expect(teardownSpy).toHaveBeenCalled();
1798
+ expect(unsubscribeSpy).toHaveBeenCalled();
1799
+ expect(scheduleRetrySpy).toHaveBeenCalled();
1800
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
1801
+ });
1802
+
1803
+ it('should retry compensating false publish when teardown usersub publish fails on logout', async () => {
1804
+ jest.useFakeTimers();
1805
+ try {
1806
+ webex.internal.device = {
1807
+ userId: 'user-123',
1808
+ url: 'https://wdm.example.com/devices/dev-1',
1809
+ };
1810
+ webex.cc['agentConfig'] = {
1811
+ agentId: 'agent-123',
1812
+ deviceType: LoginOption.EXTENSION,
1813
+ } as Profile;
1814
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1815
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1816
+ mockTaskManager.applyEnableWxBetterTogether(true);
1817
+ webex.cc['webexCrossClientService'].answerCallsState = true;
1818
+
1819
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1820
+ trackingId: 'track-1',
1821
+ } as StationLogoutResponse);
1822
+
1823
+ const publishSpy = jest
1824
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1825
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
1826
+ .mockResolvedValue(undefined);
1827
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1828
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1829
+ jest
1830
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1831
+ .mockResolvedValue(undefined);
1832
+
1833
+ const result = await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1834
+
1835
+ expect(result).toEqual({trackingId: 'track-1'});
1836
+ expect(publishSpy).toHaveBeenCalledTimes(1);
1837
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1838
+ expect(teardownSpy).toHaveBeenCalled();
1839
+ expect(unsubscribeSpy).toHaveBeenCalled();
1840
+
1841
+ jest.advanceTimersByTime(30_000);
1842
+ await publishSpy.mock.results[1]?.value;
1843
+
1844
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1845
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1846
+ } finally {
1847
+ jest.useRealTimers();
1848
+ }
1849
+ });
1850
+
1851
+ it('should cancel compensating retry when station login succeeds before retry fires', async () => {
1852
+ jest.useFakeTimers();
1853
+ try {
1854
+ webex.internal.device = {
1855
+ userId: 'user-123',
1856
+ url: 'https://wdm.example.com/devices/dev-1',
1857
+ registered: false,
1858
+ register: jest.fn().mockResolvedValue(undefined),
1859
+ unregister: jest.fn().mockResolvedValue(undefined),
1860
+ };
1861
+ webex.internal.mercury = {
1862
+ connected: false,
1863
+ connect: jest.fn().mockResolvedValue(undefined),
1864
+ disconnect: jest.fn().mockResolvedValue(undefined),
1865
+ };
1866
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1867
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1868
+ webex.cc['agentConfig'] = {
1869
+ agentId: 'agent-123',
1870
+ deviceType: LoginOption.EXTENSION,
1871
+ webRtcEnabled: false,
1872
+ loginVoiceOptions: ['EXTENSION'],
1873
+ } as Profile;
1874
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1875
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1876
+ mockTaskManager.applyEnableWxBetterTogether(true);
1877
+ webex.cc['webexCrossClientService'].answerCallsState = true;
1878
+
1879
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1880
+ trackingId: 'track-1',
1881
+ } as StationLogoutResponse);
1882
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1883
+ data: {
1884
+ agentId: 'agent-123',
1885
+ teamId: 'team-123',
1886
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1887
+ },
1888
+ trackingId: 'track-2',
1889
+ } as StationLoginSuccess);
1890
+
1891
+ const publishSpy = jest
1892
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1893
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
1894
+ .mockResolvedValue(undefined);
1895
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1896
+ jest
1897
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1898
+ .mockResolvedValue(undefined);
1899
+
1900
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1901
+ await webex.cc.stationLogin({
1902
+ teamId: 'team-123',
1903
+ loginOption: LoginOption.EXTENSION,
1904
+ dialNumber: '1001',
1905
+ });
1906
+
1907
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1908
+ expect(publishSpy).toHaveBeenNthCalledWith(1, false, {userId: 'user-123', trackPublishMetrics: true});
1909
+ expect(publishSpy).toHaveBeenNthCalledWith(2, true, {userId: 'user-123', trackPublishMetrics: true});
1910
+ expect(teardownSpy).toHaveBeenCalledTimes(1);
1911
+
1912
+ jest.advanceTimersByTime(30_000);
1913
+
1914
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1915
+ expect(teardownSpy).toHaveBeenCalledTimes(1);
1916
+ } finally {
1917
+ jest.useRealTimers();
1918
+ }
1062
1919
  });
1063
1920
 
1064
1921
  it('should handle error during stationLogout', async () => {
@@ -1276,6 +2133,41 @@ describe('webex.cc', () => {
1276
2133
  ]);
1277
2134
  });
1278
2135
 
2136
+ it('should map Transfer action to an Available backend state filter', async () => {
2137
+ webex.cc.agentConfig = {agentProfileID: 'test-agent-profile-id'};
2138
+ const buddyAgentsResponse = {data: {agentList: []}};
2139
+ const buddyAgentsSpy = jest
2140
+ .spyOn(webex.cc.services.agent, 'buddyAgents')
2141
+ .mockResolvedValue(buddyAgentsResponse);
2142
+
2143
+ await webex.cc.getBuddyAgents({action: 'Transfer', mediaType: 'chat'});
2144
+
2145
+ expect(buddyAgentsSpy).toHaveBeenCalledWith({
2146
+ data: {
2147
+ agentProfileId: 'test-agent-profile-id',
2148
+ mediaType: 'chat',
2149
+ state: 'Available',
2150
+ },
2151
+ });
2152
+ });
2153
+
2154
+ it('should omit the backend state filter for Consult and default media type to telephony', async () => {
2155
+ webex.cc.agentConfig = {agentProfileID: 'test-agent-profile-id'};
2156
+ const buddyAgentsResponse = {data: {agentList: []}};
2157
+ const buddyAgentsSpy = jest
2158
+ .spyOn(webex.cc.services.agent, 'buddyAgents')
2159
+ .mockResolvedValue(buddyAgentsResponse);
2160
+
2161
+ await webex.cc.getBuddyAgents({action: 'Consult'});
2162
+
2163
+ expect(buddyAgentsSpy).toHaveBeenCalledWith({
2164
+ data: {
2165
+ agentProfileId: 'test-agent-profile-id',
2166
+ mediaType: 'telephony',
2167
+ },
2168
+ });
2169
+ });
2170
+
1279
2171
  it('should handle error', async () => {
1280
2172
  const data: BuddyAgents = {state: 'Available', mediaType: 'telephony'};
1281
2173
  webex.cc.agentConfig = {
@@ -1503,6 +2395,82 @@ describe('webex.cc', () => {
1503
2395
  expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.AGENT_DN);
1504
2396
  expect(webex.cc.agentConfig.dn).toBe('67890');
1505
2397
  });
2398
+
2399
+ it('should publish usersub true on silent relogin when enableWxBetterTogether is true at init', async () => {
2400
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2401
+ webex.internal.device = {
2402
+ userId: 'user-123',
2403
+ url: 'https://wdm.example.com/devices/dev-1',
2404
+ registered: true,
2405
+ register: jest.fn().mockResolvedValue(undefined),
2406
+ };
2407
+ webex.internal.mercury.connected = true;
2408
+ webex.internal.mercury.connect = jest.fn().mockResolvedValue(undefined);
2409
+ webex.cc.agentConfig = {
2410
+ agentId: 'agentId',
2411
+ agentProfileID: 'test-agent-profile-id',
2412
+ isAgentLoggedIn: false,
2413
+ } as Profile;
2414
+
2415
+ jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
2416
+ data: {
2417
+ auxCodeId: 'auxCodeId',
2418
+ agentId: 'agentId',
2419
+ deviceType: LoginOption.EXTENSION,
2420
+ dn: '1001',
2421
+ teamId: 'teamId',
2422
+ lastStateChangeTimestamp: 1738575135188,
2423
+ lastIdleCodeChangeTimestamp: 1738575135189,
2424
+ },
2425
+ });
2426
+
2427
+ const publishSpy = jest
2428
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2429
+ .mockResolvedValue(undefined);
2430
+ const subscribeSpy = jest
2431
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
2432
+ .mockImplementation(() => {});
2433
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
2434
+
2435
+ await webex.cc['silentRelogin']();
2436
+
2437
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
2438
+ expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
2439
+ });
2440
+
2441
+ it('should force-publish usersub false on silent relogin when enableWxBetterTogether is false at init', async () => {
2442
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
2443
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
2444
+ webex.cc.agentConfig = {
2445
+ agentId: 'agentId',
2446
+ agentProfileID: 'test-agent-profile-id',
2447
+ isAgentLoggedIn: false,
2448
+ } as Profile;
2449
+
2450
+ jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
2451
+ data: {
2452
+ auxCodeId: 'auxCodeId',
2453
+ agentId: 'agentId',
2454
+ deviceType: LoginOption.EXTENSION,
2455
+ dn: '1001',
2456
+ teamId: 'teamId',
2457
+ lastStateChangeTimestamp: 1738575135188,
2458
+ lastIdleCodeChangeTimestamp: 1738575135189,
2459
+ },
2460
+ });
2461
+
2462
+ const publishSpy = jest
2463
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2464
+ .mockResolvedValue(undefined);
2465
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
2466
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
2467
+
2468
+ await webex.cc['silentRelogin']();
2469
+
2470
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
2471
+ expect(teardownSpy).toHaveBeenCalled();
2472
+ expect(unsubscribeSpy).toHaveBeenCalled();
2473
+ });
1506
2474
  });
1507
2475
 
1508
2476
  describe('setupEventListeners()', () => {
@@ -1741,6 +2709,18 @@ describe('webex.cc', () => {
1741
2709
  on: jest.fn(),
1742
2710
  } as any;
1743
2711
 
2712
+ webex.internal.device = {
2713
+ ...webex.internal.device,
2714
+ userId: 'user-123',
2715
+ unregister: jest.fn().mockResolvedValue(undefined),
2716
+ };
2717
+ deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
2718
+ const publishSpy = jest
2719
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2720
+ .mockResolvedValue(undefined);
2721
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
2722
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
2723
+
1744
2724
  await webex.cc.deregister();
1745
2725
 
1746
2726
  expect(mockTaskManager.off).toHaveBeenCalledWith(
@@ -1829,6 +2809,39 @@ describe('webex.cc', () => {
1829
2809
  expect(connectionCalls).toHaveLength(1);
1830
2810
  const [, connectionCallback] = connectionCalls[0];
1831
2811
  expect(connectionCallback).toBe(webex.cc['handleConnectionLost']);
2812
+
2813
+ expect(publishSpy).not.toHaveBeenCalled();
2814
+ expect(teardownSpy).toHaveBeenCalled();
2815
+ expect(unsubscribeSpy).toHaveBeenCalled();
2816
+ });
2817
+
2818
+ it('should publish usersub false before device unregister when wxApp is active on deregister', async () => {
2819
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2820
+ webex.cc['webexCrossClientService'].answerCallsState = true;
2821
+ webex.internal.device = {
2822
+ userId: 'user-123',
2823
+ url: 'https://wdm.example.com/devices/dev-1',
2824
+ unregister: jest.fn().mockResolvedValue(undefined),
2825
+ };
2826
+ deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
2827
+
2828
+ const callOrder: string[] = [];
2829
+ jest
2830
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2831
+ .mockImplementation(async (enable: boolean) => {
2832
+ if (enable === false && !webex.internal.device.url) {
2833
+ throw new Error('Device URL is unavailable for cross-client publish');
2834
+ }
2835
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
2836
+ });
2837
+ deviceUnregisterSpy.mockImplementation(async () => {
2838
+ callOrder.push('device.unregister');
2839
+ webex.internal.device.url = undefined;
2840
+ });
2841
+
2842
+ await webex.cc.deregister();
2843
+
2844
+ expect(callOrder).toEqual(['publish:false', 'device.unregister']);
1832
2845
  });
1833
2846
 
1834
2847
  it('should skip webCallingService and internal cleanup when webrtc is disabled', async () => {
@@ -1878,6 +2891,135 @@ describe('webex.cc', () => {
1878
2891
  expect(webex.cc.agentConfig).toBeNull();
1879
2892
  });
1880
2893
 
2894
+ it('should teardown wxApp state on deregister even when usersub publish fails', async () => {
2895
+ webex.cc.agentConfig = {
2896
+ agentId: 'agentId',
2897
+ webRtcEnabled: false,
2898
+ loginVoiceOptions: [LoginOption.EXTENSION],
2899
+ };
2900
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2901
+ webex.cc['webexCrossClientService'].answerCallsState = true;
2902
+ webex.internal.device = {
2903
+ userId: 'user-123',
2904
+ unregister: jest.fn().mockResolvedValue(undefined),
2905
+ };
2906
+
2907
+ jest
2908
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
2909
+ .mockRejectedValue(new Error('usersub publish failed'));
2910
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
2911
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
2912
+
2913
+ await expect(webex.cc.deregister()).rejects.toThrow('usersub publish failed');
2914
+
2915
+ expect(teardownSpy).toHaveBeenCalled();
2916
+ expect(unsubscribeSpy).toHaveBeenCalled();
2917
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
2918
+ expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
2919
+ expect(webex.cc.agentConfig).toBeNull();
2920
+ });
2921
+
2922
+ it('should publish forced false during teardown when enable publish is still in flight', async () => {
2923
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2924
+ webex.internal.device = {
2925
+ userId: 'user-123',
2926
+ url: 'https://wdm.example.com/devices/dev-1',
2927
+ };
2928
+
2929
+ let resolveEnable: () => void = () => {};
2930
+ const enableGate = new Promise<void>((resolve) => {
2931
+ resolveEnable = resolve;
2932
+ });
2933
+
2934
+ const publishSpy = jest
2935
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2936
+ .mockImplementation(async (enable: boolean) => {
2937
+ if (enable) {
2938
+ await enableGate;
2939
+ }
2940
+ });
2941
+ jest
2942
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
2943
+ .mockResolvedValue(undefined);
2944
+
2945
+ expect(webex.cc['webexCrossClientService'].answerCallsState).toBe(false);
2946
+
2947
+ const enableInFlight = (webex.cc as any).publishAnswerOnWebexCrossClientState(true);
2948
+
2949
+ await (webex.cc as any).teardownWxAppLocalState();
2950
+
2951
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
2952
+
2953
+ resolveEnable();
2954
+ await enableInFlight;
2955
+ });
2956
+
2957
+ it('should release wxApp-owned Mercury and device on deregister', async () => {
2958
+ webex.cc.agentConfig = {
2959
+ agentId: 'agentId',
2960
+ webRtcEnabled: false,
2961
+ loginVoiceOptions: [LoginOption.EXTENSION],
2962
+ };
2963
+ webex.cc['wxAppMercuryConnectedByCc'] = true;
2964
+ webex.cc['wxAppDeviceRegisteredByCc'] = true;
2965
+ webex.internal.mercury.connected = true;
2966
+ webex.internal.device.registered = true;
2967
+
2968
+ const mercuryDisconnectSpy = jest
2969
+ .spyOn(webex.internal.mercury, 'disconnect')
2970
+ .mockResolvedValue(undefined);
2971
+ const deviceUnregisterSpy = jest
2972
+ .spyOn(webex.internal.device, 'unregister')
2973
+ .mockResolvedValue(undefined);
2974
+
2975
+ await webex.cc.deregister();
2976
+
2977
+ expect(mercuryDisconnectSpy).toHaveBeenCalled();
2978
+ expect(deviceUnregisterSpy).toHaveBeenCalled();
2979
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(false);
2980
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(false);
2981
+ });
2982
+
2983
+ it('should retain wxApp Mercury ownership when disconnect fails and retry cleanup', async () => {
2984
+ webex.cc['wxAppMercuryConnectedByCc'] = true;
2985
+ webex.internal.mercury.connected = true;
2986
+
2987
+ const mercuryDisconnectSpy = jest
2988
+ .spyOn(webex.internal.mercury, 'disconnect')
2989
+ .mockRejectedValueOnce(new Error('disconnect failed'))
2990
+ .mockResolvedValueOnce(undefined);
2991
+
2992
+ await webex.cc['releaseWxAppMercuryResources']();
2993
+
2994
+ expect(mercuryDisconnectSpy).toHaveBeenCalledTimes(1);
2995
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(true);
2996
+
2997
+ await webex.cc['releaseWxAppMercuryResources']();
2998
+
2999
+ expect(mercuryDisconnectSpy).toHaveBeenCalledTimes(2);
3000
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(false);
3001
+ });
3002
+
3003
+ it('should retain wxApp device ownership when unregister fails and retry cleanup', async () => {
3004
+ webex.cc['wxAppDeviceRegisteredByCc'] = true;
3005
+ webex.internal.device.registered = true;
3006
+
3007
+ const deviceUnregisterSpy = jest
3008
+ .spyOn(webex.internal.device, 'unregister')
3009
+ .mockRejectedValueOnce(new Error('unregister failed'))
3010
+ .mockResolvedValueOnce(undefined);
3011
+
3012
+ await webex.cc['releaseWxAppMercuryResources']();
3013
+
3014
+ expect(deviceUnregisterSpy).toHaveBeenCalledTimes(1);
3015
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(true);
3016
+
3017
+ await webex.cc['releaseWxAppMercuryResources']();
3018
+
3019
+ expect(deviceUnregisterSpy).toHaveBeenCalledTimes(2);
3020
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(false);
3021
+ });
3022
+
1881
3023
  it('should handle errors during unregister and track metrics', async () => {
1882
3024
  const mockError = new Error('Failed to deregister device');
1883
3025
  webex.internal.device.unregister.mockRejectedValue(mockError);
@@ -2128,6 +3270,55 @@ describe('webex.cc', () => {
2128
3270
  expect(result).toEqual(mockResp);
2129
3271
  });
2130
3272
 
3273
+ it('should preserve enableWxBetterTogether init flag across profile update logout/login', async () => {
3274
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3275
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
3276
+ webex.cc.agentConfig = {
3277
+ ...webex.cc.agentConfig,
3278
+ agentId: 'agentId',
3279
+ webRtcEnabled: false,
3280
+ loginVoiceOptions: ['BROWSER', 'EXTENSION'],
3281
+ } as Profile;
3282
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
3283
+ webex.cc.webCallingService.setLoginOption = jest.fn((loginOption: LoginOption) => {
3284
+ webex.cc.webCallingService.loginOption = loginOption;
3285
+ });
3286
+ webex.cc['webexCrossClientService'].answerCallsState = true;
3287
+
3288
+ const publishSpy = jest
3289
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3290
+ .mockResolvedValue(undefined);
3291
+ jest
3292
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
3293
+ .mockImplementation(() => {});
3294
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
3295
+ jest.spyOn(webex.cc as any, 'releaseWxAppMercuryResources').mockResolvedValue(undefined);
3296
+
3297
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
3298
+ trackingId: 'track-logout',
3299
+ } as StationLogoutResponse);
3300
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
3301
+ data: {
3302
+ agentId: 'agentId',
3303
+ teamId: 'teamId',
3304
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
3305
+ },
3306
+ trackingId: 'track-login',
3307
+ } as StationLoginSuccess);
3308
+
3309
+ webex.internal.mercury.connected = true;
3310
+
3311
+ await webex.cc.updateAgentProfile({
3312
+ teamId: 'teamId',
3313
+ loginOption: LoginOption.EXTENSION,
3314
+ dialNumber: '1001',
3315
+ });
3316
+
3317
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
3318
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
3319
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
3320
+ });
3321
+
2131
3322
  it('should use provided teamId if passed in payload', async () => {
2132
3323
  const dataWithTeam = {
2133
3324
  teamId: 'newTeam',
@@ -2640,4 +3831,316 @@ describe('webex.cc', () => {
2640
3831
  expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'removePreviewContact', CC_FILE);
2641
3832
  });
2642
3833
  });
3834
+
3835
+ describe('private setManageWebexCallingInWxcc (runtime toggle impl)', () => {
3836
+ beforeEach(() => {
3837
+ webex.internal.device = {
3838
+ userId: 'user-123',
3839
+ registered: false,
3840
+ register: jest.fn().mockResolvedValue(undefined),
3841
+ unregister: jest.fn().mockResolvedValue(undefined),
3842
+ };
3843
+ webex.internal.mercury = {
3844
+ connected: false,
3845
+ connect: jest.fn().mockResolvedValue(undefined),
3846
+ disconnect: jest.fn().mockResolvedValue(undefined),
3847
+ };
3848
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
3849
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
3850
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
3851
+ webex.cc['agentConfig'] = {
3852
+ agentId: 'agent-123',
3853
+ deviceType: LoginOption.EXTENSION,
3854
+ } as Profile;
3855
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
3856
+ webex.cc.webCallingService.setLoginOption = jest.fn((loginOption: LoginOption) => {
3857
+ webex.cc.webCallingService.loginOption = loginOption;
3858
+ });
3859
+ });
3860
+
3861
+ afterEach(() => {
3862
+ jest.restoreAllMocks();
3863
+ });
3864
+
3865
+ it('should update config, task manager, and publish usersub when enabled', async () => {
3866
+ const publishSpy = jest
3867
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3868
+ .mockResolvedValue(undefined);
3869
+
3870
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
3871
+
3872
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
3873
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(true);
3874
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
3875
+ });
3876
+
3877
+ it('should publish false and update config when disabled after wxApp was enabled', async () => {
3878
+ webex.internal.device = {
3879
+ userId: 'user-123',
3880
+ url: 'https://wdm.example.com/devices/dev-1',
3881
+ };
3882
+ webex.request = jest.fn().mockResolvedValue({body: {}});
3883
+
3884
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
3885
+
3886
+ const publishSpy = jest
3887
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3888
+ .mockResolvedValue(undefined);
3889
+
3890
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
3891
+
3892
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3893
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
3894
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
3895
+ });
3896
+
3897
+ it('should skip usersub publish when disabling without prior wxApp publish', async () => {
3898
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3899
+ const publishSpy = jest
3900
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3901
+ .mockResolvedValue(undefined);
3902
+
3903
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
3904
+
3905
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3906
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
3907
+ expect(publishSpy).not.toHaveBeenCalled();
3908
+ });
3909
+
3910
+ it('should throw when enabling before login', async () => {
3911
+ webex.internal.device = undefined;
3912
+
3913
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3914
+ 'Cannot publish answer-calls-on-wxcc: user is not logged in'
3915
+ );
3916
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3917
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
3918
+ });
3919
+
3920
+ it('should throw when enabling on BROWSER (Desktop) login', async () => {
3921
+ webex.cc['agentConfig'] = {deviceType: LoginOption.BROWSER} as Profile;
3922
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
3923
+ const publishSpy = jest
3924
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3925
+ .mockResolvedValue(undefined);
3926
+
3927
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3928
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
3929
+ );
3930
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3931
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
3932
+ expect(publishSpy).not.toHaveBeenCalled();
3933
+ });
3934
+
3935
+ it('should throw when enabling before station login', async () => {
3936
+ webex.cc['agentConfig'] = undefined as unknown as Profile;
3937
+ webex.cc.webCallingService.loginOption = undefined as unknown as LoginOption;
3938
+ const publishSpy = jest
3939
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3940
+ .mockResolvedValue(undefined);
3941
+
3942
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3943
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
3944
+ );
3945
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3946
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
3947
+ expect(publishSpy).not.toHaveBeenCalled();
3948
+ });
3949
+
3950
+ it('should throw when webCallingService is BROWSER even if agentConfig deviceType is stale', async () => {
3951
+ webex.cc['agentConfig'] = {deviceType: LoginOption.EXTENSION} as Profile;
3952
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
3953
+ const publishSpy = jest
3954
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3955
+ .mockResolvedValue(undefined);
3956
+
3957
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3958
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
3959
+ );
3960
+ expect(publishSpy).not.toHaveBeenCalled();
3961
+ });
3962
+
3963
+ it('should publish usersub when enabling on AGENT_DN login', async () => {
3964
+ webex.cc['agentConfig'] = {
3965
+ agentId: 'agent-123',
3966
+ deviceType: LoginOption.AGENT_DN,
3967
+ } as Profile;
3968
+ const publishSpy = jest
3969
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3970
+ .mockResolvedValue(undefined);
3971
+
3972
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
3973
+
3974
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
3975
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
3976
+ });
3977
+
3978
+ it('should no-op usersub publish when disabling before login', async () => {
3979
+ webex.internal.device = undefined;
3980
+ const publishSpy = jest
3981
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3982
+ .mockResolvedValue(undefined);
3983
+
3984
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
3985
+
3986
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3987
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
3988
+ expect(publishSpy).not.toHaveBeenCalled();
3989
+ });
3990
+
3991
+ it('should subscribe to wxApp Mercury mute sync when enabled', async () => {
3992
+ webex.cc['agentConfig'] = {agentId: 'agent-123'} as Profile;
3993
+ webex.internal.device = {
3994
+ ...webex.internal.device,
3995
+ registered: true,
3996
+ register: jest.fn().mockResolvedValue(undefined),
3997
+ };
3998
+ webex.internal.mercury.connected = true;
3999
+ webex.internal.mercury.connect = jest.fn().mockResolvedValue(undefined);
4000
+ jest
4001
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4002
+ .mockResolvedValue(undefined);
4003
+ const subscribeSpy = jest
4004
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
4005
+ .mockImplementation(() => {});
4006
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
4007
+
4008
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
4009
+
4010
+ expect(subscribeSpy).toHaveBeenCalledWith('agent-123', expect.any(Function));
4011
+ });
4012
+
4013
+ it('should unsubscribe from wxApp Mercury mute sync when disabled', async () => {
4014
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
4015
+ jest
4016
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4017
+ .mockResolvedValue(undefined);
4018
+ const unsubscribeSpy = jest
4019
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe')
4020
+ .mockImplementation(() => {});
4021
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
4022
+
4023
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
4024
+
4025
+ expect(unsubscribeSpy).toHaveBeenCalled();
4026
+ });
4027
+
4028
+ it('should register device and connect mercury when enabling wxApp sync', async () => {
4029
+ webex.cc['agentConfig'] = {agentId: 'agent-123'} as Profile;
4030
+ webex.internal.device = {
4031
+ userId: 'user-123',
4032
+ registered: false,
4033
+ register: jest.fn().mockResolvedValue(undefined),
4034
+ };
4035
+ webex.internal.mercury = {
4036
+ connected: false,
4037
+ connect: jest.fn().mockResolvedValue(undefined),
4038
+ };
4039
+ jest
4040
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4041
+ .mockResolvedValue(undefined);
4042
+ const subscribeSpy = jest
4043
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
4044
+ .mockImplementation(() => {});
4045
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
4046
+
4047
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
4048
+
4049
+ expect(webex.internal.device.register).toHaveBeenCalled();
4050
+ expect(webex.internal.mercury.connect).toHaveBeenCalled();
4051
+ expect(subscribeSpy).toHaveBeenCalledWith('agent-123', expect.any(Function));
4052
+ });
4053
+
4054
+ it('should rollback config and task manager when usersub publish fails', async () => {
4055
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
4056
+ jest
4057
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4058
+ .mockRejectedValue(new Error('usersub failed'));
4059
+
4060
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow('usersub failed');
4061
+
4062
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4063
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(true);
4064
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenLastCalledWith(false);
4065
+ });
4066
+
4067
+ it('should rollback config and release Mercury resources when Mercury connect fails', async () => {
4068
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
4069
+ const publishSpy = jest
4070
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4071
+ .mockResolvedValue(undefined);
4072
+ jest
4073
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
4074
+ .mockRejectedValue(new Error('mercury connect failed'));
4075
+ const releaseSpy = jest
4076
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
4077
+ .mockResolvedValue(undefined);
4078
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
4079
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
4080
+
4081
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
4082
+ 'mercury connect failed'
4083
+ );
4084
+
4085
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4086
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenLastCalledWith(false);
4087
+ expect(publishSpy).not.toHaveBeenCalled();
4088
+ expect(teardownSpy).not.toHaveBeenCalled();
4089
+ expect(unsubscribeSpy).toHaveBeenCalled();
4090
+ expect(releaseSpy).toHaveBeenCalled();
4091
+ });
4092
+
4093
+ it('should schedule compensating false retry when rollback publish fails after enable error', async () => {
4094
+ jest.useFakeTimers();
4095
+ try {
4096
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
4097
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
4098
+ webex.cc.agentConfig = {
4099
+ agentId: 'agent-123',
4100
+ deviceType: LoginOption.EXTENSION,
4101
+ };
4102
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
4103
+
4104
+ const publishSpy = jest
4105
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4106
+ .mockResolvedValueOnce(undefined)
4107
+ .mockRejectedValueOnce(new Error('compensating false failed'))
4108
+ .mockResolvedValue(undefined);
4109
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
4110
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
4111
+ jest
4112
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
4113
+ .mockResolvedValue(undefined);
4114
+ jest
4115
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
4116
+ .mockResolvedValue(undefined);
4117
+ jest
4118
+ .spyOn(mockTaskManager, 'syncWxAppMuteFromCallDetailsForAllTasks')
4119
+ .mockImplementation(() => {
4120
+ throw new Error('sync failed');
4121
+ });
4122
+ const scheduleRetrySpy = jest.spyOn(
4123
+ webex.cc as any,
4124
+ 'scheduleCompensatingFalsePublishRetry'
4125
+ );
4126
+
4127
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
4128
+ 'sync failed'
4129
+ );
4130
+
4131
+ expect(scheduleRetrySpy).toHaveBeenCalled();
4132
+ expect(publishSpy).toHaveBeenCalledTimes(2);
4133
+ expect(publishSpy).toHaveBeenNthCalledWith(1, true, {userId: 'user-123', trackPublishMetrics: true});
4134
+ expect(publishSpy).toHaveBeenNthCalledWith(2, false, {userId: 'user-123', trackPublishMetrics: true});
4135
+
4136
+ jest.advanceTimersByTime(30_000);
4137
+ await publishSpy.mock.results[2]?.value;
4138
+
4139
+ expect(publishSpy).toHaveBeenCalledTimes(3);
4140
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
4141
+ } finally {
4142
+ jest.useRealTimers();
4143
+ }
4144
+ });
4145
+ });
2643
4146
  });