@webex/contact-center 3.12.0-next.12 → 3.12.0-next.121

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 (307) hide show
  1. package/.sdd/manifest.json +884 -0
  2. package/AGENTS.md +94 -0
  3. package/ai-docs/ARCHITECTURE.md +168 -0
  4. package/ai-docs/CONTRACTS.md +51 -0
  5. package/ai-docs/GETTING_STARTED.md +168 -0
  6. package/ai-docs/GLOSSARY.md +43 -0
  7. package/ai-docs/README.md +138 -0
  8. package/ai-docs/REVIEW_CHECKLIST.md +41 -0
  9. package/ai-docs/RULES.md +444 -0
  10. package/ai-docs/SECURITY.md +52 -0
  11. package/ai-docs/SERVICE_STATE.md +48 -0
  12. package/ai-docs/SPEC_INDEX.md +65 -0
  13. package/ai-docs/adr/0001-spec-source-policy.md +55 -0
  14. package/ai-docs/adr/README.md +8 -0
  15. package/ai-docs/adr/_adr-template.md +31 -0
  16. package/ai-docs/contact-center-spec.md +359 -0
  17. package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
  18. package/ai-docs/features/generated-spec-conformance-fidelity-remediation/spec/feature-spec.md +117 -0
  19. package/ai-docs/features/residual-warning-coverage-completion/spec/feature-spec.md +203 -0
  20. package/ai-docs/features/validator-code-fidelity-drift-fix/spec/feature-spec.md +315 -0
  21. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  22. package/ai-docs/patterns/testing-patterns.md +480 -0
  23. package/ai-docs/patterns/typescript-patterns.md +365 -0
  24. package/ai-docs/templates/README.md +102 -0
  25. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  26. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  27. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  28. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  29. package/ai-docs/templates/new-method/00-master.md +80 -0
  30. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  31. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  32. package/ai-docs/templates/new-method/03-tests.md +201 -0
  33. package/ai-docs/templates/new-method/04-validation.md +141 -0
  34. package/ai-docs/templates/new-service/00-master.md +109 -0
  35. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  36. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  37. package/ai-docs/templates/new-service/03-integration.md +178 -0
  38. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  39. package/ai-docs/templates/new-service/05-validation.md +145 -0
  40. package/dist/cc.js +808 -40
  41. package/dist/cc.js.map +1 -1
  42. package/dist/config.js +13 -0
  43. package/dist/config.js.map +1 -1
  44. package/dist/constants.js +27 -3
  45. package/dist/constants.js.map +1 -1
  46. package/dist/index.js +20 -5
  47. package/dist/index.js.map +1 -1
  48. package/dist/metrics/MetricsManager.js +16 -0
  49. package/dist/metrics/MetricsManager.js.map +1 -1
  50. package/dist/metrics/behavioral-events.js +205 -0
  51. package/dist/metrics/behavioral-events.js.map +1 -1
  52. package/dist/metrics/constants.js +43 -4
  53. package/dist/metrics/constants.js.map +1 -1
  54. package/dist/services/AddressBook.js +18 -15
  55. package/dist/services/AddressBook.js.map +1 -1
  56. package/dist/services/AnswerCallOnWebexService.js +202 -0
  57. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  58. package/dist/services/ApiAiAssistant.js +153 -8
  59. package/dist/services/ApiAiAssistant.js.map +1 -1
  60. package/dist/services/EntryPoint.js +46 -68
  61. package/dist/services/EntryPoint.js.map +1 -1
  62. package/dist/services/Queue.js +27 -22
  63. package/dist/services/Queue.js.map +1 -1
  64. package/dist/services/UserPreference.js +427 -0
  65. package/dist/services/UserPreference.js.map +1 -0
  66. package/dist/services/WebexCrossClientService.js +222 -0
  67. package/dist/services/WebexCrossClientService.js.map +1 -0
  68. package/dist/services/WxAppTelephonyMercurySync.js +108 -0
  69. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  70. package/dist/services/config/Util.js +6 -3
  71. package/dist/services/config/Util.js.map +1 -1
  72. package/dist/services/config/constants.js +33 -8
  73. package/dist/services/config/constants.js.map +1 -1
  74. package/dist/services/config/types.js +53 -9
  75. package/dist/services/config/types.js.map +1 -1
  76. package/dist/services/core/Err.js.map +1 -1
  77. package/dist/services/core/Utils.js +107 -32
  78. package/dist/services/core/Utils.js.map +1 -1
  79. package/dist/services/core/WebexRequest.js +6 -2
  80. package/dist/services/core/WebexRequest.js.map +1 -1
  81. package/dist/services/core/aqm-reqs.js +28 -14
  82. package/dist/services/core/aqm-reqs.js.map +1 -1
  83. package/dist/services/core/types.js.map +1 -1
  84. package/dist/services/core/websocket/WebSocketManager.js +2 -1
  85. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  86. package/dist/services/core/websocket/types.js.map +1 -1
  87. package/dist/services/index.js +1 -1
  88. package/dist/services/index.js.map +1 -1
  89. package/dist/services/task/Task.js +754 -0
  90. package/dist/services/task/Task.js.map +1 -0
  91. package/dist/services/task/TaskFactory.js +49 -0
  92. package/dist/services/task/TaskFactory.js.map +1 -0
  93. package/dist/services/task/TaskManager.js +1053 -520
  94. package/dist/services/task/TaskManager.js.map +1 -1
  95. package/dist/services/task/TaskUtils.js +211 -27
  96. package/dist/services/task/TaskUtils.js.map +1 -1
  97. package/dist/services/task/WebexCallingUtils.js +70 -0
  98. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  99. package/dist/services/task/constants.js +12 -2
  100. package/dist/services/task/constants.js.map +1 -1
  101. package/dist/services/task/contact.js +29 -0
  102. package/dist/services/task/contact.js.map +1 -1
  103. package/dist/services/task/dialer.js +78 -0
  104. package/dist/services/task/dialer.js.map +1 -1
  105. package/dist/services/task/digital/Digital.js +78 -0
  106. package/dist/services/task/digital/Digital.js.map +1 -0
  107. package/dist/services/task/state-machine/TaskStateMachine.js +954 -0
  108. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  109. package/dist/services/task/state-machine/actions.js +572 -0
  110. package/dist/services/task/state-machine/actions.js.map +1 -0
  111. package/dist/services/task/state-machine/constants.js +161 -0
  112. package/dist/services/task/state-machine/constants.js.map +1 -0
  113. package/dist/services/task/state-machine/guards.js +379 -0
  114. package/dist/services/task/state-machine/guards.js.map +1 -0
  115. package/dist/services/task/state-machine/index.js +53 -0
  116. package/dist/services/task/state-machine/index.js.map +1 -0
  117. package/dist/services/task/state-machine/types.js +54 -0
  118. package/dist/services/task/state-machine/types.js.map +1 -0
  119. package/dist/services/task/state-machine/uiControlsComputer.js +703 -0
  120. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  121. package/dist/services/task/taskDataNormalizer.js +99 -0
  122. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  123. package/dist/services/task/types.js +228 -5
  124. package/dist/services/task/types.js.map +1 -1
  125. package/dist/services/task/voice/Voice.js +1447 -0
  126. package/dist/services/task/voice/Voice.js.map +1 -0
  127. package/dist/services/task/voice/WebRTC.js +152 -0
  128. package/dist/services/task/voice/WebRTC.js.map +1 -0
  129. package/dist/services/task/voice/wxAppVoiceMethods.js +354 -0
  130. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  131. package/dist/services/wxAppDiagnosticLogging.js +122 -0
  132. package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
  133. package/dist/services/wxAppTelephonyUtils.js +19 -0
  134. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  135. package/dist/types/cc.d.ts +172 -5
  136. package/dist/types/config.d.ts +13 -0
  137. package/dist/types/constants.d.ts +25 -1
  138. package/dist/types/index.d.ts +24 -7
  139. package/dist/types/metrics/MetricsManager.d.ts +6 -0
  140. package/dist/types/metrics/constants.d.ts +38 -1
  141. package/dist/types/services/AddressBook.d.ts +2 -1
  142. package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -0
  143. package/dist/types/services/ApiAiAssistant.d.ts +22 -4
  144. package/dist/types/services/EntryPoint.d.ts +7 -5
  145. package/dist/types/services/Queue.d.ts +5 -3
  146. package/dist/types/services/UserPreference.d.ts +118 -0
  147. package/dist/types/services/WebexCrossClientService.d.ts +32 -0
  148. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  149. package/dist/types/services/config/constants.d.ts +29 -5
  150. package/dist/types/services/config/types.d.ts +184 -10
  151. package/dist/types/services/core/Err.d.ts +6 -0
  152. package/dist/types/services/core/Utils.d.ts +33 -13
  153. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  154. package/dist/types/services/core/types.d.ts +2 -0
  155. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
  156. package/dist/types/services/core/websocket/types.d.ts +1 -1
  157. package/dist/types/services/index.d.ts +1 -1
  158. package/dist/types/services/task/Task.d.ts +175 -0
  159. package/dist/types/services/task/TaskFactory.d.ts +13 -0
  160. package/dist/types/services/task/TaskUtils.d.ts +48 -2
  161. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  162. package/dist/types/services/task/constants.d.ts +10 -0
  163. package/dist/types/services/task/contact.d.ts +4 -0
  164. package/dist/types/services/task/dialer.d.ts +30 -0
  165. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  166. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1356 -0
  167. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  168. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  169. package/dist/types/services/task/state-machine/guards.d.ts +103 -0
  170. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  171. package/dist/types/services/task/state-machine/types.d.ts +277 -0
  172. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  173. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  174. package/dist/types/services/task/types.d.ts +680 -66
  175. package/dist/types/services/task/voice/Voice.d.ts +227 -0
  176. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  177. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +55 -0
  178. package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
  179. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  180. package/dist/types/types.d.ts +149 -8
  181. package/dist/types/utils/PageCache.d.ts +20 -3
  182. package/dist/types/webex.d.ts +1 -0
  183. package/dist/types.js +125 -4
  184. package/dist/types.js.map +1 -1
  185. package/dist/utils/PageCache.js +19 -5
  186. package/dist/utils/PageCache.js.map +1 -1
  187. package/dist/webex.js +14 -2
  188. package/dist/webex.js.map +1 -1
  189. package/package.json +16 -12
  190. package/src/cc.ts +986 -42
  191. package/src/config.ts +13 -0
  192. package/src/constants.ts +25 -1
  193. package/src/index.ts +25 -5
  194. package/src/metrics/MetricsManager.ts +16 -0
  195. package/src/metrics/ai-docs/AGENTS.md +350 -0
  196. package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
  197. package/src/metrics/ai-docs/metrics-spec.md +922 -0
  198. package/src/metrics/behavioral-events.ts +212 -0
  199. package/src/metrics/constants.ts +47 -4
  200. package/src/services/AddressBook.ts +17 -6
  201. package/src/services/AnswerCallOnWebexService.ts +239 -0
  202. package/src/services/ApiAiAssistant.ts +203 -8
  203. package/src/services/EntryPoint.ts +59 -60
  204. package/src/services/Queue.ts +29 -12
  205. package/src/services/UserPreference.ts +509 -0
  206. package/src/services/WebexCrossClientService.ts +293 -0
  207. package/src/services/WxAppTelephonyMercurySync.ts +142 -0
  208. package/src/services/agent/ai-docs/AGENTS.md +240 -0
  209. package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
  210. package/src/services/agent/ai-docs/agent-spec.md +505 -0
  211. package/src/services/ai-docs/AGENTS.md +386 -0
  212. package/src/services/ai-docs/services-spec.md +497 -0
  213. package/src/services/config/Util.ts +6 -3
  214. package/src/services/config/ai-docs/AGENTS.md +255 -0
  215. package/src/services/config/ai-docs/ARCHITECTURE.md +426 -0
  216. package/src/services/config/ai-docs/config-spec.md +675 -0
  217. package/src/services/config/constants.ts +35 -7
  218. package/src/services/config/types.ts +186 -11
  219. package/src/services/core/Err.ts +3 -0
  220. package/src/services/core/Utils.ts +123 -37
  221. package/src/services/core/WebexRequest.ts +3 -1
  222. package/src/services/core/ai-docs/AGENTS.md +381 -0
  223. package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
  224. package/src/services/core/ai-docs/core-spec.md +787 -0
  225. package/src/services/core/aqm-reqs.ts +30 -15
  226. package/src/services/core/types.ts +2 -0
  227. package/src/services/core/websocket/WebSocketManager.ts +2 -0
  228. package/src/services/core/websocket/types.ts +1 -1
  229. package/src/services/index.ts +1 -1
  230. package/src/services/task/Task.ts +908 -0
  231. package/src/services/task/TaskFactory.ts +60 -0
  232. package/src/services/task/TaskManager.ts +1239 -608
  233. package/src/services/task/TaskUtils.ts +303 -25
  234. package/src/services/task/WebexCallingUtils.ts +136 -0
  235. package/src/services/task/ai-docs/AGENTS.md +457 -0
  236. package/src/services/task/ai-docs/ARCHITECTURE.md +595 -0
  237. package/src/services/task/ai-docs/task-spec.md +1470 -0
  238. package/src/services/task/constants.ts +10 -0
  239. package/src/services/task/contact.ts +30 -0
  240. package/src/services/task/dialer.ts +80 -0
  241. package/src/services/task/digital/Digital.ts +97 -0
  242. package/src/services/task/state-machine/TaskStateMachine.ts +1293 -0
  243. package/src/services/task/state-machine/actions.ts +741 -0
  244. package/src/services/task/state-machine/ai-docs/AGENTS.md +460 -0
  245. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1145 -0
  246. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2207 -0
  247. package/src/services/task/state-machine/constants.ts +172 -0
  248. package/src/services/task/state-machine/guards.ts +442 -0
  249. package/src/services/task/state-machine/index.ts +28 -0
  250. package/src/services/task/state-machine/types.ts +258 -0
  251. package/src/services/task/state-machine/uiControlsComputer.ts +1135 -0
  252. package/src/services/task/taskDataNormalizer.ts +137 -0
  253. package/src/services/task/types.ts +819 -70
  254. package/src/services/task/voice/Voice.ts +1809 -0
  255. package/src/services/task/voice/WebRTC.ts +191 -0
  256. package/src/services/task/voice/wxAppVoiceMethods.ts +516 -0
  257. package/src/services/wxAppDiagnosticLogging.ts +174 -0
  258. package/src/services/wxAppTelephonyUtils.ts +14 -0
  259. package/src/types.ts +199 -11
  260. package/src/utils/AGENTS.md +289 -0
  261. package/src/utils/PageCache.ts +38 -5
  262. package/src/utils/ai-docs/utils-spec.md +391 -0
  263. package/src/webex.js +2 -0
  264. package/test/unit/spec/cc.ts +1947 -124
  265. package/test/unit/spec/logger-proxy.ts +70 -0
  266. package/test/unit/spec/metrics/MetricsManager.ts +25 -0
  267. package/test/unit/spec/metrics/behavioral-events.ts +60 -0
  268. package/test/unit/spec/services/AddressBook.ts +37 -6
  269. package/test/unit/spec/services/AnswerCallOnWebexService.ts +264 -0
  270. package/test/unit/spec/services/ApiAiAssistant.ts +178 -20
  271. package/test/unit/spec/services/EntryPoint.ts +87 -40
  272. package/test/unit/spec/services/Queue.ts +123 -12
  273. package/test/unit/spec/services/UserPreference.ts +401 -0
  274. package/test/unit/spec/services/WebCallingService.ts +7 -1
  275. package/test/unit/spec/services/WebexCrossClientService.ts +467 -0
  276. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +136 -0
  277. package/test/unit/spec/services/config/Util.ts +85 -0
  278. package/test/unit/spec/services/config/index.ts +30 -30
  279. package/test/unit/spec/services/core/Utils.ts +425 -8
  280. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  281. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  282. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +66 -40
  283. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  284. package/test/unit/spec/services/task/Task.ts +677 -0
  285. package/test/unit/spec/services/task/TaskFactory.ts +99 -0
  286. package/test/unit/spec/services/task/TaskManager.ts +1972 -1549
  287. package/test/unit/spec/services/task/TaskUtils.ts +235 -0
  288. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  289. package/test/unit/spec/services/task/contact.ts +33 -0
  290. package/test/unit/spec/services/task/dialer.ts +190 -0
  291. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  292. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +3459 -0
  293. package/test/unit/spec/services/task/state-machine/guards.ts +839 -0
  294. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  295. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +3101 -0
  296. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  297. package/test/unit/spec/services/task/voice/Voice.ts +1903 -0
  298. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  299. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +603 -0
  300. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
  301. package/umd/contact-center.min.js +2 -2
  302. package/umd/contact-center.min.js.map +1 -1
  303. package/dist/services/task/index.js +0 -1525
  304. package/dist/services/task/index.js.map +0 -1
  305. package/dist/types/services/task/index.d.ts +0 -650
  306. package/src/services/task/index.ts +0 -1801
  307. package/test/unit/spec/services/task/index.ts +0 -2184
@@ -59,6 +59,7 @@ global.URL.createObjectURL = jest.fn(() => 'blob:http://localhost:3000/12345');
59
59
 
60
60
  describe('webex.cc', () => {
61
61
  let webex;
62
+ let mockApiAIAssistant;
62
63
  let mockContact;
63
64
  let mockTaskManager;
64
65
  let mockMetricsManager;
@@ -109,6 +110,14 @@ describe('webex.cc', () => {
109
110
  cancelCtq: jest.fn(),
110
111
  };
111
112
 
113
+ mockApiAIAssistant = {
114
+ sendEvent: jest.fn(),
115
+ getRealTimeAssistance: jest.fn(),
116
+ fetchHistoricTranscripts: jest.fn(),
117
+ setAIFeatureFlags: jest.fn(),
118
+ setAgentId: jest.fn(),
119
+ };
120
+
112
121
  // Mock Services instance
113
122
  const mockServicesInstance = {
114
123
  agent: {
@@ -139,6 +148,8 @@ describe('webex.cc', () => {
139
148
  dialer: {
140
149
  startOutdial: jest.fn(),
141
150
  acceptPreviewContact: jest.fn(),
151
+ skipPreviewContact: jest.fn(),
152
+ removePreviewContact: jest.fn(),
142
153
  },
143
154
  apiAIAssistant: {
144
155
  sendEvent: jest.fn(),
@@ -147,21 +158,24 @@ describe('webex.cc', () => {
147
158
  };
148
159
 
149
160
  mockTaskManager = {
161
+ apiAIAssistant: mockApiAIAssistant,
150
162
  contact: mockContact,
151
163
  call: undefined,
152
164
  taskCollection: {},
153
165
  webCallingService: undefined,
154
166
  webSocketManager: mockWebSocketManager,
155
167
  task: undefined,
168
+ setConfigFlags: jest.fn(),
156
169
  setWrapupData: jest.fn(),
157
170
  setAgentId: jest.fn(),
158
171
  setWebRtcEnabled: jest.fn(),
159
- handleRealtimeTranscriptEvent: jest.fn(),
160
- setApiAIAssistant: jest.fn(),
172
+ setAnswerCallOnWebexService: jest.fn(),
161
173
  registerIncomingCallEvent: jest.fn(),
162
174
  registerTaskListeners: jest.fn(),
163
175
  getTask: jest.fn(),
164
176
  getActiveTasks: jest.fn(),
177
+ applyEnableWxBetterTogether: jest.fn(),
178
+ syncWxAppMuteFromCallDetailsForAllTasks: jest.fn(),
165
179
  on: jest.fn(),
166
180
  off: jest.fn(),
167
181
  emit: jest.fn(),
@@ -200,6 +214,35 @@ describe('webex.cc', () => {
200
214
  webex.emit('ready');
201
215
  });
202
216
 
217
+ it('should throw when WebRTC registration is disabled without multi-login', () => {
218
+ const invalidWebex = MockWebex({
219
+ children: {
220
+ mercury: Mercury,
221
+ },
222
+ logger: {
223
+ log: jest.fn(),
224
+ error: jest.fn(),
225
+ info: jest.fn(),
226
+ },
227
+ credentials: {
228
+ getOrgId: jest.fn(() => 'mockOrgId'),
229
+ },
230
+ config: {
231
+ ...config,
232
+ cc: {
233
+ ...config.cc,
234
+ allowMultiLogin: false,
235
+ disableWebRTCRegistration: true,
236
+ },
237
+ },
238
+ once: jest.fn((event, callback) => callback()),
239
+ }) as unknown as WebexSDK;
240
+
241
+ expect(() => new ContactCenter({parent: invalidWebex})).toThrow(
242
+ 'Invalid Contact Center configuration: disableWebRTCRegistration cannot be true when allowMultiLogin is false. Enable allowMultiLogin or allow WebRTC registration so an SDK instance can receive Mobius/WebRTC task events.'
243
+ );
244
+ });
245
+
203
246
  describe('cc.getDeviceId', () => {
204
247
  it('should return dialNumber when loginOption is EXTENSION', () => {
205
248
  const loginOption = LoginOption.EXTENSION;
@@ -267,10 +310,13 @@ describe('webex.cc', () => {
267
310
  isAgentAvailableAfterOutdial: false,
268
311
  isCampaignManagementEnabled: false,
269
312
  outDialEp: '',
270
- isEndCallEnabled: false,
313
+ isEndTaskEnabled: false,
271
314
  isEndConsultEnabled: false,
272
315
  agentDbId: '',
273
316
  allowConsultToQueue: false,
317
+ accessQueue: 'SPECIFIC',
318
+ accessEntryPoint: 'ALL',
319
+ accessBuddyTeam: 'ALL',
274
320
  agentPersonalStatsEnabled: false,
275
321
  isTimeoutDesktopInactivityEnabled: false,
276
322
  webRtcEnabled: true,
@@ -300,7 +346,7 @@ describe('webex.cc', () => {
300
346
  const result = await webex.cc.register();
301
347
 
302
348
  // Verify logging calls
303
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting CC SDK registration', {
349
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting CC SDK registration', {
304
350
  module: CC_FILE,
305
351
  method: 'register',
306
352
  });
@@ -334,6 +380,10 @@ describe('webex.cc', () => {
334
380
  TASK_EVENTS.TASK_HYDRATE,
335
381
  expect.any(Function)
336
382
  );
383
+ expect(mockTaskManager.on).toHaveBeenCalledWith(
384
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
385
+ expect.any(Function)
386
+ );
337
387
  expect(mockWebSocketManager.on).toHaveBeenCalledWith('message', expect.any(Function));
338
388
  expect(webex.cc.services.rtdWebSocketManager.initWebSocket).toHaveBeenCalledWith({
339
389
  body: {
@@ -354,6 +404,20 @@ describe('webex.cc', () => {
354
404
  module: CC_FILE,
355
405
  method: 'connectWebsocket',
356
406
  });
407
+ expect(mockTaskManager.setConfigFlags).toHaveBeenCalledWith({
408
+ isEndTaskEnabled: mockAgentProfile.isEndTaskEnabled,
409
+ isEndConsultEnabled: mockAgentProfile.isEndConsultEnabled,
410
+ webRtcEnabled: mockAgentProfile.webRtcEnabled,
411
+ autoWrapup: mockAgentProfile.wrapUpData.wrapUpProps.autoWrapup ?? false,
412
+ aiFeature: mockAgentProfile.aiFeature,
413
+ consultTransfer: {
414
+ allowConsultToQueue: mockAgentProfile.allowConsultToQueue,
415
+ accessQueue: mockAgentProfile.accessQueue,
416
+ accessEntryPoint: mockAgentProfile.accessEntryPoint,
417
+ accessBuddyTeam: mockAgentProfile.accessBuddyTeam,
418
+ },
419
+ enableWxBetterTogether: false,
420
+ });
357
421
  expect(reloadSpy).toHaveBeenCalled();
358
422
  expect(result).toEqual(mockAgentProfile);
359
423
  expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
@@ -413,7 +477,7 @@ describe('webex.cc', () => {
413
477
 
414
478
  await expect(webex.cc.register()).rejects.toThrow('Error while performing register');
415
479
 
416
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting CC SDK registration', {
480
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting CC SDK registration', {
417
481
  module: CC_FILE,
418
482
  method: 'register',
419
483
  });
@@ -482,6 +546,10 @@ describe('webex.cc', () => {
482
546
  TASK_EVENTS.TASK_HYDRATE,
483
547
  expect.any(Function)
484
548
  );
549
+ expect(mockTaskManager.on).toHaveBeenCalledWith(
550
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
551
+ expect.any(Function)
552
+ );
485
553
  expect(mockWebSocketManager.on).toHaveBeenCalledWith('message', expect.any(Function));
486
554
 
487
555
  expect(configSpy).toHaveBeenCalled();
@@ -493,6 +561,36 @@ describe('webex.cc', () => {
493
561
  expect(result).toEqual(mockAgentProfile);
494
562
  });
495
563
 
564
+ it('should skip mercury connection when disableWebRTCRegistration is enabled', async () => {
565
+ webex.cc.$config = {
566
+ ...webex.cc.$config,
567
+ allowAutomatedRelogin: false,
568
+ disableWebRTCRegistration: true,
569
+ };
570
+ mockAgentProfile.webRtcEnabled = true;
571
+ const mercurySpy = jest.spyOn(webex.internal.mercury, 'connect');
572
+ const connectWebsocketSpy = jest.spyOn(webex.cc, 'connectWebsocket');
573
+ const setupEventListenersSpy = jest.spyOn(webex.cc, 'setupEventListeners');
574
+ jest.spyOn(webex.cc.services.config, 'getAgentConfig').mockResolvedValue(mockAgentProfile);
575
+ mockWebSocketManager.initWebSocket.mockResolvedValue({
576
+ agentId: 'agent123',
577
+ });
578
+
579
+ const result = await webex.cc.register();
580
+
581
+ expect(connectWebsocketSpy).toHaveBeenCalled();
582
+ expect(setupEventListenersSpy).toHaveBeenCalled();
583
+ expect(mercurySpy).not.toHaveBeenCalled();
584
+ expect(LoggerProxy.info).toHaveBeenCalledWith(
585
+ 'Skipping Mobius registration because disableWebRTCRegistration is enabled',
586
+ {
587
+ module: CC_FILE,
588
+ method: 'connectWebsocket',
589
+ }
590
+ );
591
+ expect(result).toEqual(mockAgentProfile);
592
+ });
593
+
496
594
  it('should not attempt for mercury connection when webrtc is disabled', async () => {
497
595
  mockAgentProfile.webRtcEnabled = false;
498
596
  mockAgentProfile.aiFeature = {realtimeTranscripts: {enable: false}} as any;
@@ -631,7 +729,7 @@ describe('webex.cc', () => {
631
729
  expect(registerWebCallingLineSpy).toHaveBeenCalled();
632
730
  expect(stationLoginMock).toHaveBeenCalledWith({
633
731
  data: {
634
- dialNumber: 'agentId',
732
+ dialNumber: `${WEB_RTC_PREFIX}agentId`,
635
733
  teamId: 'teamId',
636
734
  deviceType: LoginOption.BROWSER,
637
735
  isExtension: false,
@@ -649,6 +747,7 @@ describe('webex.cc', () => {
649
747
  METRIC_EVENT_NAMES.STATION_LOGIN_FAILED,
650
748
  ]);
651
749
  expect(result).toEqual(responseMock);
750
+ expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.BROWSER);
652
751
 
653
752
  const onSpy = jest.spyOn(mockTaskManager, 'on');
654
753
  const emitSpy = jest.spyOn(webex.cc, 'trigger');
@@ -661,10 +760,8 @@ describe('webex.cc', () => {
661
760
 
662
761
  expect(emitSpy).toHaveBeenCalledTimes(1);
663
762
  expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, mockTask);
664
- // Verify message event listener
665
- const messageCallback = mockWebSocketManager.on.mock.calls.find(
666
- (call) => call[0] === 'message'
667
- )[1];
763
+ // Verify websocket message handling
764
+ const messageCallback = webex.cc['handleWebsocketMessage'];
668
765
  const agentStateChangeEventData = {
669
766
  type: CC_EVENTS.AGENT_STATE_CHANGE,
670
767
  data: {some: 'data'},
@@ -692,6 +789,59 @@ describe('webex.cc', () => {
692
789
  );
693
790
  });
694
791
 
792
+ it('should skip web calling line registration when disableWebRTCRegistration is enabled', async () => {
793
+ const options = {
794
+ teamId: 'teamId',
795
+ loginOption: LoginOption.BROWSER,
796
+ };
797
+
798
+ webex.cc.$config = {
799
+ ...webex.cc.$config,
800
+ disableWebRTCRegistration: true,
801
+ };
802
+ webex.cc.agentConfig = {
803
+ agentId: 'agentId',
804
+ webRtcEnabled: true,
805
+ loginVoiceOptions: ['BROWSER', 'EXTENSION', 'AGENT_DN'],
806
+ };
807
+
808
+ const registerWebCallingLineSpy = jest.spyOn(
809
+ webex.cc.webCallingService,
810
+ 'registerWebCallingLine'
811
+ );
812
+
813
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
814
+ data: {
815
+ loginOption: LoginOption.BROWSER,
816
+ agentId: 'agentId',
817
+ teamId: 'teamId',
818
+ siteId: 'siteId',
819
+ roles: [AGENT],
820
+ channelsMap: {
821
+ chat: [],
822
+ email: [],
823
+ social: [],
824
+ telephony: [],
825
+ },
826
+ },
827
+ trackingId: 'notifs_52628',
828
+ orgId: 'orgId',
829
+ type: 'StationLoginSuccess',
830
+ eventType: 'STATION_LOGIN',
831
+ } as unknown as StationLoginSuccess);
832
+
833
+ await webex.cc.stationLogin(options);
834
+
835
+ expect(registerWebCallingLineSpy).not.toHaveBeenCalled();
836
+ expect(LoggerProxy.info).toHaveBeenCalledWith(
837
+ 'Skipping web calling line registration because disableWebRTCRegistration is enabled',
838
+ {
839
+ module: CC_FILE,
840
+ method: 'stationLogin',
841
+ }
842
+ );
843
+ });
844
+
695
845
  it('should not attempt mobius registration for LoginOption.BROWSER if webrtc is disabled', async () => {
696
846
  const options = {
697
847
  teamId: 'teamId',
@@ -739,7 +889,7 @@ describe('webex.cc', () => {
739
889
  expect(registerWebCallingLineSpy).not.toHaveBeenCalled();
740
890
  expect(stationLoginSpy).toHaveBeenCalledWith({
741
891
  data: {
742
- dialNumber: 'agentId',
892
+ dialNumber: `${WEB_RTC_PREFIX}agentId`,
743
893
  teamId: 'teamId',
744
894
  deviceType: LoginOption.BROWSER,
745
895
  isExtension: false,
@@ -814,10 +964,13 @@ describe('webex.cc', () => {
814
964
  const result = await webex.cc.stationLogin(options);
815
965
 
816
966
  // Verify logging calls
817
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station login', {
818
- module: CC_FILE,
819
- method: 'stationLogin',
820
- });
967
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
968
+ `Starting agent station login | loginOption: ${options.loginOption} teamId: ${options.teamId}`,
969
+ {
970
+ module: CC_FILE,
971
+ method: 'stationLogin',
972
+ }
973
+ );
821
974
  expect(LoggerProxy.log).toHaveBeenCalledWith(
822
975
  `Agent station login completed successfully agentId: ${mockData.data.agentId} loginOption: ${mockData.data.loginOption} teamId: ${mockData.data.teamId}`,
823
976
  {
@@ -868,132 +1021,978 @@ describe('webex.cc', () => {
868
1021
 
869
1022
  await expect(webex.cc.stationLogin(options)).rejects.toThrow(error.details.data.reason);
870
1023
 
871
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station login', {
872
- module: CC_FILE,
873
- method: 'stationLogin',
874
- });
1024
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
1025
+ `Starting agent station login | loginOption: ${options.loginOption} teamId: ${options.teamId}`,
1026
+ {
1027
+ module: CC_FILE,
1028
+ method: 'stationLogin',
1029
+ }
1030
+ );
875
1031
  expect(LoggerProxy.error).toHaveBeenCalledWith(
876
1032
  `stationLogin failed with reason: ${error.details.data.reason}`,
877
1033
  {module: CC_FILE, method: 'stationLogin', trackingId: error.details.trackingId}
878
1034
  );
879
1035
  });
880
- });
881
1036
 
882
- describe('stationLogout', () => {
883
- it('should logout successfully', async () => {
884
- const data = {logoutReason: 'Logout reason'};
885
- const response = {};
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;
886
1053
 
887
- const stationLogoutMock = jest
888
- .spyOn(webex.cc.services.agent, 'logout')
889
- .mockResolvedValue({} as StationLogoutResponse);
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);
890
1061
 
891
- const result = await webex.cc.stationLogout(data);
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);
892
1070
 
893
- // Verify logging calls
894
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
895
- module: CC_FILE,
896
- method: 'stationLogout',
897
- });
898
- expect(LoggerProxy.log).toHaveBeenCalledWith('Agent station logout completed successfully', {
899
- module: CC_FILE,
900
- method: 'stationLogout',
1071
+ await webex.cc.stationLogin({
1072
+ teamId: 'teamId',
1073
+ loginOption: LoginOption.EXTENSION,
1074
+ dialNumber: '1001',
901
1075
  });
902
1076
 
903
- expect(stationLogoutMock).toHaveBeenCalledWith({data: data});
904
- // TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
905
- // expect(mockTaskManager.unregisterIncomingCallEvent).toHaveBeenCalledWith();
906
- // expect(mockTaskManager.off).toHaveBeenCalledWith(
907
- // TASK_EVENTS.TASK_INCOMING,
908
- // expect.any(Function)
909
- // );
910
- // expect(mockTaskManager.off).toHaveBeenCalledWith(
911
- // TASK_EVENTS.TASK_HYDRATE,
912
- // expect.any(Function)
913
- // );
914
- // expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
915
- expect(result).toEqual(response);
916
- expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
917
- METRIC_EVENT_NAMES.STATION_LOGOUT_SUCCESS,
918
- METRIC_EVENT_NAMES.STATION_LOGOUT_FAILED,
919
- ]);
1077
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
1078
+ expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
920
1079
  });
921
1080
 
922
- it('should handle error during stationLogout', async () => {
923
- const data = {logoutReason: 'Logout reason'};
924
- const error = {
925
- details: {
926
- trackingId: '1234',
927
- data: {
928
- reason: 'Error while performing station logout',
929
- },
930
- },
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'],
931
1088
  };
1089
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1090
+ mockTaskManager.applyEnableWxBetterTogether(true);
932
1091
 
933
- jest.spyOn(webex.cc.services.agent, 'logout').mockRejectedValue(error);
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');
934
1096
 
935
- await expect(webex.cc.stationLogout(data)).rejects.toThrow(error.details.data.reason);
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);
936
1105
 
937
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
938
- module: CC_FILE,
939
- method: 'stationLogout',
1106
+ await webex.cc.stationLogin({
1107
+ teamId: 'teamId',
1108
+ loginOption: LoginOption.EXTENSION,
1109
+ dialNumber: '1001',
940
1110
  });
941
- expect(LoggerProxy.error).toHaveBeenCalledWith(
942
- `stationLogout failed with reason: ${error.details.data.reason}`,
943
- {module: CC_FILE, method: 'stationLogout', trackingId: error.details.trackingId}
944
- );
1111
+
1112
+ expect(unsubscribeSpy).toHaveBeenCalled();
1113
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1114
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
945
1115
  });
946
- });
947
1116
 
948
- describe('setAgentStatus', () => {
949
- it('should set agent status successfully when status is Available', async () => {
950
- const expectedPayload = {
951
- state: 'Available',
952
- auxCodeId: '0',
953
- agentId: '123',
954
- lastStateChangeReason: 'Agent is available',
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'],
955
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');
956
1140
 
957
- const setAgentStatusMock = jest
958
- .spyOn(webex.cc.services.agent, 'stateChange')
959
- .mockResolvedValue({data: expectedPayload});
960
-
961
- const result = await webex.cc.setAgentState(expectedPayload);
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);
962
1149
 
963
- // Verify logging calls
964
- expect(LoggerProxy.info).toHaveBeenCalledWith('Setting agent state', {
965
- module: CC_FILE,
966
- method: 'setAgentState',
1150
+ await webex.cc.stationLogin({
1151
+ teamId: 'teamId',
1152
+ loginOption: LoginOption.EXTENSION,
1153
+ dialNumber: '1001',
967
1154
  });
968
- expect(LoggerProxy.log).toHaveBeenCalledWith(
969
- `Agent state changed successfully to auxCodeId: ${expectedPayload.auxCodeId}`,
970
- {
971
- module: CC_FILE,
972
- method: 'setAgentState',
973
- }
974
- );
975
1155
 
976
- expect(setAgentStatusMock).toHaveBeenCalledWith({data: expectedPayload});
977
- expect(result).toEqual({data: expectedPayload});
978
- expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
979
- METRIC_EVENT_NAMES.AGENT_STATE_CHANGE_SUCCESS,
980
- METRIC_EVENT_NAMES.AGENT_STATE_CHANGE_FAILED,
981
- ]);
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);
982
1166
  });
983
1167
 
984
- it('should set agent status successfully when status is Meeting', async () => {
985
- const expectedPayload = {
986
- state: 'Meeting',
987
- auxCodeId: '12345',
988
- agentId: '123',
989
- lastStateChangeReason: 'Agent is in meeting',
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'],
990
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');
991
1191
 
992
- const setAgentStatusMock = jest
993
- .spyOn(webex.cc.services.agent, 'stateChange')
994
- .mockResolvedValue({data: expectedPayload});
995
-
996
- const result = await webex.cc.setAgentState(expectedPayload);
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
+ });
1669
+ });
1670
+
1671
+ describe('stationLogout', () => {
1672
+ it('should logout successfully', async () => {
1673
+ const data = {logoutReason: 'Logout reason'};
1674
+ const response = {};
1675
+
1676
+ const stationLogoutMock = jest
1677
+ .spyOn(webex.cc.services.agent, 'logout')
1678
+ .mockResolvedValue({} as StationLogoutResponse);
1679
+
1680
+ const result = await webex.cc.stationLogout(data);
1681
+
1682
+ // Verify logging calls
1683
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
1684
+ module: CC_FILE,
1685
+ method: 'stationLogout',
1686
+ });
1687
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Agent station logout completed successfully', {
1688
+ module: CC_FILE,
1689
+ method: 'stationLogout',
1690
+ });
1691
+
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
+ }
1919
+ });
1920
+
1921
+ it('should handle error during stationLogout', async () => {
1922
+ const data = {logoutReason: 'Logout reason'};
1923
+ const error = {
1924
+ details: {
1925
+ trackingId: '1234',
1926
+ data: {
1927
+ reason: 'Error while performing station logout',
1928
+ },
1929
+ },
1930
+ };
1931
+
1932
+ jest.spyOn(webex.cc.services.agent, 'logout').mockRejectedValue(error);
1933
+
1934
+ await expect(webex.cc.stationLogout(data)).rejects.toThrow(error.details.data.reason);
1935
+
1936
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
1937
+ module: CC_FILE,
1938
+ method: 'stationLogout',
1939
+ });
1940
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
1941
+ `stationLogout failed with reason: ${error.details.data.reason}`,
1942
+ {module: CC_FILE, method: 'stationLogout', trackingId: error.details.trackingId}
1943
+ );
1944
+ });
1945
+ });
1946
+
1947
+ describe('setAgentStatus', () => {
1948
+ it('should set agent status successfully when status is Available', async () => {
1949
+ const expectedPayload = {
1950
+ state: 'Available',
1951
+ auxCodeId: '0',
1952
+ agentId: '123',
1953
+ lastStateChangeReason: 'Agent is available',
1954
+ };
1955
+
1956
+ const setAgentStatusMock = jest
1957
+ .spyOn(webex.cc.services.agent, 'stateChange')
1958
+ .mockResolvedValue({data: expectedPayload});
1959
+
1960
+ const result = await webex.cc.setAgentState(expectedPayload);
1961
+
1962
+ // Verify logging calls
1963
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Setting agent state', {
1964
+ module: CC_FILE,
1965
+ method: 'setAgentState',
1966
+ });
1967
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
1968
+ `Agent state changed successfully to auxCodeId: ${expectedPayload.auxCodeId}`,
1969
+ {
1970
+ module: CC_FILE,
1971
+ method: 'setAgentState',
1972
+ }
1973
+ );
1974
+
1975
+ expect(setAgentStatusMock).toHaveBeenCalledWith({data: expectedPayload});
1976
+ expect(result).toEqual({data: expectedPayload});
1977
+ expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
1978
+ METRIC_EVENT_NAMES.AGENT_STATE_CHANGE_SUCCESS,
1979
+ METRIC_EVENT_NAMES.AGENT_STATE_CHANGE_FAILED,
1980
+ ]);
1981
+ });
1982
+
1983
+ it('should set agent status successfully when status is Meeting', async () => {
1984
+ const expectedPayload = {
1985
+ state: 'Meeting',
1986
+ auxCodeId: '12345',
1987
+ agentId: '123',
1988
+ lastStateChangeReason: 'Agent is in meeting',
1989
+ };
1990
+
1991
+ const setAgentStatusMock = jest
1992
+ .spyOn(webex.cc.services.agent, 'stateChange')
1993
+ .mockResolvedValue({data: expectedPayload});
1994
+
1995
+ const result = await webex.cc.setAgentState(expectedPayload);
997
1996
 
998
1997
  expect(setAgentStatusMock).toHaveBeenCalledWith({data: expectedPayload});
999
1998
  expect(result).toEqual({data: expectedPayload});
@@ -1134,6 +2133,41 @@ describe('webex.cc', () => {
1134
2133
  ]);
1135
2134
  });
1136
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
+
1137
2171
  it('should handle error', async () => {
1138
2172
  const data: BuddyAgents = {state: 'Available', mediaType: 'telephony'};
1139
2173
  webex.cc.agentConfig = {
@@ -1210,11 +2244,11 @@ describe('webex.cc', () => {
1210
2244
  const webSocketManagerOnSpy = jest.spyOn(webex.cc.services.webSocketManager, 'on');
1211
2245
  await webex.cc['silentRelogin']();
1212
2246
 
1213
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2247
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1214
2248
  module: CC_FILE,
1215
2249
  method: 'silentRelogin',
1216
2250
  });
1217
- expect(LoggerProxy.info).toHaveBeenCalledWith(
2251
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
1218
2252
  'event=requestAutoStateChange | Requesting state change to available on socket reconnect',
1219
2253
  {module: CC_FILE, method: 'silentRelogin'}
1220
2254
  );
@@ -1259,7 +2293,7 @@ describe('webex.cc', () => {
1259
2293
 
1260
2294
  jest.spyOn(webex.cc.services.agent, 'reload').mockRejectedValue(error);
1261
2295
  await webex.cc['silentRelogin']();
1262
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2296
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1263
2297
  module: CC_FILE,
1264
2298
  method: 'silentRelogin',
1265
2299
  });
@@ -1274,7 +2308,7 @@ describe('webex.cc', () => {
1274
2308
  jest.spyOn(webex.cc.services.agent, 'reload').mockRejectedValue(error);
1275
2309
 
1276
2310
  await expect(webex.cc['silentRelogin']()).rejects.toThrow(error);
1277
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2311
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1278
2312
  module: CC_FILE,
1279
2313
  method: 'silentRelogin',
1280
2314
  });
@@ -1316,7 +2350,7 @@ describe('webex.cc', () => {
1316
2350
 
1317
2351
  await webex.cc['silentRelogin']();
1318
2352
 
1319
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2353
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1320
2354
  module: CC_FILE,
1321
2355
  method: 'silentRelogin',
1322
2356
  });
@@ -1361,6 +2395,82 @@ describe('webex.cc', () => {
1361
2395
  expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.AGENT_DN);
1362
2396
  expect(webex.cc.agentConfig.dn).toBe('67890');
1363
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
+ });
1364
2474
  });
1365
2475
 
1366
2476
  describe('setupEventListeners()', () => {
@@ -1500,6 +2610,27 @@ describe('webex.cc', () => {
1500
2610
  });
1501
2611
  });
1502
2612
 
2613
+ describe('getQueues', () => {
2614
+ it('delegates to the queue service when successful', async () => {
2615
+ const mockQueuesResponse = [{queueId: 'queue1', queueName: 'Queue 1'}];
2616
+ const queueSpy = jest
2617
+ .spyOn(webex.cc.queue, 'getQueues')
2618
+ .mockResolvedValue(mockQueuesResponse as any);
2619
+
2620
+ const result = await webex.cc.getQueues({page: 1});
2621
+
2622
+ expect(queueSpy).toHaveBeenCalledWith({page: 1});
2623
+ expect(result).toBe(mockQueuesResponse);
2624
+ });
2625
+
2626
+ it('propagates queue service errors', async () => {
2627
+ const error = new Error('Test error.');
2628
+ jest.spyOn(webex.cc.queue, 'getQueues').mockRejectedValue(error);
2629
+
2630
+ await expect(webex.cc.getQueues()).rejects.toThrow('Test error.');
2631
+ });
2632
+ });
2633
+
1503
2634
  describe('uploadLogs', () => {
1504
2635
  it('should upload logs successfully', async () => {
1505
2636
  const uploadLogsMock = jest.spyOn(webex.cc.webexRequest, 'uploadLogs').mockResolvedValue({
@@ -1529,6 +2660,7 @@ describe('webex.cc', () => {
1529
2660
 
1530
2661
  describe('unregister', () => {
1531
2662
  let mockWebSocketManager;
2663
+ let mockRTDWebSocketManager;
1532
2664
  let mercuryDisconnectSpy;
1533
2665
  let deviceUnregisterSpy;
1534
2666
 
@@ -1545,8 +2677,15 @@ describe('webex.cc', () => {
1545
2677
  off: jest.fn(),
1546
2678
  on: jest.fn(),
1547
2679
  };
2680
+ mockRTDWebSocketManager = {
2681
+ isSocketClosed: false,
2682
+ close: jest.fn(),
2683
+ off: jest.fn(),
2684
+ on: jest.fn(),
2685
+ };
1548
2686
 
1549
2687
  webex.cc.services.webSocketManager = mockWebSocketManager;
2688
+ webex.cc.services.rtdWebSocketManager = mockRTDWebSocketManager;
1550
2689
 
1551
2690
  webex.internal = webex.internal || {};
1552
2691
  webex.internal.mercury = {
@@ -1570,6 +2709,18 @@ describe('webex.cc', () => {
1570
2709
  on: jest.fn(),
1571
2710
  } as any;
1572
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
+
1573
2724
  await webex.cc.deregister();
1574
2725
 
1575
2726
  expect(mockTaskManager.off).toHaveBeenCalledWith(
@@ -1580,6 +2731,10 @@ describe('webex.cc', () => {
1580
2731
  TASK_EVENTS.TASK_HYDRATE,
1581
2732
  expect.any(Function)
1582
2733
  );
2734
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
2735
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
2736
+ expect.any(Function)
2737
+ );
1583
2738
  expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1584
2739
  expect(webex.cc.services.rtdWebSocketManager.off).toHaveBeenCalledWith(
1585
2740
  'message',
@@ -1636,17 +2791,57 @@ describe('webex.cc', () => {
1636
2791
  const [, hydrateCallback] = hydrateCalls[0];
1637
2792
  expect(hydrateCallback).toBe(webex.cc['handleTaskHydrate']);
1638
2793
 
2794
+ const multiLoginHydrateCalls = mockTaskManager.off.mock.calls.filter(
2795
+ ([evt]) => evt === TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE
2796
+ );
2797
+ expect(multiLoginHydrateCalls).toHaveLength(1);
2798
+ const [, multiLoginHydrateCallback] = multiLoginHydrateCalls[0];
2799
+ expect(multiLoginHydrateCallback).toBe(webex.cc['handleTaskMultiLoginHydrate']);
2800
+
1639
2801
  const messageCalls = mockWebSocketManager.off.mock.calls.filter(([evt]) => evt === 'message');
1640
2802
  expect(messageCalls).toHaveLength(1);
1641
2803
  const [, messageCallback] = messageCalls[0];
1642
2804
  expect(messageCallback).toBe(webex.cc['handleWebsocketMessage']);
1643
2805
 
1644
- const connectionCalls = webex.cc.services.connectionService.off.mock.calls.filter(
1645
- ([evt]) => evt === 'connectionLost'
1646
- );
1647
- expect(connectionCalls).toHaveLength(1);
1648
- const [, connectionCallback] = connectionCalls[0];
1649
- expect(connectionCallback).toBe(webex.cc['handleConnectionLost']);
2806
+ const connectionCalls = webex.cc.services.connectionService.off.mock.calls.filter(
2807
+ ([evt]) => evt === 'connectionLost'
2808
+ );
2809
+ expect(connectionCalls).toHaveLength(1);
2810
+ const [, connectionCallback] = connectionCalls[0];
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']);
1650
2845
  });
1651
2846
 
1652
2847
  it('should skip webCallingService and internal cleanup when webrtc is disabled', async () => {
@@ -1661,6 +2856,10 @@ describe('webex.cc', () => {
1661
2856
  TASK_EVENTS.TASK_HYDRATE,
1662
2857
  expect.any(Function)
1663
2858
  );
2859
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
2860
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
2861
+ expect.any(Function)
2862
+ );
1664
2863
  expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1665
2864
  expect(webex.cc.services.connectionService.off).toHaveBeenCalledWith(
1666
2865
  'connectionLost',
@@ -1670,6 +2869,7 @@ describe('webex.cc', () => {
1670
2869
  expect(webex.internal.mercury.off).not.toHaveBeenCalled();
1671
2870
  expect(mercuryDisconnectSpy).not.toHaveBeenCalled();
1672
2871
  expect(deviceUnregisterSpy).not.toHaveBeenCalled();
2872
+ expect(mockRTDWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
1673
2873
  });
1674
2874
 
1675
2875
  it('should skip internal mercury cleanup when loginVoiceOptions does not include BROWSER', async () => {
@@ -1686,10 +2886,140 @@ describe('webex.cc', () => {
1686
2886
  expect(mercuryDisconnectSpy).not.toHaveBeenCalled();
1687
2887
  expect(deviceUnregisterSpy).not.toHaveBeenCalled();
1688
2888
 
2889
+ expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
2890
+ expect(mockRTDWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
2891
+ expect(webex.cc.agentConfig).toBeNull();
2892
+ });
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);
1689
2918
  expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
1690
2919
  expect(webex.cc.agentConfig).toBeNull();
1691
2920
  });
1692
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
+
1693
3023
  it('should handle errors during unregister and track metrics', async () => {
1694
3024
  const mockError = new Error('Failed to deregister device');
1695
3025
  webex.internal.device.unregister.mockRejectedValue(mockError);
@@ -1704,6 +3034,10 @@ describe('webex.cc', () => {
1704
3034
  TASK_EVENTS.TASK_HYDRATE,
1705
3035
  expect.any(Function)
1706
3036
  );
3037
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
3038
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
3039
+ expect.any(Function)
3040
+ );
1707
3041
 
1708
3042
  expect(LoggerProxy.error).toHaveBeenCalledWith(`Error during deregister: ${mockError}`, {
1709
3043
  module: CC_FILE,
@@ -1726,7 +3060,7 @@ describe('webex.cc', () => {
1726
3060
 
1727
3061
  beforeEach(() => {
1728
3062
  emitSpy = jest.spyOn(webex.cc, 'emit');
1729
- messageCallback = mockWebSocketManager.on.mock.calls.find((c) => c[0] === 'message')[1];
3063
+ messageCallback = webex.cc['handleWebsocketMessage'];
1730
3064
  });
1731
3065
 
1732
3066
  it('should emit AGENT_STATION_LOGIN_SUCCESS on CC_EVENTS.AGENT_STATION_LOGIN_SUCCESS with mapped payload', () => {
@@ -1936,6 +3270,55 @@ describe('webex.cc', () => {
1936
3270
  expect(result).toEqual(mockResp);
1937
3271
  });
1938
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
+
1939
3322
  it('should use provided teamId if passed in payload', async () => {
1940
3323
  const dataWithTeam = {
1941
3324
  teamId: 'newTeam',
@@ -2320,4 +3703,444 @@ describe('webex.cc', () => {
2320
3703
  expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'acceptPreviewContact', CC_FILE);
2321
3704
  });
2322
3705
  });
3706
+
3707
+ describe('skipPreviewContact', () => {
3708
+ const previewPayload = {
3709
+ interactionId: 'interaction-123',
3710
+ campaignId: 'campaign-456',
3711
+ };
3712
+
3713
+ it('should skip preview contact successfully', async () => {
3714
+ const mockResponse = {trackingId: 'track-123'} as AgentContact;
3715
+
3716
+ const skipPreviewContactMock = jest
3717
+ .spyOn(webex.cc.services.dialer, 'skipPreviewContact')
3718
+ .mockResolvedValue(mockResponse);
3719
+
3720
+ const result = await webex.cc.skipPreviewContact(previewPayload);
3721
+
3722
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Skipping campaign preview contact', {
3723
+ module: CC_FILE,
3724
+ method: 'skipPreviewContact',
3725
+ });
3726
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
3727
+ 'Campaign preview contact skipped successfully',
3728
+ {
3729
+ module: CC_FILE,
3730
+ method: 'skipPreviewContact',
3731
+ trackingId: 'track-123',
3732
+ interactionId: previewPayload.interactionId,
3733
+ }
3734
+ );
3735
+
3736
+ expect(skipPreviewContactMock).toHaveBeenCalledWith({data: previewPayload});
3737
+ expect(result).toEqual(mockResponse);
3738
+ });
3739
+
3740
+ it('should handle error during skipPreviewContact', async () => {
3741
+ getErrorDetailsSpy.mockRestore();
3742
+ getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
3743
+
3744
+ const error = {
3745
+ details: {
3746
+ trackingId: '1234',
3747
+ data: {
3748
+ reason: 'Error while performing skipPreviewContact',
3749
+ },
3750
+ },
3751
+ };
3752
+
3753
+ jest.spyOn(webex.cc.services.dialer, 'skipPreviewContact').mockRejectedValue(error);
3754
+
3755
+ await expect(webex.cc.skipPreviewContact(previewPayload)).rejects.toThrow(
3756
+ error.details.data.reason
3757
+ );
3758
+
3759
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Skipping campaign preview contact', {
3760
+ module: CC_FILE,
3761
+ method: 'skipPreviewContact',
3762
+ });
3763
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
3764
+ `skipPreviewContact failed with reason: ${error.details.data.reason}`,
3765
+ {module: CC_FILE, method: 'skipPreviewContact', trackingId: error.details.trackingId}
3766
+ );
3767
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'skipPreviewContact', CC_FILE);
3768
+ });
3769
+ });
3770
+
3771
+ describe('removePreviewContact', () => {
3772
+ const previewPayload = {
3773
+ interactionId: 'interaction-123',
3774
+ campaignId: 'campaign-456',
3775
+ };
3776
+
3777
+ it('should remove preview contact successfully', async () => {
3778
+ const mockResponse = {trackingId: 'track-123'} as AgentContact;
3779
+
3780
+ const removePreviewContactMock = jest
3781
+ .spyOn(webex.cc.services.dialer, 'removePreviewContact')
3782
+ .mockResolvedValue(mockResponse);
3783
+
3784
+ const result = await webex.cc.removePreviewContact(previewPayload);
3785
+
3786
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Removing campaign preview contact', {
3787
+ module: CC_FILE,
3788
+ method: 'removePreviewContact',
3789
+ });
3790
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
3791
+ 'Campaign preview contact removed successfully',
3792
+ {
3793
+ module: CC_FILE,
3794
+ method: 'removePreviewContact',
3795
+ trackingId: 'track-123',
3796
+ interactionId: previewPayload.interactionId,
3797
+ }
3798
+ );
3799
+
3800
+ expect(removePreviewContactMock).toHaveBeenCalledWith({data: previewPayload});
3801
+ expect(result).toEqual(mockResponse);
3802
+ });
3803
+
3804
+ it('should handle error during removePreviewContact', async () => {
3805
+ getErrorDetailsSpy.mockRestore();
3806
+ getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
3807
+
3808
+ const error = {
3809
+ details: {
3810
+ trackingId: '1234',
3811
+ data: {
3812
+ reason: 'Error while performing removePreviewContact',
3813
+ },
3814
+ },
3815
+ };
3816
+
3817
+ jest.spyOn(webex.cc.services.dialer, 'removePreviewContact').mockRejectedValue(error);
3818
+
3819
+ await expect(webex.cc.removePreviewContact(previewPayload)).rejects.toThrow(
3820
+ error.details.data.reason
3821
+ );
3822
+
3823
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Removing campaign preview contact', {
3824
+ module: CC_FILE,
3825
+ method: 'removePreviewContact',
3826
+ });
3827
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
3828
+ `removePreviewContact failed with reason: ${error.details.data.reason}`,
3829
+ {module: CC_FILE, method: 'removePreviewContact', trackingId: error.details.trackingId}
3830
+ );
3831
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'removePreviewContact', CC_FILE);
3832
+ });
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
+ });
2323
4146
  });