@webex/contact-center 3.12.0-llmrefactor.2 → 3.12.0-llmrefactor.3

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 (187) hide show
  1. package/.sdd/manifest.json +5 -4
  2. package/ai-docs/ARCHITECTURE.md +1 -1
  3. package/ai-docs/CONTRACTS.md +5 -1
  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 +440 -10
  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/behavioral-events.js +13 -0
  15. package/dist/metrics/behavioral-events.js.map +1 -1
  16. package/dist/metrics/constants.js +2 -0
  17. package/dist/metrics/constants.js.map +1 -1
  18. package/dist/services/AddressBook.js +18 -15
  19. package/dist/services/AddressBook.js.map +1 -1
  20. package/dist/services/AnswerCallOnWebexService.js +174 -0
  21. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  22. package/dist/services/EntryPoint.js +46 -68
  23. package/dist/services/EntryPoint.js.map +1 -1
  24. package/dist/services/Queue.js +27 -22
  25. package/dist/services/Queue.js.map +1 -1
  26. package/dist/services/WebexCrossClientService.js +171 -0
  27. package/dist/services/WebexCrossClientService.js.map +1 -0
  28. package/dist/services/WxAppTelephonyMercurySync.js +93 -0
  29. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  30. package/dist/services/config/Util.js +3 -0
  31. package/dist/services/config/Util.js.map +1 -1
  32. package/dist/services/config/constants.js +10 -6
  33. package/dist/services/config/constants.js.map +1 -1
  34. package/dist/services/config/types.js +4 -0
  35. package/dist/services/config/types.js.map +1 -1
  36. package/dist/services/core/Err.js.map +1 -1
  37. package/dist/services/core/WebexRequest.js +6 -2
  38. package/dist/services/core/WebexRequest.js.map +1 -1
  39. package/dist/services/core/aqm-reqs.js +28 -14
  40. package/dist/services/core/aqm-reqs.js.map +1 -1
  41. package/dist/services/core/types.js.map +1 -1
  42. package/dist/services/task/Task.js +73 -7
  43. package/dist/services/task/Task.js.map +1 -1
  44. package/dist/services/task/TaskFactory.js +8 -4
  45. package/dist/services/task/TaskFactory.js.map +1 -1
  46. package/dist/services/task/TaskManager.js +176 -20
  47. package/dist/services/task/TaskManager.js.map +1 -1
  48. package/dist/services/task/WebexCallingUtils.js +70 -0
  49. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  50. package/dist/services/task/constants.js +4 -1
  51. package/dist/services/task/constants.js.map +1 -1
  52. package/dist/services/task/contact.js +29 -0
  53. package/dist/services/task/contact.js.map +1 -1
  54. package/dist/services/task/digital/Digital.js +3 -2
  55. package/dist/services/task/digital/Digital.js.map +1 -1
  56. package/dist/services/task/state-machine/TaskStateMachine.js +135 -37
  57. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  58. package/dist/services/task/state-machine/actions.js +6 -1
  59. package/dist/services/task/state-machine/actions.js.map +1 -1
  60. package/dist/services/task/state-machine/guards.js +38 -11
  61. package/dist/services/task/state-machine/guards.js.map +1 -1
  62. package/dist/services/task/state-machine/types.js.map +1 -1
  63. package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
  64. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  65. package/dist/services/task/types.js +16 -1
  66. package/dist/services/task/types.js.map +1 -1
  67. package/dist/services/task/voice/Voice.js +350 -14
  68. package/dist/services/task/voice/Voice.js.map +1 -1
  69. package/dist/services/task/voice/WebRTC.js +4 -1
  70. package/dist/services/task/voice/WebRTC.js.map +1 -1
  71. package/dist/services/task/voice/wxAppVoiceMethods.js +201 -0
  72. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  73. package/dist/services/wxAppTelephonyUtils.js +19 -0
  74. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  75. package/dist/types/cc.d.ts +77 -4
  76. package/dist/types/config.d.ts +7 -0
  77. package/dist/types/constants.d.ts +8 -0
  78. package/dist/types/index.d.ts +3 -1
  79. package/dist/types/metrics/constants.d.ts +2 -0
  80. package/dist/types/services/AddressBook.d.ts +2 -1
  81. package/dist/types/services/AnswerCallOnWebexService.d.ts +37 -0
  82. package/dist/types/services/EntryPoint.d.ts +7 -5
  83. package/dist/types/services/Queue.d.ts +5 -3
  84. package/dist/types/services/WebexCrossClientService.d.ts +28 -0
  85. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  86. package/dist/types/services/config/constants.d.ts +8 -5
  87. package/dist/types/services/config/types.d.ts +17 -4
  88. package/dist/types/services/core/Err.d.ts +2 -0
  89. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  90. package/dist/types/services/core/types.d.ts +2 -0
  91. package/dist/types/services/task/Task.d.ts +20 -2
  92. package/dist/types/services/task/TaskFactory.d.ts +2 -1
  93. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  94. package/dist/types/services/task/constants.d.ts +3 -0
  95. package/dist/types/services/task/contact.d.ts +4 -0
  96. package/dist/types/services/task/digital/Digital.d.ts +2 -2
  97. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +260 -56
  98. package/dist/types/services/task/state-machine/guards.d.ts +6 -5
  99. package/dist/types/services/task/state-machine/types.d.ts +9 -1
  100. package/dist/types/services/task/types.d.ts +81 -4
  101. package/dist/types/services/task/voice/Voice.d.ts +48 -9
  102. package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
  103. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +52 -0
  104. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  105. package/dist/types/types.d.ts +14 -8
  106. package/dist/types/utils/PageCache.d.ts +20 -3
  107. package/dist/types.js +7 -2
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/PageCache.js +19 -5
  110. package/dist/utils/PageCache.js.map +1 -1
  111. package/dist/webex.js +1 -1
  112. package/package.json +1 -1
  113. package/src/cc.ts +506 -9
  114. package/src/config.ts +7 -0
  115. package/src/constants.ts +8 -0
  116. package/src/index.ts +2 -0
  117. package/src/metrics/ai-docs/metrics-spec.md +9 -3
  118. package/src/metrics/behavioral-events.ts +14 -0
  119. package/src/metrics/constants.ts +2 -0
  120. package/src/services/AddressBook.ts +17 -6
  121. package/src/services/AnswerCallOnWebexService.ts +206 -0
  122. package/src/services/EntryPoint.ts +59 -60
  123. package/src/services/Queue.ts +29 -12
  124. package/src/services/WebexCrossClientService.ts +212 -0
  125. package/src/services/WxAppTelephonyMercurySync.ts +115 -0
  126. package/src/services/ai-docs/AGENTS.md +10 -10
  127. package/src/services/ai-docs/services-spec.md +6 -1
  128. package/src/services/config/Util.ts +3 -0
  129. package/src/services/config/ai-docs/AGENTS.md +1 -1
  130. package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
  131. package/src/services/config/ai-docs/config-spec.md +6 -0
  132. package/src/services/config/constants.ts +10 -6
  133. package/src/services/config/types.ts +16 -4
  134. package/src/services/core/Err.ts +1 -0
  135. package/src/services/core/WebexRequest.ts +3 -1
  136. package/src/services/core/ai-docs/core-spec.md +5 -1
  137. package/src/services/core/aqm-reqs.ts +30 -15
  138. package/src/services/core/types.ts +2 -0
  139. package/src/services/task/Task.ts +79 -8
  140. package/src/services/task/TaskFactory.ts +8 -3
  141. package/src/services/task/TaskManager.ts +254 -15
  142. package/src/services/task/WebexCallingUtils.ts +136 -0
  143. package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
  144. package/src/services/task/ai-docs/task-spec.md +117 -2
  145. package/src/services/task/constants.ts +3 -0
  146. package/src/services/task/contact.ts +30 -0
  147. package/src/services/task/digital/Digital.ts +4 -2
  148. package/src/services/task/state-machine/TaskStateMachine.ts +210 -63
  149. package/src/services/task/state-machine/actions.ts +5 -2
  150. package/src/services/task/state-machine/ai-docs/AGENTS.md +8 -4
  151. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +17 -8
  152. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +46 -22
  153. package/src/services/task/state-machine/guards.ts +64 -11
  154. package/src/services/task/state-machine/types.ts +16 -1
  155. package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
  156. package/src/services/task/types.ts +114 -4
  157. package/src/services/task/voice/Voice.ts +462 -12
  158. package/src/services/task/voice/WebRTC.ts +5 -1
  159. package/src/services/task/voice/wxAppVoiceMethods.ts +307 -0
  160. package/src/services/wxAppTelephonyUtils.ts +14 -0
  161. package/src/types.ts +33 -9
  162. package/src/utils/AGENTS.md +21 -10
  163. package/src/utils/PageCache.ts +38 -5
  164. package/src/utils/ai-docs/utils-spec.md +21 -11
  165. package/test/unit/spec/cc.ts +1274 -0
  166. package/test/unit/spec/metrics/behavioral-events.ts +18 -0
  167. package/test/unit/spec/services/AddressBook.ts +37 -6
  168. package/test/unit/spec/services/AnswerCallOnWebexService.ts +223 -0
  169. package/test/unit/spec/services/EntryPoint.ts +87 -40
  170. package/test/unit/spec/services/Queue.ts +123 -12
  171. package/test/unit/spec/services/WebexCrossClientService.ts +261 -0
  172. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +113 -0
  173. package/test/unit/spec/services/config/Util.ts +85 -0
  174. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  175. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  176. package/test/unit/spec/services/task/Task.ts +200 -0
  177. package/test/unit/spec/services/task/TaskFactory.ts +39 -2
  178. package/test/unit/spec/services/task/TaskManager.ts +653 -1
  179. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  180. package/test/unit/spec/services/task/contact.ts +33 -0
  181. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +854 -72
  182. package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
  183. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
  184. package/test/unit/spec/services/task/voice/Voice.ts +874 -0
  185. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +459 -0
  186. package/umd/contact-center.min.js +2 -2
  187. 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);
@@ -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');
@@ -1019,6 +1033,419 @@ 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
+
1061
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1062
+ data: {
1063
+ agentId: 'agentId',
1064
+ teamId: 'teamId',
1065
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1066
+ },
1067
+ trackingId: 'track-1',
1068
+ } as StationLoginSuccess);
1069
+
1070
+ await webex.cc.stationLogin({
1071
+ teamId: 'teamId',
1072
+ loginOption: LoginOption.EXTENSION,
1073
+ dialNumber: '1001',
1074
+ });
1075
+
1076
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
1077
+ expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
1078
+ });
1079
+
1080
+ it('should rollback wxApp config when post-station-login init fails', async () => {
1081
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1082
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1083
+ webex.cc.agentConfig = {
1084
+ agentId: 'agentId',
1085
+ webRtcEnabled: false,
1086
+ loginVoiceOptions: ['EXTENSION'],
1087
+ };
1088
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1089
+ mockTaskManager.applyEnableWxBetterTogether(true);
1090
+
1091
+ jest
1092
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
1093
+ .mockRejectedValue(new Error('usersub publish failed'));
1094
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1095
+
1096
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1097
+ data: {
1098
+ agentId: 'agentId',
1099
+ teamId: 'teamId',
1100
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1101
+ },
1102
+ trackingId: 'track-1',
1103
+ } as StationLoginSuccess);
1104
+
1105
+ await webex.cc.stationLogin({
1106
+ teamId: 'teamId',
1107
+ loginOption: LoginOption.EXTENSION,
1108
+ dialNumber: '1001',
1109
+ });
1110
+
1111
+ expect(unsubscribeSpy).toHaveBeenCalled();
1112
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1113
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
1114
+ });
1115
+
1116
+ it('should reset wxApp config when Mercury fails before usersub publish on station login', async () => {
1117
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1118
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1119
+ webex.cc.agentConfig = {
1120
+ agentId: 'agentId',
1121
+ webRtcEnabled: false,
1122
+ loginVoiceOptions: ['EXTENSION'],
1123
+ };
1124
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1125
+ mockTaskManager.applyEnableWxBetterTogether(true);
1126
+
1127
+ const publishSpy = jest
1128
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1129
+ .mockResolvedValue(undefined);
1130
+ jest
1131
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
1132
+ .mockRejectedValue(new Error('mercury connect failed'));
1133
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1134
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1135
+ jest
1136
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1137
+ .mockResolvedValue(undefined);
1138
+
1139
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1140
+ data: {
1141
+ agentId: 'agentId',
1142
+ teamId: 'teamId',
1143
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1144
+ },
1145
+ trackingId: 'track-1',
1146
+ } as StationLoginSuccess);
1147
+
1148
+ await webex.cc.stationLogin({
1149
+ teamId: 'teamId',
1150
+ loginOption: LoginOption.EXTENSION,
1151
+ dialNumber: '1001',
1152
+ });
1153
+
1154
+ expect(publishSpy).not.toHaveBeenCalled();
1155
+ expect(teardownSpy).not.toHaveBeenCalled();
1156
+ expect(unsubscribeSpy).toHaveBeenCalled();
1157
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1158
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
1159
+ });
1160
+
1161
+ it('should register device before publishing usersub true on extension station login', async () => {
1162
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1163
+ webex.internal.device = {
1164
+ userId: 'user-123',
1165
+ registered: false,
1166
+ register: jest.fn().mockImplementation(async function (this: {
1167
+ url?: string;
1168
+ registered?: boolean;
1169
+ }) {
1170
+ this.url = 'https://wdm.example.com/devices/dev-1';
1171
+ this.registered = true;
1172
+ }),
1173
+ };
1174
+ webex.internal.mercury = {
1175
+ connected: false,
1176
+ connect: jest.fn().mockResolvedValue(undefined),
1177
+ };
1178
+ webex.cc.agentConfig = {
1179
+ agentId: 'agentId',
1180
+ webRtcEnabled: false,
1181
+ loginVoiceOptions: ['EXTENSION'],
1182
+ };
1183
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1184
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1185
+
1186
+ const callOrder: string[] = [];
1187
+ jest.spyOn(webex.internal.device, 'register').mockImplementation(async function (this: {
1188
+ url?: string;
1189
+ registered?: boolean;
1190
+ }) {
1191
+ callOrder.push('device.register');
1192
+ this.url = 'https://wdm.example.com/devices/dev-1';
1193
+ this.registered = true;
1194
+ });
1195
+ jest
1196
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1197
+ .mockImplementation(async (enable: boolean) => {
1198
+ if (enable === true && !webex.internal.device.url) {
1199
+ throw new Error('Device URL is unavailable for cross-client publish');
1200
+ }
1201
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
1202
+ });
1203
+
1204
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1205
+ data: {
1206
+ agentId: 'agentId',
1207
+ teamId: 'teamId',
1208
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1209
+ },
1210
+ trackingId: 'track-1',
1211
+ } as StationLoginSuccess);
1212
+
1213
+ await webex.cc.stationLogin({
1214
+ teamId: 'teamId',
1215
+ loginOption: LoginOption.EXTENSION,
1216
+ dialNumber: '1001',
1217
+ });
1218
+
1219
+ expect(callOrder).toEqual(['device.register', 'publish:true']);
1220
+ });
1221
+
1222
+ it('should register device before publishing forced usersub false when wxApp is disabled at station login', async () => {
1223
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1224
+ webex.internal.device = {
1225
+ userId: 'user-123',
1226
+ registered: false,
1227
+ register: jest.fn().mockImplementation(async function (this: {
1228
+ url?: string;
1229
+ registered?: boolean;
1230
+ }) {
1231
+ this.url = 'https://wdm.example.com/devices/dev-1';
1232
+ this.registered = true;
1233
+ }),
1234
+ };
1235
+ webex.internal.mercury = {
1236
+ connected: false,
1237
+ connect: jest.fn().mockResolvedValue(undefined),
1238
+ };
1239
+ webex.cc.agentConfig = {
1240
+ agentId: 'agentId',
1241
+ webRtcEnabled: false,
1242
+ loginVoiceOptions: ['EXTENSION'],
1243
+ };
1244
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1245
+ jest
1246
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1247
+ .mockResolvedValue(undefined);
1248
+
1249
+ const callOrder: string[] = [];
1250
+ jest.spyOn(webex.internal.device, 'register').mockImplementation(async function (this: {
1251
+ url?: string;
1252
+ registered?: boolean;
1253
+ }) {
1254
+ callOrder.push('device.register');
1255
+ this.url = 'https://wdm.example.com/devices/dev-1';
1256
+ this.registered = true;
1257
+ });
1258
+ jest
1259
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1260
+ .mockImplementation(async (enable: boolean) => {
1261
+ if (!webex.internal.device.url) {
1262
+ throw new Error('Device URL is unavailable for cross-client publish');
1263
+ }
1264
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
1265
+ });
1266
+
1267
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1268
+ data: {
1269
+ agentId: 'agentId',
1270
+ teamId: 'teamId',
1271
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1272
+ },
1273
+ trackingId: 'track-1',
1274
+ } as StationLoginSuccess);
1275
+
1276
+ await webex.cc.stationLogin({
1277
+ teamId: 'teamId',
1278
+ loginOption: LoginOption.EXTENSION,
1279
+ dialNumber: '1001',
1280
+ });
1281
+
1282
+ expect(callOrder).toEqual(['device.register', 'publish:false']);
1283
+ });
1284
+
1285
+ it('should retry forced false publish when wxApp is disabled at station login', async () => {
1286
+ jest.useFakeTimers();
1287
+ try {
1288
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1289
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1290
+ webex.cc.agentConfig = {
1291
+ agentId: 'agentId',
1292
+ webRtcEnabled: false,
1293
+ loginVoiceOptions: ['EXTENSION'],
1294
+ };
1295
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1296
+
1297
+ const publishSpy = jest
1298
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1299
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
1300
+ .mockResolvedValue(undefined);
1301
+ const scheduleRetrySpy = jest.spyOn(
1302
+ webex.cc as any,
1303
+ 'scheduleForcedFalsePublishRetry'
1304
+ );
1305
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1306
+ jest
1307
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1308
+ .mockResolvedValue(undefined);
1309
+
1310
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1311
+ data: {
1312
+ agentId: 'agentId',
1313
+ teamId: 'teamId',
1314
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1315
+ },
1316
+ trackingId: 'track-1',
1317
+ } as StationLoginSuccess);
1318
+
1319
+ await webex.cc.stationLogin({
1320
+ teamId: 'teamId',
1321
+ loginOption: LoginOption.EXTENSION,
1322
+ dialNumber: '1001',
1323
+ });
1324
+
1325
+ expect(publishSpy).toHaveBeenCalledTimes(1);
1326
+ expect(scheduleRetrySpy).toHaveBeenCalled();
1327
+ expect(teardownSpy).not.toHaveBeenCalled();
1328
+
1329
+ jest.advanceTimersByTime(30_000);
1330
+ await publishSpy.mock.results[1]?.value;
1331
+
1332
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1333
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123'});
1334
+ } finally {
1335
+ jest.useRealTimers();
1336
+ }
1337
+ });
1338
+
1339
+ it('should not publish usersub on BROWSER station login even when enableWxBetterTogether is true at init', async () => {
1340
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1341
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1342
+ webex.cc.agentConfig = {
1343
+ agentId: 'agentId',
1344
+ webRtcEnabled: true,
1345
+ loginVoiceOptions: ['BROWSER'],
1346
+ };
1347
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
1348
+
1349
+ const publishSpy = jest.spyOn(
1350
+ webex.cc['webexCrossClientService'],
1351
+ 'setManageWebexCallingInWxcc'
1352
+ );
1353
+
1354
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1355
+ data: {
1356
+ agentId: 'agentId',
1357
+ teamId: 'teamId',
1358
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1359
+ },
1360
+ trackingId: 'track-1',
1361
+ } as StationLoginSuccess);
1362
+
1363
+ await webex.cc.stationLogin({
1364
+ teamId: 'teamId',
1365
+ loginOption: LoginOption.BROWSER,
1366
+ });
1367
+
1368
+ expect(publishSpy).not.toHaveBeenCalled();
1369
+ });
1370
+
1371
+ it('should force-publish usersub false on station login when enableWxBetterTogether is false at init', async () => {
1372
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1373
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1374
+ webex.cc.agentConfig = {
1375
+ agentId: 'agentId',
1376
+ webRtcEnabled: false,
1377
+ loginVoiceOptions: ['EXTENSION'],
1378
+ };
1379
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1380
+
1381
+ const publishSpy = jest
1382
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1383
+ .mockResolvedValue(undefined);
1384
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1385
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1386
+
1387
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1388
+ data: {
1389
+ agentId: 'agentId',
1390
+ teamId: 'teamId',
1391
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1392
+ },
1393
+ trackingId: 'track-1',
1394
+ } as StationLoginSuccess);
1395
+
1396
+ await webex.cc.stationLogin({
1397
+ teamId: 'teamId',
1398
+ loginOption: LoginOption.EXTENSION,
1399
+ dialNumber: '1001',
1400
+ });
1401
+
1402
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
1403
+ expect(teardownSpy).toHaveBeenCalled();
1404
+ expect(unsubscribeSpy).toHaveBeenCalled();
1405
+ });
1406
+
1407
+ it('should clear stale usersub on login after prior session enabled wxApp then hard refresh', async () => {
1408
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1409
+ webex.cc.agentConfig = {
1410
+ agentId: 'agentId',
1411
+ webRtcEnabled: false,
1412
+ loginVoiceOptions: ['EXTENSION'],
1413
+ };
1414
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1415
+ webex.request = jest.fn().mockResolvedValue({body: {}});
1416
+
1417
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1418
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1419
+ data: {
1420
+ agentId: 'agentId',
1421
+ teamId: 'teamId',
1422
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1423
+ },
1424
+ trackingId: 'track-1',
1425
+ } as StationLoginSuccess);
1426
+
1427
+ await webex.cc.stationLogin({
1428
+ teamId: 'teamId',
1429
+ loginOption: LoginOption.EXTENSION,
1430
+ dialNumber: '1001',
1431
+ });
1432
+
1433
+ webex.cc['webexCrossClientService'].teardown();
1434
+ webex.cc['wxAppTelephonyMercurySync'].unsubscribe();
1435
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1436
+
1437
+ const publishSpy = jest
1438
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1439
+ .mockResolvedValue(undefined);
1440
+
1441
+ await webex.cc.stationLogin({
1442
+ teamId: 'teamId',
1443
+ loginOption: LoginOption.EXTENSION,
1444
+ dialNumber: '1001',
1445
+ });
1446
+
1447
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
1448
+ });
1022
1449
  });
1023
1450
 
1024
1451
  describe('stationLogout', () => {
@@ -1061,6 +1488,214 @@ describe('webex.cc', () => {
1061
1488
  ]);
1062
1489
  });
1063
1490
 
1491
+ it('should not publish usersub false on logout when wxApp was never enabled', async () => {
1492
+ webex.internal.device = {userId: 'user-123'};
1493
+ const publishSpy = jest
1494
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1495
+ .mockResolvedValue(undefined);
1496
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1497
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1498
+
1499
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1500
+ trackingId: 'track-1',
1501
+ } as StationLogoutResponse);
1502
+
1503
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1504
+
1505
+ expect(publishSpy).not.toHaveBeenCalled();
1506
+ expect(teardownSpy).toHaveBeenCalled();
1507
+ expect(unsubscribeSpy).toHaveBeenCalled();
1508
+ });
1509
+
1510
+ it('should publish usersub false and teardown wxApp sync on logout when wxApp was enabled', async () => {
1511
+ webex.internal.device = {
1512
+ userId: 'user-123',
1513
+ url: 'https://wdm.example.com/devices/dev-1',
1514
+ registered: false,
1515
+ register: jest.fn().mockResolvedValue(undefined),
1516
+ unregister: jest.fn().mockResolvedValue(undefined),
1517
+ };
1518
+ webex.internal.mercury = {
1519
+ connected: false,
1520
+ connect: jest.fn().mockResolvedValue(undefined),
1521
+ disconnect: jest.fn().mockResolvedValue(undefined),
1522
+ };
1523
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1524
+ webex.request = jest.fn().mockResolvedValue({body: {}});
1525
+ webex.cc['agentConfig'] = {
1526
+ agentId: 'agent-123',
1527
+ deviceType: LoginOption.EXTENSION,
1528
+ } as Profile;
1529
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1530
+
1531
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
1532
+
1533
+ const publishSpy = jest
1534
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1535
+ .mockResolvedValue(undefined);
1536
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1537
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1538
+
1539
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1540
+ trackingId: 'track-1',
1541
+ } as StationLogoutResponse);
1542
+
1543
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1544
+
1545
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
1546
+ expect(teardownSpy).toHaveBeenCalled();
1547
+ expect(unsubscribeSpy).toHaveBeenCalled();
1548
+ });
1549
+
1550
+ it('should teardown wxApp state on logout even when usersub publish fails', async () => {
1551
+ webex.internal.device = {userId: 'user-123'};
1552
+ webex.cc['agentConfig'] = {
1553
+ agentId: 'agent-123',
1554
+ deviceType: LoginOption.EXTENSION,
1555
+ } as Profile;
1556
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1557
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1558
+ mockTaskManager.applyEnableWxBetterTogether(true);
1559
+
1560
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1561
+ trackingId: 'track-1',
1562
+ } as StationLogoutResponse);
1563
+ jest
1564
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
1565
+ .mockRejectedValue(new Error('usersub publish failed'));
1566
+ const scheduleRetrySpy = jest.spyOn(
1567
+ webex.cc as any,
1568
+ 'scheduleCompensatingFalsePublishRetry'
1569
+ );
1570
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1571
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1572
+
1573
+ const result = await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1574
+
1575
+ expect(result).toEqual({trackingId: 'track-1'});
1576
+ expect(teardownSpy).toHaveBeenCalled();
1577
+ expect(unsubscribeSpy).toHaveBeenCalled();
1578
+ expect(scheduleRetrySpy).toHaveBeenCalled();
1579
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
1580
+ });
1581
+
1582
+ it('should retry compensating false publish when teardown usersub publish fails on logout', async () => {
1583
+ jest.useFakeTimers();
1584
+ try {
1585
+ webex.internal.device = {
1586
+ userId: 'user-123',
1587
+ url: 'https://wdm.example.com/devices/dev-1',
1588
+ };
1589
+ webex.cc['agentConfig'] = {
1590
+ agentId: 'agent-123',
1591
+ deviceType: LoginOption.EXTENSION,
1592
+ } as Profile;
1593
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1594
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1595
+ mockTaskManager.applyEnableWxBetterTogether(true);
1596
+ webex.cc['webexCrossClientService'].answerCallsState = true;
1597
+
1598
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1599
+ trackingId: 'track-1',
1600
+ } as StationLogoutResponse);
1601
+
1602
+ const publishSpy = jest
1603
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1604
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
1605
+ .mockResolvedValue(undefined);
1606
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1607
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1608
+ jest
1609
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1610
+ .mockResolvedValue(undefined);
1611
+
1612
+ const result = await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1613
+
1614
+ expect(result).toEqual({trackingId: 'track-1'});
1615
+ expect(publishSpy).toHaveBeenCalledTimes(1);
1616
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
1617
+ expect(teardownSpy).toHaveBeenCalled();
1618
+ expect(unsubscribeSpy).toHaveBeenCalled();
1619
+
1620
+ jest.advanceTimersByTime(30_000);
1621
+ await publishSpy.mock.results[1]?.value;
1622
+
1623
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1624
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123'});
1625
+ } finally {
1626
+ jest.useRealTimers();
1627
+ }
1628
+ });
1629
+
1630
+ it('should cancel compensating retry when station login succeeds before retry fires', async () => {
1631
+ jest.useFakeTimers();
1632
+ try {
1633
+ webex.internal.device = {
1634
+ userId: 'user-123',
1635
+ url: 'https://wdm.example.com/devices/dev-1',
1636
+ registered: false,
1637
+ register: jest.fn().mockResolvedValue(undefined),
1638
+ unregister: jest.fn().mockResolvedValue(undefined),
1639
+ };
1640
+ webex.internal.mercury = {
1641
+ connected: false,
1642
+ connect: jest.fn().mockResolvedValue(undefined),
1643
+ disconnect: jest.fn().mockResolvedValue(undefined),
1644
+ };
1645
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1646
+ webex.cc['agentConfig'] = {
1647
+ agentId: 'agent-123',
1648
+ deviceType: LoginOption.EXTENSION,
1649
+ webRtcEnabled: false,
1650
+ loginVoiceOptions: ['EXTENSION'],
1651
+ } as Profile;
1652
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1653
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1654
+ mockTaskManager.applyEnableWxBetterTogether(true);
1655
+ webex.cc['webexCrossClientService'].answerCallsState = true;
1656
+
1657
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1658
+ trackingId: 'track-1',
1659
+ } as StationLogoutResponse);
1660
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1661
+ data: {
1662
+ agentId: 'agent-123',
1663
+ teamId: 'team-123',
1664
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1665
+ },
1666
+ trackingId: 'track-2',
1667
+ } as StationLoginSuccess);
1668
+
1669
+ const publishSpy = jest
1670
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1671
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
1672
+ .mockResolvedValue(undefined);
1673
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1674
+ jest
1675
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1676
+ .mockResolvedValue(undefined);
1677
+
1678
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1679
+ await webex.cc.stationLogin({
1680
+ teamId: 'team-123',
1681
+ loginOption: LoginOption.EXTENSION,
1682
+ dialNumber: '1001',
1683
+ });
1684
+
1685
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1686
+ expect(publishSpy).toHaveBeenNthCalledWith(1, false, {userId: 'user-123'});
1687
+ expect(publishSpy).toHaveBeenNthCalledWith(2, true, {userId: 'user-123'});
1688
+ expect(teardownSpy).toHaveBeenCalledTimes(1);
1689
+
1690
+ jest.advanceTimersByTime(30_000);
1691
+
1692
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1693
+ expect(teardownSpy).toHaveBeenCalledTimes(1);
1694
+ } finally {
1695
+ jest.useRealTimers();
1696
+ }
1697
+ });
1698
+
1064
1699
  it('should handle error during stationLogout', async () => {
1065
1700
  const data = {logoutReason: 'Logout reason'};
1066
1701
  const error = {
@@ -1276,6 +1911,41 @@ describe('webex.cc', () => {
1276
1911
  ]);
1277
1912
  });
1278
1913
 
1914
+ it('should map Transfer action to an Available backend state filter', async () => {
1915
+ webex.cc.agentConfig = {agentProfileID: 'test-agent-profile-id'};
1916
+ const buddyAgentsResponse = {data: {agentList: []}};
1917
+ const buddyAgentsSpy = jest
1918
+ .spyOn(webex.cc.services.agent, 'buddyAgents')
1919
+ .mockResolvedValue(buddyAgentsResponse);
1920
+
1921
+ await webex.cc.getBuddyAgents({action: 'Transfer', mediaType: 'chat'});
1922
+
1923
+ expect(buddyAgentsSpy).toHaveBeenCalledWith({
1924
+ data: {
1925
+ agentProfileId: 'test-agent-profile-id',
1926
+ mediaType: 'chat',
1927
+ state: 'Available',
1928
+ },
1929
+ });
1930
+ });
1931
+
1932
+ it('should omit the backend state filter for Consult and default media type to telephony', async () => {
1933
+ webex.cc.agentConfig = {agentProfileID: 'test-agent-profile-id'};
1934
+ const buddyAgentsResponse = {data: {agentList: []}};
1935
+ const buddyAgentsSpy = jest
1936
+ .spyOn(webex.cc.services.agent, 'buddyAgents')
1937
+ .mockResolvedValue(buddyAgentsResponse);
1938
+
1939
+ await webex.cc.getBuddyAgents({action: 'Consult'});
1940
+
1941
+ expect(buddyAgentsSpy).toHaveBeenCalledWith({
1942
+ data: {
1943
+ agentProfileId: 'test-agent-profile-id',
1944
+ mediaType: 'telephony',
1945
+ },
1946
+ });
1947
+ });
1948
+
1279
1949
  it('should handle error', async () => {
1280
1950
  const data: BuddyAgents = {state: 'Available', mediaType: 'telephony'};
1281
1951
  webex.cc.agentConfig = {
@@ -1503,6 +2173,81 @@ describe('webex.cc', () => {
1503
2173
  expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.AGENT_DN);
1504
2174
  expect(webex.cc.agentConfig.dn).toBe('67890');
1505
2175
  });
2176
+
2177
+ it('should publish usersub true on silent relogin when enableWxBetterTogether is true at init', async () => {
2178
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2179
+ webex.internal.device = {
2180
+ userId: 'user-123',
2181
+ url: 'https://wdm.example.com/devices/dev-1',
2182
+ registered: true,
2183
+ register: jest.fn().mockResolvedValue(undefined),
2184
+ };
2185
+ webex.internal.mercury.connected = true;
2186
+ webex.internal.mercury.connect = jest.fn().mockResolvedValue(undefined);
2187
+ webex.cc.agentConfig = {
2188
+ agentId: 'agentId',
2189
+ agentProfileID: 'test-agent-profile-id',
2190
+ isAgentLoggedIn: false,
2191
+ } as Profile;
2192
+
2193
+ jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
2194
+ data: {
2195
+ auxCodeId: 'auxCodeId',
2196
+ agentId: 'agentId',
2197
+ deviceType: LoginOption.EXTENSION,
2198
+ dn: '1001',
2199
+ teamId: 'teamId',
2200
+ lastStateChangeTimestamp: 1738575135188,
2201
+ lastIdleCodeChangeTimestamp: 1738575135189,
2202
+ },
2203
+ });
2204
+
2205
+ const publishSpy = jest
2206
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2207
+ .mockResolvedValue(undefined);
2208
+ const subscribeSpy = jest
2209
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
2210
+ .mockImplementation(() => {});
2211
+
2212
+ await webex.cc['silentRelogin']();
2213
+
2214
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
2215
+ expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
2216
+ });
2217
+
2218
+ it('should force-publish usersub false on silent relogin when enableWxBetterTogether is false at init', async () => {
2219
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
2220
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
2221
+ webex.cc.agentConfig = {
2222
+ agentId: 'agentId',
2223
+ agentProfileID: 'test-agent-profile-id',
2224
+ isAgentLoggedIn: false,
2225
+ } as Profile;
2226
+
2227
+ jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
2228
+ data: {
2229
+ auxCodeId: 'auxCodeId',
2230
+ agentId: 'agentId',
2231
+ deviceType: LoginOption.EXTENSION,
2232
+ dn: '1001',
2233
+ teamId: 'teamId',
2234
+ lastStateChangeTimestamp: 1738575135188,
2235
+ lastIdleCodeChangeTimestamp: 1738575135189,
2236
+ },
2237
+ });
2238
+
2239
+ const publishSpy = jest
2240
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2241
+ .mockResolvedValue(undefined);
2242
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
2243
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
2244
+
2245
+ await webex.cc['silentRelogin']();
2246
+
2247
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
2248
+ expect(teardownSpy).toHaveBeenCalled();
2249
+ expect(unsubscribeSpy).toHaveBeenCalled();
2250
+ });
1506
2251
  });
1507
2252
 
1508
2253
  describe('setupEventListeners()', () => {
@@ -1741,6 +2486,18 @@ describe('webex.cc', () => {
1741
2486
  on: jest.fn(),
1742
2487
  } as any;
1743
2488
 
2489
+ webex.internal.device = {
2490
+ ...webex.internal.device,
2491
+ userId: 'user-123',
2492
+ unregister: jest.fn().mockResolvedValue(undefined),
2493
+ };
2494
+ deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
2495
+ const publishSpy = jest
2496
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2497
+ .mockResolvedValue(undefined);
2498
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
2499
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
2500
+
1744
2501
  await webex.cc.deregister();
1745
2502
 
1746
2503
  expect(mockTaskManager.off).toHaveBeenCalledWith(
@@ -1829,6 +2586,39 @@ describe('webex.cc', () => {
1829
2586
  expect(connectionCalls).toHaveLength(1);
1830
2587
  const [, connectionCallback] = connectionCalls[0];
1831
2588
  expect(connectionCallback).toBe(webex.cc['handleConnectionLost']);
2589
+
2590
+ expect(publishSpy).not.toHaveBeenCalled();
2591
+ expect(teardownSpy).toHaveBeenCalled();
2592
+ expect(unsubscribeSpy).toHaveBeenCalled();
2593
+ });
2594
+
2595
+ it('should publish usersub false before device unregister when wxApp is active on deregister', async () => {
2596
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2597
+ webex.cc['webexCrossClientService'].answerCallsState = true;
2598
+ webex.internal.device = {
2599
+ userId: 'user-123',
2600
+ url: 'https://wdm.example.com/devices/dev-1',
2601
+ unregister: jest.fn().mockResolvedValue(undefined),
2602
+ };
2603
+ deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
2604
+
2605
+ const callOrder: string[] = [];
2606
+ jest
2607
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2608
+ .mockImplementation(async (enable: boolean) => {
2609
+ if (enable === false && !webex.internal.device.url) {
2610
+ throw new Error('Device URL is unavailable for cross-client publish');
2611
+ }
2612
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
2613
+ });
2614
+ deviceUnregisterSpy.mockImplementation(async () => {
2615
+ callOrder.push('device.unregister');
2616
+ webex.internal.device.url = undefined;
2617
+ });
2618
+
2619
+ await webex.cc.deregister();
2620
+
2621
+ expect(callOrder).toEqual(['publish:false', 'device.unregister']);
1832
2622
  });
1833
2623
 
1834
2624
  it('should skip webCallingService and internal cleanup when webrtc is disabled', async () => {
@@ -1878,6 +2668,135 @@ describe('webex.cc', () => {
1878
2668
  expect(webex.cc.agentConfig).toBeNull();
1879
2669
  });
1880
2670
 
2671
+ it('should teardown wxApp state on deregister even when usersub publish fails', async () => {
2672
+ webex.cc.agentConfig = {
2673
+ agentId: 'agentId',
2674
+ webRtcEnabled: false,
2675
+ loginVoiceOptions: [LoginOption.EXTENSION],
2676
+ };
2677
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2678
+ webex.cc['webexCrossClientService'].answerCallsState = true;
2679
+ webex.internal.device = {
2680
+ userId: 'user-123',
2681
+ unregister: jest.fn().mockResolvedValue(undefined),
2682
+ };
2683
+
2684
+ jest
2685
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
2686
+ .mockRejectedValue(new Error('usersub publish failed'));
2687
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
2688
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
2689
+
2690
+ await expect(webex.cc.deregister()).rejects.toThrow('usersub publish failed');
2691
+
2692
+ expect(teardownSpy).toHaveBeenCalled();
2693
+ expect(unsubscribeSpy).toHaveBeenCalled();
2694
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
2695
+ expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
2696
+ expect(webex.cc.agentConfig).toBeNull();
2697
+ });
2698
+
2699
+ it('should publish forced false during teardown when enable publish is still in flight', async () => {
2700
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2701
+ webex.internal.device = {
2702
+ userId: 'user-123',
2703
+ url: 'https://wdm.example.com/devices/dev-1',
2704
+ };
2705
+
2706
+ let resolveEnable: () => void = () => {};
2707
+ const enableGate = new Promise<void>((resolve) => {
2708
+ resolveEnable = resolve;
2709
+ });
2710
+
2711
+ const publishSpy = jest
2712
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2713
+ .mockImplementation(async (enable: boolean) => {
2714
+ if (enable) {
2715
+ await enableGate;
2716
+ }
2717
+ });
2718
+ jest
2719
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
2720
+ .mockResolvedValue(undefined);
2721
+
2722
+ expect(webex.cc['webexCrossClientService'].answerCallsState).toBe(false);
2723
+
2724
+ const enableInFlight = (webex.cc as any).publishAnswerOnWebexCrossClientState(true);
2725
+
2726
+ await (webex.cc as any).teardownWxAppLocalState();
2727
+
2728
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
2729
+
2730
+ resolveEnable();
2731
+ await enableInFlight;
2732
+ });
2733
+
2734
+ it('should release wxApp-owned Mercury and device on deregister', async () => {
2735
+ webex.cc.agentConfig = {
2736
+ agentId: 'agentId',
2737
+ webRtcEnabled: false,
2738
+ loginVoiceOptions: [LoginOption.EXTENSION],
2739
+ };
2740
+ webex.cc['wxAppMercuryConnectedByCc'] = true;
2741
+ webex.cc['wxAppDeviceRegisteredByCc'] = true;
2742
+ webex.internal.mercury.connected = true;
2743
+ webex.internal.device.registered = true;
2744
+
2745
+ const mercuryDisconnectSpy = jest
2746
+ .spyOn(webex.internal.mercury, 'disconnect')
2747
+ .mockResolvedValue(undefined);
2748
+ const deviceUnregisterSpy = jest
2749
+ .spyOn(webex.internal.device, 'unregister')
2750
+ .mockResolvedValue(undefined);
2751
+
2752
+ await webex.cc.deregister();
2753
+
2754
+ expect(mercuryDisconnectSpy).toHaveBeenCalled();
2755
+ expect(deviceUnregisterSpy).toHaveBeenCalled();
2756
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(false);
2757
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(false);
2758
+ });
2759
+
2760
+ it('should retain wxApp Mercury ownership when disconnect fails and retry cleanup', async () => {
2761
+ webex.cc['wxAppMercuryConnectedByCc'] = true;
2762
+ webex.internal.mercury.connected = true;
2763
+
2764
+ const mercuryDisconnectSpy = jest
2765
+ .spyOn(webex.internal.mercury, 'disconnect')
2766
+ .mockRejectedValueOnce(new Error('disconnect failed'))
2767
+ .mockResolvedValueOnce(undefined);
2768
+
2769
+ await webex.cc['releaseWxAppMercuryResources']();
2770
+
2771
+ expect(mercuryDisconnectSpy).toHaveBeenCalledTimes(1);
2772
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(true);
2773
+
2774
+ await webex.cc['releaseWxAppMercuryResources']();
2775
+
2776
+ expect(mercuryDisconnectSpy).toHaveBeenCalledTimes(2);
2777
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(false);
2778
+ });
2779
+
2780
+ it('should retain wxApp device ownership when unregister fails and retry cleanup', async () => {
2781
+ webex.cc['wxAppDeviceRegisteredByCc'] = true;
2782
+ webex.internal.device.registered = true;
2783
+
2784
+ const deviceUnregisterSpy = jest
2785
+ .spyOn(webex.internal.device, 'unregister')
2786
+ .mockRejectedValueOnce(new Error('unregister failed'))
2787
+ .mockResolvedValueOnce(undefined);
2788
+
2789
+ await webex.cc['releaseWxAppMercuryResources']();
2790
+
2791
+ expect(deviceUnregisterSpy).toHaveBeenCalledTimes(1);
2792
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(true);
2793
+
2794
+ await webex.cc['releaseWxAppMercuryResources']();
2795
+
2796
+ expect(deviceUnregisterSpy).toHaveBeenCalledTimes(2);
2797
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(false);
2798
+ });
2799
+
1881
2800
  it('should handle errors during unregister and track metrics', async () => {
1882
2801
  const mockError = new Error('Failed to deregister device');
1883
2802
  webex.internal.device.unregister.mockRejectedValue(mockError);
@@ -2128,6 +3047,54 @@ describe('webex.cc', () => {
2128
3047
  expect(result).toEqual(mockResp);
2129
3048
  });
2130
3049
 
3050
+ it('should preserve enableWxBetterTogether init flag across profile update logout/login', async () => {
3051
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3052
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
3053
+ webex.cc.agentConfig = {
3054
+ ...webex.cc.agentConfig,
3055
+ agentId: 'agentId',
3056
+ webRtcEnabled: false,
3057
+ loginVoiceOptions: ['BROWSER', 'EXTENSION'],
3058
+ } as Profile;
3059
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
3060
+ webex.cc.webCallingService.setLoginOption = jest.fn((loginOption: LoginOption) => {
3061
+ webex.cc.webCallingService.loginOption = loginOption;
3062
+ });
3063
+ webex.cc['webexCrossClientService'].answerCallsState = true;
3064
+
3065
+ const publishSpy = jest
3066
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3067
+ .mockResolvedValue(undefined);
3068
+ jest
3069
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
3070
+ .mockImplementation(() => {});
3071
+ jest.spyOn(webex.cc as any, 'releaseWxAppMercuryResources').mockResolvedValue(undefined);
3072
+
3073
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
3074
+ trackingId: 'track-logout',
3075
+ } as StationLogoutResponse);
3076
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
3077
+ data: {
3078
+ agentId: 'agentId',
3079
+ teamId: 'teamId',
3080
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
3081
+ },
3082
+ trackingId: 'track-login',
3083
+ } as StationLoginSuccess);
3084
+
3085
+ webex.internal.mercury.connected = true;
3086
+
3087
+ await webex.cc.updateAgentProfile({
3088
+ teamId: 'teamId',
3089
+ loginOption: LoginOption.EXTENSION,
3090
+ dialNumber: '1001',
3091
+ });
3092
+
3093
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
3094
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
3095
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
3096
+ });
3097
+
2131
3098
  it('should use provided teamId if passed in payload', async () => {
2132
3099
  const dataWithTeam = {
2133
3100
  teamId: 'newTeam',
@@ -2640,4 +3607,311 @@ describe('webex.cc', () => {
2640
3607
  expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'removePreviewContact', CC_FILE);
2641
3608
  });
2642
3609
  });
3610
+
3611
+ describe('private setManageWebexCallingInWxcc (runtime toggle impl)', () => {
3612
+ beforeEach(() => {
3613
+ webex.internal.device = {
3614
+ userId: 'user-123',
3615
+ registered: false,
3616
+ register: jest.fn().mockResolvedValue(undefined),
3617
+ unregister: jest.fn().mockResolvedValue(undefined),
3618
+ };
3619
+ webex.internal.mercury = {
3620
+ connected: false,
3621
+ connect: jest.fn().mockResolvedValue(undefined),
3622
+ disconnect: jest.fn().mockResolvedValue(undefined),
3623
+ };
3624
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
3625
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
3626
+ webex.cc['agentConfig'] = {
3627
+ agentId: 'agent-123',
3628
+ deviceType: LoginOption.EXTENSION,
3629
+ } as Profile;
3630
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
3631
+ webex.cc.webCallingService.setLoginOption = jest.fn((loginOption: LoginOption) => {
3632
+ webex.cc.webCallingService.loginOption = loginOption;
3633
+ });
3634
+ });
3635
+
3636
+ afterEach(() => {
3637
+ jest.restoreAllMocks();
3638
+ });
3639
+
3640
+ it('should update config, task manager, and publish usersub when enabled', async () => {
3641
+ const publishSpy = jest
3642
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3643
+ .mockResolvedValue(undefined);
3644
+
3645
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
3646
+
3647
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
3648
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(true);
3649
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
3650
+ });
3651
+
3652
+ it('should publish false and update config when disabled after wxApp was enabled', async () => {
3653
+ webex.internal.device = {
3654
+ userId: 'user-123',
3655
+ url: 'https://wdm.example.com/devices/dev-1',
3656
+ };
3657
+ webex.request = jest.fn().mockResolvedValue({body: {}});
3658
+
3659
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
3660
+
3661
+ const publishSpy = jest
3662
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3663
+ .mockResolvedValue(undefined);
3664
+
3665
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
3666
+
3667
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3668
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
3669
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
3670
+ });
3671
+
3672
+ it('should skip usersub publish when disabling without prior wxApp publish', async () => {
3673
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3674
+ const publishSpy = jest
3675
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3676
+ .mockResolvedValue(undefined);
3677
+
3678
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
3679
+
3680
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3681
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
3682
+ expect(publishSpy).not.toHaveBeenCalled();
3683
+ });
3684
+
3685
+ it('should throw when enabling before login', async () => {
3686
+ webex.internal.device = undefined;
3687
+
3688
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3689
+ 'Cannot publish answer-calls-on-wxcc: user is not logged in'
3690
+ );
3691
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3692
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
3693
+ });
3694
+
3695
+ it('should throw when enabling on BROWSER (Desktop) login', async () => {
3696
+ webex.cc['agentConfig'] = {deviceType: LoginOption.BROWSER} as Profile;
3697
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
3698
+ const publishSpy = jest
3699
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3700
+ .mockResolvedValue(undefined);
3701
+
3702
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3703
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
3704
+ );
3705
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3706
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
3707
+ expect(publishSpy).not.toHaveBeenCalled();
3708
+ });
3709
+
3710
+ it('should throw when enabling before station login', async () => {
3711
+ webex.cc['agentConfig'] = undefined as unknown as Profile;
3712
+ webex.cc.webCallingService.loginOption = undefined as unknown as LoginOption;
3713
+ const publishSpy = jest
3714
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3715
+ .mockResolvedValue(undefined);
3716
+
3717
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3718
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
3719
+ );
3720
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3721
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
3722
+ expect(publishSpy).not.toHaveBeenCalled();
3723
+ });
3724
+
3725
+ it('should throw when webCallingService is BROWSER even if agentConfig deviceType is stale', async () => {
3726
+ webex.cc['agentConfig'] = {deviceType: LoginOption.EXTENSION} as Profile;
3727
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
3728
+ const publishSpy = jest
3729
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3730
+ .mockResolvedValue(undefined);
3731
+
3732
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3733
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
3734
+ );
3735
+ expect(publishSpy).not.toHaveBeenCalled();
3736
+ });
3737
+
3738
+ it('should publish usersub when enabling on AGENT_DN login', async () => {
3739
+ webex.cc['agentConfig'] = {
3740
+ agentId: 'agent-123',
3741
+ deviceType: LoginOption.AGENT_DN,
3742
+ } as Profile;
3743
+ const publishSpy = jest
3744
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3745
+ .mockResolvedValue(undefined);
3746
+
3747
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
3748
+
3749
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
3750
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
3751
+ });
3752
+
3753
+ it('should no-op usersub publish when disabling before login', async () => {
3754
+ webex.internal.device = undefined;
3755
+ const publishSpy = jest
3756
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3757
+ .mockResolvedValue(undefined);
3758
+
3759
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
3760
+
3761
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3762
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
3763
+ expect(publishSpy).not.toHaveBeenCalled();
3764
+ });
3765
+
3766
+ it('should subscribe to wxApp Mercury mute sync when enabled', async () => {
3767
+ webex.cc['agentConfig'] = {agentId: 'agent-123'} as Profile;
3768
+ webex.internal.device = {
3769
+ ...webex.internal.device,
3770
+ registered: true,
3771
+ register: jest.fn().mockResolvedValue(undefined),
3772
+ };
3773
+ webex.internal.mercury.connected = true;
3774
+ webex.internal.mercury.connect = jest.fn().mockResolvedValue(undefined);
3775
+ jest
3776
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3777
+ .mockResolvedValue(undefined);
3778
+ const subscribeSpy = jest
3779
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
3780
+ .mockImplementation(() => {});
3781
+
3782
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
3783
+
3784
+ expect(subscribeSpy).toHaveBeenCalledWith('agent-123', expect.any(Function));
3785
+ });
3786
+
3787
+ it('should unsubscribe from wxApp Mercury mute sync when disabled', async () => {
3788
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3789
+ jest
3790
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3791
+ .mockResolvedValue(undefined);
3792
+ const unsubscribeSpy = jest
3793
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe')
3794
+ .mockImplementation(() => {});
3795
+
3796
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
3797
+
3798
+ expect(unsubscribeSpy).toHaveBeenCalled();
3799
+ });
3800
+
3801
+ it('should register device and connect mercury when enabling wxApp sync', async () => {
3802
+ webex.cc['agentConfig'] = {agentId: 'agent-123'} as Profile;
3803
+ webex.internal.device = {
3804
+ userId: 'user-123',
3805
+ registered: false,
3806
+ register: jest.fn().mockResolvedValue(undefined),
3807
+ };
3808
+ webex.internal.mercury = {
3809
+ connected: false,
3810
+ connect: jest.fn().mockResolvedValue(undefined),
3811
+ };
3812
+ jest
3813
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3814
+ .mockResolvedValue(undefined);
3815
+ const subscribeSpy = jest
3816
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
3817
+ .mockImplementation(() => {});
3818
+
3819
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
3820
+
3821
+ expect(webex.internal.device.register).toHaveBeenCalled();
3822
+ expect(webex.internal.mercury.connect).toHaveBeenCalled();
3823
+ expect(subscribeSpy).toHaveBeenCalledWith('agent-123', expect.any(Function));
3824
+ });
3825
+
3826
+ it('should rollback config and task manager when usersub publish fails', async () => {
3827
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
3828
+ jest
3829
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3830
+ .mockRejectedValue(new Error('usersub failed'));
3831
+
3832
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow('usersub failed');
3833
+
3834
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3835
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(true);
3836
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenLastCalledWith(false);
3837
+ });
3838
+
3839
+ it('should rollback config and release Mercury resources when Mercury connect fails', async () => {
3840
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
3841
+ const publishSpy = jest
3842
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3843
+ .mockResolvedValue(undefined);
3844
+ jest
3845
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
3846
+ .mockRejectedValue(new Error('mercury connect failed'));
3847
+ const releaseSpy = jest
3848
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
3849
+ .mockResolvedValue(undefined);
3850
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
3851
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
3852
+
3853
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3854
+ 'mercury connect failed'
3855
+ );
3856
+
3857
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3858
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenLastCalledWith(false);
3859
+ expect(publishSpy).not.toHaveBeenCalled();
3860
+ expect(teardownSpy).not.toHaveBeenCalled();
3861
+ expect(unsubscribeSpy).toHaveBeenCalled();
3862
+ expect(releaseSpy).toHaveBeenCalled();
3863
+ });
3864
+
3865
+ it('should schedule compensating false retry when rollback publish fails after enable error', async () => {
3866
+ jest.useFakeTimers();
3867
+ try {
3868
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3869
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
3870
+ webex.cc.agentConfig = {
3871
+ agentId: 'agent-123',
3872
+ deviceType: LoginOption.EXTENSION,
3873
+ };
3874
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
3875
+
3876
+ const publishSpy = jest
3877
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3878
+ .mockResolvedValueOnce(undefined)
3879
+ .mockRejectedValueOnce(new Error('compensating false failed'))
3880
+ .mockResolvedValue(undefined);
3881
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
3882
+ jest
3883
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
3884
+ .mockResolvedValue(undefined);
3885
+ jest
3886
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
3887
+ .mockResolvedValue(undefined);
3888
+ jest
3889
+ .spyOn(mockTaskManager, 'syncWxAppMuteFromCallDetailsForAllTasks')
3890
+ .mockImplementation(() => {
3891
+ throw new Error('sync failed');
3892
+ });
3893
+ const scheduleRetrySpy = jest.spyOn(
3894
+ webex.cc as any,
3895
+ 'scheduleCompensatingFalsePublishRetry'
3896
+ );
3897
+
3898
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3899
+ 'sync failed'
3900
+ );
3901
+
3902
+ expect(scheduleRetrySpy).toHaveBeenCalled();
3903
+ expect(publishSpy).toHaveBeenCalledTimes(2);
3904
+ expect(publishSpy).toHaveBeenNthCalledWith(1, true, {userId: 'user-123'});
3905
+ expect(publishSpy).toHaveBeenNthCalledWith(2, false, {userId: 'user-123'});
3906
+
3907
+ jest.advanceTimersByTime(30_000);
3908
+ await publishSpy.mock.results[2]?.value;
3909
+
3910
+ expect(publishSpy).toHaveBeenCalledTimes(3);
3911
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123'});
3912
+ } finally {
3913
+ jest.useRealTimers();
3914
+ }
3915
+ });
3916
+ });
2643
3917
  });