@webex/contact-center 3.11.0 → 3.12.0-auth-prejoin-fetch.1

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 (325) hide show
  1. package/.sdd/manifest.json +883 -0
  2. package/AGENTS.md +94 -0
  3. package/ai-docs/ARCHITECTURE.md +168 -0
  4. package/ai-docs/CONTRACTS.md +50 -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 +818 -59
  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 +31 -3
  45. package/dist/constants.js.map +1 -1
  46. package/dist/index.js +27 -5
  47. package/dist/index.js.map +1 -1
  48. package/dist/metrics/behavioral-events.js +127 -0
  49. package/dist/metrics/behavioral-events.js.map +1 -1
  50. package/dist/metrics/constants.js +34 -3
  51. package/dist/metrics/constants.js.map +1 -1
  52. package/dist/services/AddressBook.js +18 -15
  53. package/dist/services/AddressBook.js.map +1 -1
  54. package/dist/services/AnswerCallOnWebexService.js +174 -0
  55. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  56. package/dist/services/ApiAiAssistant.js +318 -0
  57. package/dist/services/ApiAiAssistant.js.map +1 -0
  58. package/dist/services/EntryPoint.js +46 -68
  59. package/dist/services/EntryPoint.js.map +1 -1
  60. package/dist/services/Queue.js +27 -22
  61. package/dist/services/Queue.js.map +1 -1
  62. package/dist/services/UserPreference.js +427 -0
  63. package/dist/services/UserPreference.js.map +1 -0
  64. package/dist/services/WebexCrossClientService.js +171 -0
  65. package/dist/services/WebexCrossClientService.js.map +1 -0
  66. package/dist/services/WxAppTelephonyMercurySync.js +93 -0
  67. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  68. package/dist/services/agent/types.js.map +1 -1
  69. package/dist/services/config/Util.js +11 -4
  70. package/dist/services/config/Util.js.map +1 -1
  71. package/dist/services/config/constants.js +45 -8
  72. package/dist/services/config/constants.js.map +1 -1
  73. package/dist/services/config/index.js +41 -2
  74. package/dist/services/config/index.js.map +1 -1
  75. package/dist/services/config/types.js +70 -8
  76. package/dist/services/config/types.js.map +1 -1
  77. package/dist/services/constants.js +27 -1
  78. package/dist/services/constants.js.map +1 -1
  79. package/dist/services/core/Err.js.map +1 -1
  80. package/dist/services/core/Utils.js +122 -25
  81. package/dist/services/core/Utils.js.map +1 -1
  82. package/dist/services/core/WebexRequest.js +6 -2
  83. package/dist/services/core/WebexRequest.js.map +1 -1
  84. package/dist/services/core/aqm-reqs.js +119 -30
  85. package/dist/services/core/aqm-reqs.js.map +1 -1
  86. package/dist/services/core/types.js.map +1 -1
  87. package/dist/services/core/websocket/WebSocketManager.js +22 -6
  88. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  89. package/dist/services/core/websocket/connection-service.js +3 -1
  90. package/dist/services/core/websocket/connection-service.js.map +1 -1
  91. package/dist/services/core/websocket/types.js.map +1 -1
  92. package/dist/services/index.js +6 -0
  93. package/dist/services/index.js.map +1 -1
  94. package/dist/services/task/Task.js +754 -0
  95. package/dist/services/task/Task.js.map +1 -0
  96. package/dist/services/task/TaskFactory.js +49 -0
  97. package/dist/services/task/TaskFactory.js.map +1 -0
  98. package/dist/services/task/TaskManager.js +1073 -447
  99. package/dist/services/task/TaskManager.js.map +1 -1
  100. package/dist/services/task/TaskUtils.js +220 -23
  101. package/dist/services/task/TaskUtils.js.map +1 -1
  102. package/dist/services/task/WebexCallingUtils.js +70 -0
  103. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  104. package/dist/services/task/constants.js +26 -2
  105. package/dist/services/task/constants.js.map +1 -1
  106. package/dist/services/task/contact.js +29 -0
  107. package/dist/services/task/contact.js.map +1 -1
  108. package/dist/services/task/dialer.js +129 -0
  109. package/dist/services/task/dialer.js.map +1 -1
  110. package/dist/services/task/digital/Digital.js +78 -0
  111. package/dist/services/task/digital/Digital.js.map +1 -0
  112. package/dist/services/task/state-machine/TaskStateMachine.js +971 -0
  113. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  114. package/dist/services/task/state-machine/actions.js +572 -0
  115. package/dist/services/task/state-machine/actions.js.map +1 -0
  116. package/dist/services/task/state-machine/constants.js +161 -0
  117. package/dist/services/task/state-machine/constants.js.map +1 -0
  118. package/dist/services/task/state-machine/guards.js +409 -0
  119. package/dist/services/task/state-machine/guards.js.map +1 -0
  120. package/dist/services/task/state-machine/index.js +53 -0
  121. package/dist/services/task/state-machine/index.js.map +1 -0
  122. package/dist/services/task/state-machine/types.js +54 -0
  123. package/dist/services/task/state-machine/types.js.map +1 -0
  124. package/dist/services/task/state-machine/uiControlsComputer.js +703 -0
  125. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  126. package/dist/services/task/taskDataNormalizer.js +99 -0
  127. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  128. package/dist/services/task/types.js +243 -5
  129. package/dist/services/task/types.js.map +1 -1
  130. package/dist/services/task/voice/Voice.js +1380 -0
  131. package/dist/services/task/voice/Voice.js.map +1 -0
  132. package/dist/services/task/voice/WebRTC.js +152 -0
  133. package/dist/services/task/voice/WebRTC.js.map +1 -0
  134. package/dist/services/task/voice/wxAppVoiceMethods.js +201 -0
  135. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  136. package/dist/services/wxAppTelephonyUtils.js +19 -0
  137. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  138. package/dist/types/cc.d.ts +967 -0
  139. package/dist/types/config.d.ts +79 -0
  140. package/dist/types/constants.d.ts +74 -0
  141. package/dist/types/index.d.ts +201 -0
  142. package/dist/types/logger-proxy.d.ts +71 -0
  143. package/dist/types/metrics/MetricsManager.d.ts +223 -0
  144. package/dist/types/metrics/behavioral-events.d.ts +29 -0
  145. package/dist/types/metrics/constants.d.ts +183 -0
  146. package/dist/types/services/AddressBook.d.ts +75 -0
  147. package/dist/types/services/AnswerCallOnWebexService.d.ts +37 -0
  148. package/dist/types/services/ApiAiAssistant.d.ts +49 -0
  149. package/dist/types/services/EntryPoint.d.ts +69 -0
  150. package/dist/types/services/Queue.d.ts +78 -0
  151. package/dist/types/services/UserPreference.d.ts +118 -0
  152. package/dist/types/services/WebCallingService.d.ts +1 -0
  153. package/dist/types/services/WebexCrossClientService.d.ts +28 -0
  154. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  155. package/dist/types/services/agent/index.d.ts +46 -0
  156. package/dist/types/services/agent/types.d.ts +413 -0
  157. package/dist/types/services/config/Util.d.ts +20 -0
  158. package/dist/types/services/config/constants.d.ts +273 -0
  159. package/dist/types/services/config/index.d.ts +177 -0
  160. package/dist/types/services/config/types.d.ts +1381 -0
  161. package/dist/types/services/constants.d.ts +110 -0
  162. package/dist/types/services/core/Err.d.ts +127 -0
  163. package/dist/types/services/core/GlobalTypes.d.ts +58 -0
  164. package/dist/types/services/core/Utils.d.ts +121 -0
  165. package/dist/types/services/core/WebexRequest.d.ts +23 -0
  166. package/dist/types/services/core/aqm-reqs.d.ts +65 -0
  167. package/dist/types/services/core/constants.d.ts +99 -0
  168. package/dist/types/services/core/types.d.ts +49 -0
  169. package/dist/types/services/core/websocket/WebSocketManager.d.ts +36 -0
  170. package/dist/types/services/core/websocket/connection-service.d.ts +27 -0
  171. package/dist/types/services/core/websocket/keepalive.worker.d.ts +2 -0
  172. package/dist/types/services/core/websocket/types.d.ts +37 -0
  173. package/dist/types/services/index.d.ts +54 -0
  174. package/dist/types/services/task/AutoWrapup.d.ts +40 -0
  175. package/dist/types/services/task/Task.d.ts +175 -0
  176. package/dist/types/services/task/TaskFactory.d.ts +13 -0
  177. package/dist/types/services/task/TaskManager.d.ts +1 -0
  178. package/dist/types/services/task/TaskUtils.d.ts +138 -0
  179. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  180. package/dist/types/services/task/constants.d.ts +94 -0
  181. package/dist/types/services/task/contact.d.ts +73 -0
  182. package/dist/types/services/task/dialer.d.ts +73 -0
  183. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  184. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1398 -0
  185. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  186. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  187. package/dist/types/services/task/state-machine/guards.d.ts +103 -0
  188. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  189. package/dist/types/services/task/state-machine/types.d.ts +277 -0
  190. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  191. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  192. package/dist/types/services/task/types.d.ts +1933 -0
  193. package/dist/types/services/task/voice/Voice.d.ts +223 -0
  194. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  195. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +52 -0
  196. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  197. package/dist/types/types.d.ts +784 -0
  198. package/dist/types/utils/PageCache.d.ts +190 -0
  199. package/dist/types/webex-config.d.ts +53 -0
  200. package/dist/types/webex.d.ts +8 -0
  201. package/dist/types.js +137 -3
  202. package/dist/types.js.map +1 -1
  203. package/dist/utils/PageCache.js +19 -5
  204. package/dist/utils/PageCache.js.map +1 -1
  205. package/dist/webex.js +14 -2
  206. package/dist/webex.js.map +1 -1
  207. package/package.json +16 -12
  208. package/src/cc.ts +983 -60
  209. package/src/config.ts +13 -0
  210. package/src/constants.ts +29 -1
  211. package/src/index.ts +26 -5
  212. package/src/metrics/ai-docs/AGENTS.md +350 -0
  213. package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
  214. package/src/metrics/ai-docs/metrics-spec.md +860 -0
  215. package/src/metrics/behavioral-events.ts +134 -0
  216. package/src/metrics/constants.ts +39 -3
  217. package/src/services/AddressBook.ts +17 -6
  218. package/src/services/AnswerCallOnWebexService.ts +206 -0
  219. package/src/services/ApiAiAssistant.ts +412 -0
  220. package/src/services/EntryPoint.ts +59 -60
  221. package/src/services/Queue.ts +29 -12
  222. package/src/services/UserPreference.ts +509 -0
  223. package/src/services/WebexCrossClientService.ts +212 -0
  224. package/src/services/WxAppTelephonyMercurySync.ts +115 -0
  225. package/src/services/agent/ai-docs/AGENTS.md +240 -0
  226. package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
  227. package/src/services/agent/ai-docs/agent-spec.md +504 -0
  228. package/src/services/agent/types.ts +1 -1
  229. package/src/services/ai-docs/AGENTS.md +386 -0
  230. package/src/services/ai-docs/services-spec.md +497 -0
  231. package/src/services/config/Util.ts +13 -2
  232. package/src/services/config/ai-docs/AGENTS.md +255 -0
  233. package/src/services/config/ai-docs/ARCHITECTURE.md +426 -0
  234. package/src/services/config/ai-docs/config-spec.md +675 -0
  235. package/src/services/config/constants.ts +47 -7
  236. package/src/services/config/index.ts +45 -1
  237. package/src/services/config/types.ts +253 -11
  238. package/src/services/constants.ts +29 -0
  239. package/src/services/core/Err.ts +4 -0
  240. package/src/services/core/Utils.ts +143 -30
  241. package/src/services/core/WebexRequest.ts +3 -1
  242. package/src/services/core/ai-docs/AGENTS.md +381 -0
  243. package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
  244. package/src/services/core/ai-docs/core-spec.md +787 -0
  245. package/src/services/core/aqm-reqs.ts +125 -32
  246. package/src/services/core/types.ts +2 -0
  247. package/src/services/core/websocket/WebSocketManager.ts +23 -6
  248. package/src/services/core/websocket/connection-service.ts +5 -1
  249. package/src/services/core/websocket/types.ts +1 -1
  250. package/src/services/index.ts +4 -0
  251. package/src/services/task/Task.ts +908 -0
  252. package/src/services/task/TaskFactory.ts +60 -0
  253. package/src/services/task/TaskManager.ts +1291 -513
  254. package/src/services/task/TaskUtils.ts +314 -24
  255. package/src/services/task/WebexCallingUtils.ts +136 -0
  256. package/src/services/task/ai-docs/AGENTS.md +457 -0
  257. package/src/services/task/ai-docs/ARCHITECTURE.md +595 -0
  258. package/src/services/task/ai-docs/task-spec.md +1469 -0
  259. package/src/services/task/constants.ts +26 -0
  260. package/src/services/task/contact.ts +30 -0
  261. package/src/services/task/dialer.ts +136 -1
  262. package/src/services/task/digital/Digital.ts +97 -0
  263. package/src/services/task/state-machine/TaskStateMachine.ts +1313 -0
  264. package/src/services/task/state-machine/actions.ts +741 -0
  265. package/src/services/task/state-machine/ai-docs/AGENTS.md +462 -0
  266. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1146 -0
  267. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2209 -0
  268. package/src/services/task/state-machine/constants.ts +172 -0
  269. package/src/services/task/state-machine/guards.ts +498 -0
  270. package/src/services/task/state-machine/index.ts +28 -0
  271. package/src/services/task/state-machine/types.ts +258 -0
  272. package/src/services/task/state-machine/uiControlsComputer.ts +1135 -0
  273. package/src/services/task/taskDataNormalizer.ts +137 -0
  274. package/src/services/task/types.ts +843 -70
  275. package/src/services/task/voice/Voice.ts +1720 -0
  276. package/src/services/task/voice/WebRTC.ts +191 -0
  277. package/src/services/task/voice/wxAppVoiceMethods.ts +307 -0
  278. package/src/services/wxAppTelephonyUtils.ts +14 -0
  279. package/src/types.ts +238 -11
  280. package/src/utils/AGENTS.md +289 -0
  281. package/src/utils/PageCache.ts +38 -5
  282. package/src/utils/ai-docs/utils-spec.md +391 -0
  283. package/src/webex.js +2 -0
  284. package/test/unit/spec/cc.ts +1856 -122
  285. package/test/unit/spec/logger-proxy.ts +70 -0
  286. package/test/unit/spec/metrics/behavioral-events.ts +18 -0
  287. package/test/unit/spec/services/AddressBook.ts +37 -6
  288. package/test/unit/spec/services/AnswerCallOnWebexService.ts +223 -0
  289. package/test/unit/spec/services/ApiAiAssistant.ts +273 -0
  290. package/test/unit/spec/services/EntryPoint.ts +87 -40
  291. package/test/unit/spec/services/Queue.ts +123 -12
  292. package/test/unit/spec/services/UserPreference.ts +401 -0
  293. package/test/unit/spec/services/WebCallingService.ts +7 -1
  294. package/test/unit/spec/services/WebexCrossClientService.ts +261 -0
  295. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +113 -0
  296. package/test/unit/spec/services/config/Util.ts +85 -0
  297. package/test/unit/spec/services/config/index.ts +85 -29
  298. package/test/unit/spec/services/core/Utils.ts +481 -2
  299. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  300. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  301. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +137 -41
  302. package/test/unit/spec/services/core/websocket/connection-service.ts +3 -1
  303. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  304. package/test/unit/spec/services/task/Task.ts +677 -0
  305. package/test/unit/spec/services/task/TaskFactory.ts +99 -0
  306. package/test/unit/spec/services/task/TaskManager.ts +2209 -918
  307. package/test/unit/spec/services/task/TaskUtils.ts +235 -0
  308. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  309. package/test/unit/spec/services/task/contact.ts +33 -0
  310. package/test/unit/spec/services/task/dialer.ts +372 -96
  311. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  312. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +3433 -0
  313. package/test/unit/spec/services/task/state-machine/guards.ts +839 -0
  314. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  315. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +3101 -0
  316. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  317. package/test/unit/spec/services/task/voice/Voice.ts +1523 -0
  318. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  319. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +459 -0
  320. package/umd/contact-center.min.js +2 -2
  321. package/umd/contact-center.min.js.map +1 -1
  322. package/dist/services/task/index.js +0 -1525
  323. package/dist/services/task/index.js.map +0 -1
  324. package/src/services/task/index.ts +0 -1801
  325. package/test/unit/spec/services/task/index.ts +0 -2184
@@ -15,7 +15,7 @@ import type {ContactServiceQueuesResponse} from '../../../src/types';
15
15
  import MockWebex from '@webex/test-helper-mock-webex';
16
16
  import {StationLoginSuccess, AGENT_EVENTS} from '../../../src/services/agent/types';
17
17
  import {SetStateResponse} from '../../../src/types';
18
- import {AGENT, WEB_RTC_PREFIX} from '../../../src/services/constants';
18
+ import {AGENT, SUBSCRIBE_API, WEB_RTC_PREFIX} from '../../../src/services/constants';
19
19
  import Services from '../../../src/services';
20
20
  import config from '../../../src/config';
21
21
  import {CC_EVENTS} from '../../../src/services/config/types';
@@ -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;
@@ -88,6 +89,8 @@ describe('webex.cc', () => {
88
89
  initWebSocket: jest.fn(),
89
90
  on: jest.fn(),
90
91
  off: jest.fn(),
92
+ close: jest.fn(),
93
+ isSocketClosed: false,
91
94
  };
92
95
 
93
96
  mockContact = {
@@ -107,6 +110,14 @@ describe('webex.cc', () => {
107
110
  cancelCtq: jest.fn(),
108
111
  };
109
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
+
110
121
  // Mock Services instance
111
122
  const mockServicesInstance = {
112
123
  agent: {
@@ -121,6 +132,13 @@ describe('webex.cc', () => {
121
132
  getOutdialAniEntries: jest.fn(),
122
133
  },
123
134
  webSocketManager: mockWebSocketManager,
135
+ rtdWebSocketManager: {
136
+ initWebSocket: jest.fn().mockResolvedValue({}),
137
+ on: jest.fn(),
138
+ off: jest.fn(),
139
+ close: jest.fn(),
140
+ isSocketClosed: false,
141
+ },
124
142
  connectionService: {
125
143
  on: jest.fn(),
126
144
  off: jest.fn(),
@@ -129,23 +147,35 @@ describe('webex.cc', () => {
129
147
 
130
148
  dialer: {
131
149
  startOutdial: jest.fn(),
150
+ acceptPreviewContact: jest.fn(),
151
+ skipPreviewContact: jest.fn(),
152
+ removePreviewContact: jest.fn(),
153
+ },
154
+ apiAIAssistant: {
155
+ sendEvent: jest.fn(),
156
+ fetchHistoricTranscripts: jest.fn(),
132
157
  },
133
158
  };
134
159
 
135
160
  mockTaskManager = {
161
+ apiAIAssistant: mockApiAIAssistant,
136
162
  contact: mockContact,
137
163
  call: undefined,
138
164
  taskCollection: {},
139
165
  webCallingService: undefined,
140
166
  webSocketManager: mockWebSocketManager,
141
167
  task: undefined,
168
+ setConfigFlags: jest.fn(),
142
169
  setWrapupData: jest.fn(),
143
170
  setAgentId: jest.fn(),
144
171
  setWebRtcEnabled: jest.fn(),
172
+ setAnswerCallOnWebexService: jest.fn(),
145
173
  registerIncomingCallEvent: jest.fn(),
146
174
  registerTaskListeners: jest.fn(),
147
175
  getTask: jest.fn(),
148
176
  getActiveTasks: jest.fn(),
177
+ applyEnableWxBetterTogether: jest.fn(),
178
+ syncWxAppMuteFromCallDetailsForAllTasks: jest.fn(),
149
179
  on: jest.fn(),
150
180
  off: jest.fn(),
151
181
  emit: jest.fn(),
@@ -184,6 +214,35 @@ describe('webex.cc', () => {
184
214
  webex.emit('ready');
185
215
  });
186
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
+
187
246
  describe('cc.getDeviceId', () => {
188
247
  it('should return dialNumber when loginOption is EXTENSION', () => {
189
248
  const loginOption = LoginOption.EXTENSION;
@@ -251,10 +310,13 @@ describe('webex.cc', () => {
251
310
  isAgentAvailableAfterOutdial: false,
252
311
  isCampaignManagementEnabled: false,
253
312
  outDialEp: '',
254
- isEndCallEnabled: false,
313
+ isEndTaskEnabled: false,
255
314
  isEndConsultEnabled: false,
256
315
  agentDbId: '',
257
316
  allowConsultToQueue: false,
317
+ accessQueue: 'SPECIFIC',
318
+ accessEntryPoint: 'ALL',
319
+ accessBuddyTeam: 'ALL',
258
320
  agentPersonalStatsEnabled: false,
259
321
  isTimeoutDesktopInactivityEnabled: false,
260
322
  webRtcEnabled: true,
@@ -262,6 +324,7 @@ describe('webex.cc', () => {
262
324
  };
263
325
 
264
326
  it('should register successfully and return agent profile', async () => {
327
+ mockAgentProfile.aiFeature = {realtimeTranscripts: {enable: true}} as any;
265
328
  const mercuryConnect = jest.spyOn(webex.internal.mercury, 'connect').mockResolvedValue(true);
266
329
  const connectWebsocketSpy = jest.spyOn(webex.cc, 'connectWebsocket');
267
330
  const setupEventListenersSpy = jest.spyOn(webex.cc, 'setupEventListeners');
@@ -283,7 +346,7 @@ describe('webex.cc', () => {
283
346
  const result = await webex.cc.register();
284
347
 
285
348
  // Verify logging calls
286
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting CC SDK registration', {
349
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting CC SDK registration', {
287
350
  module: CC_FILE,
288
351
  method: 'register',
289
352
  });
@@ -305,6 +368,7 @@ describe('webex.cc', () => {
305
368
  clientType: 'WebexCCSDK',
306
369
  allowMultiLogin: false,
307
370
  },
371
+ resource: SUBSCRIBE_API,
308
372
  });
309
373
 
310
374
  // TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
@@ -316,13 +380,44 @@ describe('webex.cc', () => {
316
380
  TASK_EVENTS.TASK_HYDRATE,
317
381
  expect.any(Function)
318
382
  );
383
+ expect(mockTaskManager.on).toHaveBeenCalledWith(
384
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
385
+ expect.any(Function)
386
+ );
319
387
  expect(mockWebSocketManager.on).toHaveBeenCalledWith('message', expect.any(Function));
388
+ expect(webex.cc.services.rtdWebSocketManager.initWebSocket).toHaveBeenCalledWith({
389
+ body: {
390
+ force: true,
391
+ isKeepAliveEnabled: false,
392
+ clientType: 'WebexCCSDK',
393
+ allowMultiLogin: false,
394
+ },
395
+ resource: 'v1/realtime/subscribe',
396
+ });
397
+ expect(webex.cc.services.rtdWebSocketManager.on).toHaveBeenCalledWith(
398
+ 'message',
399
+ expect.any(Function)
400
+ );
320
401
 
321
402
  expect(configSpy).toHaveBeenCalled();
322
403
  expect(LoggerProxy.log).toHaveBeenCalledWith('Agent config is fetched successfully', {
323
404
  module: CC_FILE,
324
405
  method: 'connectWebsocket',
325
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
+ });
326
421
  expect(reloadSpy).toHaveBeenCalled();
327
422
  expect(result).toEqual(mockAgentProfile);
328
423
  expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
@@ -360,6 +455,7 @@ describe('webex.cc', () => {
360
455
  clientType: 'WebexCCSDK',
361
456
  allowMultiLogin: true,
362
457
  },
458
+ resource: SUBSCRIBE_API,
363
459
  });
364
460
  expect(configSpy).toHaveBeenCalled();
365
461
  expect(LoggerProxy.log).toHaveBeenCalledWith('Agent config is fetched successfully', {
@@ -381,7 +477,7 @@ describe('webex.cc', () => {
381
477
 
382
478
  await expect(webex.cc.register()).rejects.toThrow('Error while performing register');
383
479
 
384
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting CC SDK registration', {
480
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting CC SDK registration', {
385
481
  module: CC_FILE,
386
482
  method: 'register',
387
483
  });
@@ -439,6 +535,7 @@ describe('webex.cc', () => {
439
535
  clientType: 'WebexCCSDK',
440
536
  allowMultiLogin: false,
441
537
  },
538
+ resource: SUBSCRIBE_API,
442
539
  });
443
540
 
444
541
  expect(mockTaskManager.on).toHaveBeenCalledWith(
@@ -449,6 +546,10 @@ describe('webex.cc', () => {
449
546
  TASK_EVENTS.TASK_HYDRATE,
450
547
  expect.any(Function)
451
548
  );
549
+ expect(mockTaskManager.on).toHaveBeenCalledWith(
550
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
551
+ expect.any(Function)
552
+ );
452
553
  expect(mockWebSocketManager.on).toHaveBeenCalledWith('message', expect.any(Function));
453
554
 
454
555
  expect(configSpy).toHaveBeenCalled();
@@ -460,8 +561,39 @@ describe('webex.cc', () => {
460
561
  expect(result).toEqual(mockAgentProfile);
461
562
  });
462
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
+
463
594
  it('should not attempt for mercury connection when webrtc is disabled', async () => {
464
595
  mockAgentProfile.webRtcEnabled = false;
596
+ mockAgentProfile.aiFeature = {realtimeTranscripts: {enable: false}} as any;
465
597
  const mercurySpy = jest.spyOn(webex.internal.mercury, 'connect');
466
598
  const connectWebsocketSpy = jest.spyOn(webex.cc, 'connectWebsocket');
467
599
  const setupEventListenersSpy = jest.spyOn(webex.cc, 'setupEventListeners');
@@ -491,12 +623,39 @@ describe('webex.cc', () => {
491
623
  clientType: 'WebexCCSDK',
492
624
  allowMultiLogin: false,
493
625
  },
626
+ resource: SUBSCRIBE_API,
494
627
  });
495
628
 
496
629
  expect(configSpy).toHaveBeenCalled();
497
630
  expect(mercurySpy).not.toHaveBeenCalled();
631
+ expect(webex.cc.services.rtdWebSocketManager.initWebSocket).not.toHaveBeenCalled();
498
632
  expect(result).toEqual(mockAgentProfile);
499
633
  });
634
+
635
+ it('should not connect RTD websocket when realtime transcripts feature is disabled', async () => {
636
+ mockAgentProfile.aiFeature = {realtimeTranscripts: {enable: false}} as any;
637
+ jest.spyOn(webex.internal.mercury, 'connect').mockResolvedValue(true);
638
+ jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
639
+ data: {
640
+ auxCodeId: 'auxCodeId',
641
+ agentId: 'agentId',
642
+ deviceType: LoginOption.EXTENSION,
643
+ dn: '12345',
644
+ },
645
+ });
646
+ jest.spyOn(webex.cc.services.config, 'getAgentConfig').mockResolvedValue(mockAgentProfile);
647
+ mockWebSocketManager.initWebSocket.mockResolvedValue({
648
+ agentId: 'agent123',
649
+ });
650
+
651
+ await webex.cc.register();
652
+
653
+ expect(webex.cc.services.rtdWebSocketManager.initWebSocket).not.toHaveBeenCalled();
654
+ expect(webex.cc.services.rtdWebSocketManager.on).not.toHaveBeenCalledWith(
655
+ 'message',
656
+ expect.any(Function)
657
+ );
658
+ });
500
659
  });
501
660
 
502
661
  describe('stationLogin', () => {
@@ -588,6 +747,7 @@ describe('webex.cc', () => {
588
747
  METRIC_EVENT_NAMES.STATION_LOGIN_FAILED,
589
748
  ]);
590
749
  expect(result).toEqual(responseMock);
750
+ expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.BROWSER);
591
751
 
592
752
  const onSpy = jest.spyOn(mockTaskManager, 'on');
593
753
  const emitSpy = jest.spyOn(webex.cc, 'trigger');
@@ -600,10 +760,8 @@ describe('webex.cc', () => {
600
760
 
601
761
  expect(emitSpy).toHaveBeenCalledTimes(1);
602
762
  expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, mockTask);
603
- // Verify message event listener
604
- const messageCallback = mockWebSocketManager.on.mock.calls.find(
605
- (call) => call[0] === 'message'
606
- )[1];
763
+ // Verify websocket message handling
764
+ const messageCallback = webex.cc['handleWebsocketMessage'];
607
765
  const agentStateChangeEventData = {
608
766
  type: CC_EVENTS.AGENT_STATE_CHANGE,
609
767
  data: {some: 'data'},
@@ -631,6 +789,59 @@ describe('webex.cc', () => {
631
789
  );
632
790
  });
633
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
+
634
845
  it('should not attempt mobius registration for LoginOption.BROWSER if webrtc is disabled', async () => {
635
846
  const options = {
636
847
  teamId: 'teamId',
@@ -753,10 +964,13 @@ describe('webex.cc', () => {
753
964
  const result = await webex.cc.stationLogin(options);
754
965
 
755
966
  // Verify logging calls
756
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station login', {
757
- module: CC_FILE,
758
- method: 'stationLogin',
759
- });
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
+ );
760
974
  expect(LoggerProxy.log).toHaveBeenCalledWith(
761
975
  `Agent station login completed successfully agentId: ${mockData.data.agentId} loginOption: ${mockData.data.loginOption} teamId: ${mockData.data.teamId}`,
762
976
  {
@@ -807,102 +1021,726 @@ describe('webex.cc', () => {
807
1021
 
808
1022
  await expect(webex.cc.stationLogin(options)).rejects.toThrow(error.details.data.reason);
809
1023
 
810
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station login', {
811
- module: CC_FILE,
812
- method: 'stationLogin',
813
- });
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
+ );
814
1031
  expect(LoggerProxy.error).toHaveBeenCalledWith(
815
1032
  `stationLogin failed with reason: ${error.details.data.reason}`,
816
1033
  {module: CC_FILE, method: 'stationLogin', trackingId: error.details.trackingId}
817
1034
  );
818
1035
  });
819
- });
820
1036
 
821
- describe('stationLogout', () => {
822
- it('should logout successfully', async () => {
823
- const data = {logoutReason: 'Logout reason'};
824
- 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;
825
1053
 
826
- const stationLogoutMock = jest
827
- .spyOn(webex.cc.services.agent, 'logout')
828
- .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(() => {});
829
1060
 
830
- const result = await webex.cc.stationLogout(data);
1061
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1062
+ data: {
1063
+ agentId: 'agentId',
1064
+ teamId: 'teamId',
1065
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1066
+ },
1067
+ trackingId: 'track-1',
1068
+ } as StationLoginSuccess);
831
1069
 
832
- // Verify logging calls
833
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
834
- module: CC_FILE,
835
- method: 'stationLogout',
836
- });
837
- expect(LoggerProxy.log).toHaveBeenCalledWith('Agent station logout completed successfully', {
838
- module: CC_FILE,
839
- method: 'stationLogout',
1070
+ await webex.cc.stationLogin({
1071
+ teamId: 'teamId',
1072
+ loginOption: LoginOption.EXTENSION,
1073
+ dialNumber: '1001',
840
1074
  });
841
1075
 
842
- expect(stationLogoutMock).toHaveBeenCalledWith({data: data});
843
- // TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
844
- // expect(mockTaskManager.unregisterIncomingCallEvent).toHaveBeenCalledWith();
845
- // expect(mockTaskManager.off).toHaveBeenCalledWith(
846
- // TASK_EVENTS.TASK_INCOMING,
847
- // expect.any(Function)
848
- // );
849
- // expect(mockTaskManager.off).toHaveBeenCalledWith(
850
- // TASK_EVENTS.TASK_HYDRATE,
851
- // expect.any(Function)
852
- // );
853
- // expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
854
- expect(result).toEqual(response);
855
- expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
856
- METRIC_EVENT_NAMES.STATION_LOGOUT_SUCCESS,
857
- METRIC_EVENT_NAMES.STATION_LOGOUT_FAILED,
858
- ]);
1076
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
1077
+ expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
859
1078
  });
860
1079
 
861
- it('should handle error during stationLogout', async () => {
862
- const data = {logoutReason: 'Logout reason'};
863
- const error = {
864
- details: {
865
- trackingId: '1234',
866
- data: {
867
- reason: 'Error while performing station logout',
868
- },
869
- },
1080
+ it('should rollback wxApp config when post-station-login init fails', async () => {
1081
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1082
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1083
+ webex.cc.agentConfig = {
1084
+ agentId: 'agentId',
1085
+ webRtcEnabled: false,
1086
+ loginVoiceOptions: ['EXTENSION'],
870
1087
  };
1088
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1089
+ mockTaskManager.applyEnableWxBetterTogether(true);
871
1090
 
872
- jest.spyOn(webex.cc.services.agent, 'logout').mockRejectedValue(error);
1091
+ jest
1092
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
1093
+ .mockRejectedValue(new Error('usersub publish failed'));
1094
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
873
1095
 
874
- await expect(webex.cc.stationLogout(data)).rejects.toThrow(error.details.data.reason);
1096
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1097
+ data: {
1098
+ agentId: 'agentId',
1099
+ teamId: 'teamId',
1100
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1101
+ },
1102
+ trackingId: 'track-1',
1103
+ } as StationLoginSuccess);
875
1104
 
876
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
877
- module: CC_FILE,
878
- method: 'stationLogout',
1105
+ await webex.cc.stationLogin({
1106
+ teamId: 'teamId',
1107
+ loginOption: LoginOption.EXTENSION,
1108
+ dialNumber: '1001',
879
1109
  });
880
- expect(LoggerProxy.error).toHaveBeenCalledWith(
881
- `stationLogout failed with reason: ${error.details.data.reason}`,
882
- {module: CC_FILE, method: 'stationLogout', trackingId: error.details.trackingId}
883
- );
1110
+
1111
+ expect(unsubscribeSpy).toHaveBeenCalled();
1112
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1113
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
884
1114
  });
885
- });
886
1115
 
887
- describe('setAgentStatus', () => {
888
- it('should set agent status successfully when status is Available', async () => {
889
- const expectedPayload = {
890
- state: 'Available',
891
- auxCodeId: '0',
892
- agentId: '123',
893
- lastStateChangeReason: 'Agent is available',
1116
+ it('should reset wxApp config when Mercury fails before usersub publish on station login', async () => {
1117
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1118
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1119
+ webex.cc.agentConfig = {
1120
+ agentId: 'agentId',
1121
+ webRtcEnabled: false,
1122
+ loginVoiceOptions: ['EXTENSION'],
894
1123
  };
1124
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1125
+ mockTaskManager.applyEnableWxBetterTogether(true);
1126
+
1127
+ const publishSpy = jest
1128
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1129
+ .mockResolvedValue(undefined);
1130
+ jest
1131
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
1132
+ .mockRejectedValue(new Error('mercury connect failed'));
1133
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1134
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1135
+ jest
1136
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1137
+ .mockResolvedValue(undefined);
1138
+
1139
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1140
+ data: {
1141
+ agentId: 'agentId',
1142
+ teamId: 'teamId',
1143
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1144
+ },
1145
+ trackingId: 'track-1',
1146
+ } as StationLoginSuccess);
895
1147
 
896
- const setAgentStatusMock = jest
897
- .spyOn(webex.cc.services.agent, 'stateChange')
898
- .mockResolvedValue({data: expectedPayload});
1148
+ await webex.cc.stationLogin({
1149
+ teamId: 'teamId',
1150
+ loginOption: LoginOption.EXTENSION,
1151
+ dialNumber: '1001',
1152
+ });
899
1153
 
900
- const result = await webex.cc.setAgentState(expectedPayload);
1154
+ expect(publishSpy).not.toHaveBeenCalled();
1155
+ expect(teardownSpy).not.toHaveBeenCalled();
1156
+ expect(unsubscribeSpy).toHaveBeenCalled();
1157
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1158
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
1159
+ });
901
1160
 
902
- // Verify logging calls
903
- expect(LoggerProxy.info).toHaveBeenCalledWith('Setting agent state', {
904
- module: CC_FILE,
905
- method: 'setAgentState',
1161
+ it('should register device before publishing usersub true on extension station login', async () => {
1162
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1163
+ webex.internal.device = {
1164
+ userId: 'user-123',
1165
+ registered: false,
1166
+ register: jest.fn().mockImplementation(async function (this: {
1167
+ url?: string;
1168
+ registered?: boolean;
1169
+ }) {
1170
+ this.url = 'https://wdm.example.com/devices/dev-1';
1171
+ this.registered = true;
1172
+ }),
1173
+ };
1174
+ webex.internal.mercury = {
1175
+ connected: false,
1176
+ connect: jest.fn().mockResolvedValue(undefined),
1177
+ };
1178
+ webex.cc.agentConfig = {
1179
+ agentId: 'agentId',
1180
+ webRtcEnabled: false,
1181
+ loginVoiceOptions: ['EXTENSION'],
1182
+ };
1183
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1184
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1185
+
1186
+ const callOrder: string[] = [];
1187
+ jest.spyOn(webex.internal.device, 'register').mockImplementation(async function (this: {
1188
+ url?: string;
1189
+ registered?: boolean;
1190
+ }) {
1191
+ callOrder.push('device.register');
1192
+ this.url = 'https://wdm.example.com/devices/dev-1';
1193
+ this.registered = true;
1194
+ });
1195
+ jest
1196
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1197
+ .mockImplementation(async (enable: boolean) => {
1198
+ if (enable === true && !webex.internal.device.url) {
1199
+ throw new Error('Device URL is unavailable for cross-client publish');
1200
+ }
1201
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
1202
+ });
1203
+
1204
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1205
+ data: {
1206
+ agentId: 'agentId',
1207
+ teamId: 'teamId',
1208
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1209
+ },
1210
+ trackingId: 'track-1',
1211
+ } as StationLoginSuccess);
1212
+
1213
+ await webex.cc.stationLogin({
1214
+ teamId: 'teamId',
1215
+ loginOption: LoginOption.EXTENSION,
1216
+ dialNumber: '1001',
1217
+ });
1218
+
1219
+ expect(callOrder).toEqual(['device.register', 'publish:true']);
1220
+ });
1221
+
1222
+ it('should register device before publishing forced usersub false when wxApp is disabled at station login', async () => {
1223
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1224
+ webex.internal.device = {
1225
+ userId: 'user-123',
1226
+ registered: false,
1227
+ register: jest.fn().mockImplementation(async function (this: {
1228
+ url?: string;
1229
+ registered?: boolean;
1230
+ }) {
1231
+ this.url = 'https://wdm.example.com/devices/dev-1';
1232
+ this.registered = true;
1233
+ }),
1234
+ };
1235
+ webex.internal.mercury = {
1236
+ connected: false,
1237
+ connect: jest.fn().mockResolvedValue(undefined),
1238
+ };
1239
+ webex.cc.agentConfig = {
1240
+ agentId: 'agentId',
1241
+ webRtcEnabled: false,
1242
+ loginVoiceOptions: ['EXTENSION'],
1243
+ };
1244
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1245
+ jest
1246
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1247
+ .mockResolvedValue(undefined);
1248
+
1249
+ const callOrder: string[] = [];
1250
+ jest.spyOn(webex.internal.device, 'register').mockImplementation(async function (this: {
1251
+ url?: string;
1252
+ registered?: boolean;
1253
+ }) {
1254
+ callOrder.push('device.register');
1255
+ this.url = 'https://wdm.example.com/devices/dev-1';
1256
+ this.registered = true;
1257
+ });
1258
+ jest
1259
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1260
+ .mockImplementation(async (enable: boolean) => {
1261
+ if (!webex.internal.device.url) {
1262
+ throw new Error('Device URL is unavailable for cross-client publish');
1263
+ }
1264
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
1265
+ });
1266
+
1267
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1268
+ data: {
1269
+ agentId: 'agentId',
1270
+ teamId: 'teamId',
1271
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1272
+ },
1273
+ trackingId: 'track-1',
1274
+ } as StationLoginSuccess);
1275
+
1276
+ await webex.cc.stationLogin({
1277
+ teamId: 'teamId',
1278
+ loginOption: LoginOption.EXTENSION,
1279
+ dialNumber: '1001',
1280
+ });
1281
+
1282
+ expect(callOrder).toEqual(['device.register', 'publish:false']);
1283
+ });
1284
+
1285
+ it('should retry forced false publish when wxApp is disabled at station login', async () => {
1286
+ jest.useFakeTimers();
1287
+ try {
1288
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1289
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1290
+ webex.cc.agentConfig = {
1291
+ agentId: 'agentId',
1292
+ webRtcEnabled: false,
1293
+ loginVoiceOptions: ['EXTENSION'],
1294
+ };
1295
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1296
+
1297
+ const publishSpy = jest
1298
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1299
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
1300
+ .mockResolvedValue(undefined);
1301
+ const scheduleRetrySpy = jest.spyOn(
1302
+ webex.cc as any,
1303
+ 'scheduleForcedFalsePublishRetry'
1304
+ );
1305
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1306
+ jest
1307
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1308
+ .mockResolvedValue(undefined);
1309
+
1310
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1311
+ data: {
1312
+ agentId: 'agentId',
1313
+ teamId: 'teamId',
1314
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1315
+ },
1316
+ trackingId: 'track-1',
1317
+ } as StationLoginSuccess);
1318
+
1319
+ await webex.cc.stationLogin({
1320
+ teamId: 'teamId',
1321
+ loginOption: LoginOption.EXTENSION,
1322
+ dialNumber: '1001',
1323
+ });
1324
+
1325
+ expect(publishSpy).toHaveBeenCalledTimes(1);
1326
+ expect(scheduleRetrySpy).toHaveBeenCalled();
1327
+ expect(teardownSpy).not.toHaveBeenCalled();
1328
+
1329
+ jest.advanceTimersByTime(30_000);
1330
+ await publishSpy.mock.results[1]?.value;
1331
+
1332
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1333
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123'});
1334
+ } finally {
1335
+ jest.useRealTimers();
1336
+ }
1337
+ });
1338
+
1339
+ it('should not publish usersub on BROWSER station login even when enableWxBetterTogether is true at init', async () => {
1340
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1341
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1342
+ webex.cc.agentConfig = {
1343
+ agentId: 'agentId',
1344
+ webRtcEnabled: true,
1345
+ loginVoiceOptions: ['BROWSER'],
1346
+ };
1347
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
1348
+
1349
+ const publishSpy = jest.spyOn(
1350
+ webex.cc['webexCrossClientService'],
1351
+ 'setManageWebexCallingInWxcc'
1352
+ );
1353
+
1354
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1355
+ data: {
1356
+ agentId: 'agentId',
1357
+ teamId: 'teamId',
1358
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1359
+ },
1360
+ trackingId: 'track-1',
1361
+ } as StationLoginSuccess);
1362
+
1363
+ await webex.cc.stationLogin({
1364
+ teamId: 'teamId',
1365
+ loginOption: LoginOption.BROWSER,
1366
+ });
1367
+
1368
+ expect(publishSpy).not.toHaveBeenCalled();
1369
+ });
1370
+
1371
+ it('should force-publish usersub false on station login when enableWxBetterTogether is false at init', async () => {
1372
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1373
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1374
+ webex.cc.agentConfig = {
1375
+ agentId: 'agentId',
1376
+ webRtcEnabled: false,
1377
+ loginVoiceOptions: ['EXTENSION'],
1378
+ };
1379
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1380
+
1381
+ const publishSpy = jest
1382
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1383
+ .mockResolvedValue(undefined);
1384
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1385
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1386
+
1387
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1388
+ data: {
1389
+ agentId: 'agentId',
1390
+ teamId: 'teamId',
1391
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1392
+ },
1393
+ trackingId: 'track-1',
1394
+ } as StationLoginSuccess);
1395
+
1396
+ await webex.cc.stationLogin({
1397
+ teamId: 'teamId',
1398
+ loginOption: LoginOption.EXTENSION,
1399
+ dialNumber: '1001',
1400
+ });
1401
+
1402
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
1403
+ expect(teardownSpy).toHaveBeenCalled();
1404
+ expect(unsubscribeSpy).toHaveBeenCalled();
1405
+ });
1406
+
1407
+ it('should clear stale usersub on login after prior session enabled wxApp then hard refresh', async () => {
1408
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1409
+ webex.cc.agentConfig = {
1410
+ agentId: 'agentId',
1411
+ webRtcEnabled: false,
1412
+ loginVoiceOptions: ['EXTENSION'],
1413
+ };
1414
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1415
+ webex.request = jest.fn().mockResolvedValue({body: {}});
1416
+
1417
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1418
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1419
+ data: {
1420
+ agentId: 'agentId',
1421
+ teamId: 'teamId',
1422
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1423
+ },
1424
+ trackingId: 'track-1',
1425
+ } as StationLoginSuccess);
1426
+
1427
+ await webex.cc.stationLogin({
1428
+ teamId: 'teamId',
1429
+ loginOption: LoginOption.EXTENSION,
1430
+ dialNumber: '1001',
1431
+ });
1432
+
1433
+ webex.cc['webexCrossClientService'].teardown();
1434
+ webex.cc['wxAppTelephonyMercurySync'].unsubscribe();
1435
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1436
+
1437
+ const publishSpy = jest
1438
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1439
+ .mockResolvedValue(undefined);
1440
+
1441
+ await webex.cc.stationLogin({
1442
+ teamId: 'teamId',
1443
+ loginOption: LoginOption.EXTENSION,
1444
+ dialNumber: '1001',
1445
+ });
1446
+
1447
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
1448
+ });
1449
+ });
1450
+
1451
+ describe('stationLogout', () => {
1452
+ it('should logout successfully', async () => {
1453
+ const data = {logoutReason: 'Logout reason'};
1454
+ const response = {};
1455
+
1456
+ const stationLogoutMock = jest
1457
+ .spyOn(webex.cc.services.agent, 'logout')
1458
+ .mockResolvedValue({} as StationLogoutResponse);
1459
+
1460
+ const result = await webex.cc.stationLogout(data);
1461
+
1462
+ // Verify logging calls
1463
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
1464
+ module: CC_FILE,
1465
+ method: 'stationLogout',
1466
+ });
1467
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Agent station logout completed successfully', {
1468
+ module: CC_FILE,
1469
+ method: 'stationLogout',
1470
+ });
1471
+
1472
+ expect(stationLogoutMock).toHaveBeenCalledWith({data: data});
1473
+ // TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
1474
+ // expect(mockTaskManager.unregisterIncomingCallEvent).toHaveBeenCalledWith();
1475
+ // expect(mockTaskManager.off).toHaveBeenCalledWith(
1476
+ // TASK_EVENTS.TASK_INCOMING,
1477
+ // expect.any(Function)
1478
+ // );
1479
+ // expect(mockTaskManager.off).toHaveBeenCalledWith(
1480
+ // TASK_EVENTS.TASK_HYDRATE,
1481
+ // expect.any(Function)
1482
+ // );
1483
+ // expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1484
+ expect(result).toEqual(response);
1485
+ expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
1486
+ METRIC_EVENT_NAMES.STATION_LOGOUT_SUCCESS,
1487
+ METRIC_EVENT_NAMES.STATION_LOGOUT_FAILED,
1488
+ ]);
1489
+ });
1490
+
1491
+ it('should not publish usersub false on logout when wxApp was never enabled', async () => {
1492
+ webex.internal.device = {userId: 'user-123'};
1493
+ const publishSpy = jest
1494
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1495
+ .mockResolvedValue(undefined);
1496
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1497
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1498
+
1499
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1500
+ trackingId: 'track-1',
1501
+ } as StationLogoutResponse);
1502
+
1503
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1504
+
1505
+ expect(publishSpy).not.toHaveBeenCalled();
1506
+ expect(teardownSpy).toHaveBeenCalled();
1507
+ expect(unsubscribeSpy).toHaveBeenCalled();
1508
+ });
1509
+
1510
+ it('should publish usersub false and teardown wxApp sync on logout when wxApp was enabled', async () => {
1511
+ webex.internal.device = {
1512
+ userId: 'user-123',
1513
+ url: 'https://wdm.example.com/devices/dev-1',
1514
+ registered: false,
1515
+ register: jest.fn().mockResolvedValue(undefined),
1516
+ unregister: jest.fn().mockResolvedValue(undefined),
1517
+ };
1518
+ webex.internal.mercury = {
1519
+ connected: false,
1520
+ connect: jest.fn().mockResolvedValue(undefined),
1521
+ disconnect: jest.fn().mockResolvedValue(undefined),
1522
+ };
1523
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1524
+ webex.request = jest.fn().mockResolvedValue({body: {}});
1525
+ webex.cc['agentConfig'] = {
1526
+ agentId: 'agent-123',
1527
+ deviceType: LoginOption.EXTENSION,
1528
+ } as Profile;
1529
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1530
+
1531
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
1532
+
1533
+ const publishSpy = jest
1534
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1535
+ .mockResolvedValue(undefined);
1536
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1537
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1538
+
1539
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1540
+ trackingId: 'track-1',
1541
+ } as StationLogoutResponse);
1542
+
1543
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1544
+
1545
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
1546
+ expect(teardownSpy).toHaveBeenCalled();
1547
+ expect(unsubscribeSpy).toHaveBeenCalled();
1548
+ });
1549
+
1550
+ it('should teardown wxApp state on logout even when usersub publish fails', async () => {
1551
+ webex.internal.device = {userId: 'user-123'};
1552
+ webex.cc['agentConfig'] = {
1553
+ agentId: 'agent-123',
1554
+ deviceType: LoginOption.EXTENSION,
1555
+ } as Profile;
1556
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1557
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1558
+ mockTaskManager.applyEnableWxBetterTogether(true);
1559
+
1560
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1561
+ trackingId: 'track-1',
1562
+ } as StationLogoutResponse);
1563
+ jest
1564
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
1565
+ .mockRejectedValue(new Error('usersub publish failed'));
1566
+ const scheduleRetrySpy = jest.spyOn(
1567
+ webex.cc as any,
1568
+ 'scheduleCompensatingFalsePublishRetry'
1569
+ );
1570
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1571
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1572
+
1573
+ const result = await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1574
+
1575
+ expect(result).toEqual({trackingId: 'track-1'});
1576
+ expect(teardownSpy).toHaveBeenCalled();
1577
+ expect(unsubscribeSpy).toHaveBeenCalled();
1578
+ expect(scheduleRetrySpy).toHaveBeenCalled();
1579
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
1580
+ });
1581
+
1582
+ it('should retry compensating false publish when teardown usersub publish fails on logout', async () => {
1583
+ jest.useFakeTimers();
1584
+ try {
1585
+ webex.internal.device = {
1586
+ userId: 'user-123',
1587
+ url: 'https://wdm.example.com/devices/dev-1',
1588
+ };
1589
+ webex.cc['agentConfig'] = {
1590
+ agentId: 'agent-123',
1591
+ deviceType: LoginOption.EXTENSION,
1592
+ } as Profile;
1593
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1594
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1595
+ mockTaskManager.applyEnableWxBetterTogether(true);
1596
+ webex.cc['webexCrossClientService'].answerCallsState = true;
1597
+
1598
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1599
+ trackingId: 'track-1',
1600
+ } as StationLogoutResponse);
1601
+
1602
+ const publishSpy = jest
1603
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1604
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
1605
+ .mockResolvedValue(undefined);
1606
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1607
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1608
+ jest
1609
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1610
+ .mockResolvedValue(undefined);
1611
+
1612
+ const result = await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1613
+
1614
+ expect(result).toEqual({trackingId: 'track-1'});
1615
+ expect(publishSpy).toHaveBeenCalledTimes(1);
1616
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
1617
+ expect(teardownSpy).toHaveBeenCalled();
1618
+ expect(unsubscribeSpy).toHaveBeenCalled();
1619
+
1620
+ jest.advanceTimersByTime(30_000);
1621
+ await publishSpy.mock.results[1]?.value;
1622
+
1623
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1624
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123'});
1625
+ } finally {
1626
+ jest.useRealTimers();
1627
+ }
1628
+ });
1629
+
1630
+ it('should cancel compensating retry when station login succeeds before retry fires', async () => {
1631
+ jest.useFakeTimers();
1632
+ try {
1633
+ webex.internal.device = {
1634
+ userId: 'user-123',
1635
+ url: 'https://wdm.example.com/devices/dev-1',
1636
+ registered: false,
1637
+ register: jest.fn().mockResolvedValue(undefined),
1638
+ unregister: jest.fn().mockResolvedValue(undefined),
1639
+ };
1640
+ webex.internal.mercury = {
1641
+ connected: false,
1642
+ connect: jest.fn().mockResolvedValue(undefined),
1643
+ disconnect: jest.fn().mockResolvedValue(undefined),
1644
+ };
1645
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1646
+ webex.cc['agentConfig'] = {
1647
+ agentId: 'agent-123',
1648
+ deviceType: LoginOption.EXTENSION,
1649
+ webRtcEnabled: false,
1650
+ loginVoiceOptions: ['EXTENSION'],
1651
+ } as Profile;
1652
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1653
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1654
+ mockTaskManager.applyEnableWxBetterTogether(true);
1655
+ webex.cc['webexCrossClientService'].answerCallsState = true;
1656
+
1657
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1658
+ trackingId: 'track-1',
1659
+ } as StationLogoutResponse);
1660
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1661
+ data: {
1662
+ agentId: 'agent-123',
1663
+ teamId: 'team-123',
1664
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1665
+ },
1666
+ trackingId: 'track-2',
1667
+ } as StationLoginSuccess);
1668
+
1669
+ const publishSpy = jest
1670
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1671
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
1672
+ .mockResolvedValue(undefined);
1673
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1674
+ jest
1675
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1676
+ .mockResolvedValue(undefined);
1677
+
1678
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1679
+ await webex.cc.stationLogin({
1680
+ teamId: 'team-123',
1681
+ loginOption: LoginOption.EXTENSION,
1682
+ dialNumber: '1001',
1683
+ });
1684
+
1685
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1686
+ expect(publishSpy).toHaveBeenNthCalledWith(1, false, {userId: 'user-123'});
1687
+ expect(publishSpy).toHaveBeenNthCalledWith(2, true, {userId: 'user-123'});
1688
+ expect(teardownSpy).toHaveBeenCalledTimes(1);
1689
+
1690
+ jest.advanceTimersByTime(30_000);
1691
+
1692
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1693
+ expect(teardownSpy).toHaveBeenCalledTimes(1);
1694
+ } finally {
1695
+ jest.useRealTimers();
1696
+ }
1697
+ });
1698
+
1699
+ it('should handle error during stationLogout', async () => {
1700
+ const data = {logoutReason: 'Logout reason'};
1701
+ const error = {
1702
+ details: {
1703
+ trackingId: '1234',
1704
+ data: {
1705
+ reason: 'Error while performing station logout',
1706
+ },
1707
+ },
1708
+ };
1709
+
1710
+ jest.spyOn(webex.cc.services.agent, 'logout').mockRejectedValue(error);
1711
+
1712
+ await expect(webex.cc.stationLogout(data)).rejects.toThrow(error.details.data.reason);
1713
+
1714
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
1715
+ module: CC_FILE,
1716
+ method: 'stationLogout',
1717
+ });
1718
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
1719
+ `stationLogout failed with reason: ${error.details.data.reason}`,
1720
+ {module: CC_FILE, method: 'stationLogout', trackingId: error.details.trackingId}
1721
+ );
1722
+ });
1723
+ });
1724
+
1725
+ describe('setAgentStatus', () => {
1726
+ it('should set agent status successfully when status is Available', async () => {
1727
+ const expectedPayload = {
1728
+ state: 'Available',
1729
+ auxCodeId: '0',
1730
+ agentId: '123',
1731
+ lastStateChangeReason: 'Agent is available',
1732
+ };
1733
+
1734
+ const setAgentStatusMock = jest
1735
+ .spyOn(webex.cc.services.agent, 'stateChange')
1736
+ .mockResolvedValue({data: expectedPayload});
1737
+
1738
+ const result = await webex.cc.setAgentState(expectedPayload);
1739
+
1740
+ // Verify logging calls
1741
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Setting agent state', {
1742
+ module: CC_FILE,
1743
+ method: 'setAgentState',
906
1744
  });
907
1745
  expect(LoggerProxy.log).toHaveBeenCalledWith(
908
1746
  `Agent state changed successfully to auxCodeId: ${expectedPayload.auxCodeId}`,
@@ -1073,6 +1911,41 @@ describe('webex.cc', () => {
1073
1911
  ]);
1074
1912
  });
1075
1913
 
1914
+ it('should map Transfer action to an Available backend state filter', async () => {
1915
+ webex.cc.agentConfig = {agentProfileID: 'test-agent-profile-id'};
1916
+ const buddyAgentsResponse = {data: {agentList: []}};
1917
+ const buddyAgentsSpy = jest
1918
+ .spyOn(webex.cc.services.agent, 'buddyAgents')
1919
+ .mockResolvedValue(buddyAgentsResponse);
1920
+
1921
+ await webex.cc.getBuddyAgents({action: 'Transfer', mediaType: 'chat'});
1922
+
1923
+ expect(buddyAgentsSpy).toHaveBeenCalledWith({
1924
+ data: {
1925
+ agentProfileId: 'test-agent-profile-id',
1926
+ mediaType: 'chat',
1927
+ state: 'Available',
1928
+ },
1929
+ });
1930
+ });
1931
+
1932
+ it('should omit the backend state filter for Consult and default media type to telephony', async () => {
1933
+ webex.cc.agentConfig = {agentProfileID: 'test-agent-profile-id'};
1934
+ const buddyAgentsResponse = {data: {agentList: []}};
1935
+ const buddyAgentsSpy = jest
1936
+ .spyOn(webex.cc.services.agent, 'buddyAgents')
1937
+ .mockResolvedValue(buddyAgentsResponse);
1938
+
1939
+ await webex.cc.getBuddyAgents({action: 'Consult'});
1940
+
1941
+ expect(buddyAgentsSpy).toHaveBeenCalledWith({
1942
+ data: {
1943
+ agentProfileId: 'test-agent-profile-id',
1944
+ mediaType: 'telephony',
1945
+ },
1946
+ });
1947
+ });
1948
+
1076
1949
  it('should handle error', async () => {
1077
1950
  const data: BuddyAgents = {state: 'Available', mediaType: 'telephony'};
1078
1951
  webex.cc.agentConfig = {
@@ -1149,11 +2022,11 @@ describe('webex.cc', () => {
1149
2022
  const webSocketManagerOnSpy = jest.spyOn(webex.cc.services.webSocketManager, 'on');
1150
2023
  await webex.cc['silentRelogin']();
1151
2024
 
1152
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2025
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1153
2026
  module: CC_FILE,
1154
2027
  method: 'silentRelogin',
1155
2028
  });
1156
- expect(LoggerProxy.info).toHaveBeenCalledWith(
2029
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
1157
2030
  'event=requestAutoStateChange | Requesting state change to available on socket reconnect',
1158
2031
  {module: CC_FILE, method: 'silentRelogin'}
1159
2032
  );
@@ -1198,7 +2071,7 @@ describe('webex.cc', () => {
1198
2071
 
1199
2072
  jest.spyOn(webex.cc.services.agent, 'reload').mockRejectedValue(error);
1200
2073
  await webex.cc['silentRelogin']();
1201
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2074
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1202
2075
  module: CC_FILE,
1203
2076
  method: 'silentRelogin',
1204
2077
  });
@@ -1213,7 +2086,7 @@ describe('webex.cc', () => {
1213
2086
  jest.spyOn(webex.cc.services.agent, 'reload').mockRejectedValue(error);
1214
2087
 
1215
2088
  await expect(webex.cc['silentRelogin']()).rejects.toThrow(error);
1216
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2089
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1217
2090
  module: CC_FILE,
1218
2091
  method: 'silentRelogin',
1219
2092
  });
@@ -1255,7 +2128,7 @@ describe('webex.cc', () => {
1255
2128
 
1256
2129
  await webex.cc['silentRelogin']();
1257
2130
 
1258
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2131
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1259
2132
  module: CC_FILE,
1260
2133
  method: 'silentRelogin',
1261
2134
  });
@@ -1300,6 +2173,81 @@ describe('webex.cc', () => {
1300
2173
  expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.AGENT_DN);
1301
2174
  expect(webex.cc.agentConfig.dn).toBe('67890');
1302
2175
  });
2176
+
2177
+ it('should publish usersub true on silent relogin when enableWxBetterTogether is true at init', async () => {
2178
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2179
+ webex.internal.device = {
2180
+ userId: 'user-123',
2181
+ url: 'https://wdm.example.com/devices/dev-1',
2182
+ registered: true,
2183
+ register: jest.fn().mockResolvedValue(undefined),
2184
+ };
2185
+ webex.internal.mercury.connected = true;
2186
+ webex.internal.mercury.connect = jest.fn().mockResolvedValue(undefined);
2187
+ webex.cc.agentConfig = {
2188
+ agentId: 'agentId',
2189
+ agentProfileID: 'test-agent-profile-id',
2190
+ isAgentLoggedIn: false,
2191
+ } as Profile;
2192
+
2193
+ jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
2194
+ data: {
2195
+ auxCodeId: 'auxCodeId',
2196
+ agentId: 'agentId',
2197
+ deviceType: LoginOption.EXTENSION,
2198
+ dn: '1001',
2199
+ teamId: 'teamId',
2200
+ lastStateChangeTimestamp: 1738575135188,
2201
+ lastIdleCodeChangeTimestamp: 1738575135189,
2202
+ },
2203
+ });
2204
+
2205
+ const publishSpy = jest
2206
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2207
+ .mockResolvedValue(undefined);
2208
+ const subscribeSpy = jest
2209
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
2210
+ .mockImplementation(() => {});
2211
+
2212
+ await webex.cc['silentRelogin']();
2213
+
2214
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
2215
+ expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
2216
+ });
2217
+
2218
+ it('should force-publish usersub false on silent relogin when enableWxBetterTogether is false at init', async () => {
2219
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
2220
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
2221
+ webex.cc.agentConfig = {
2222
+ agentId: 'agentId',
2223
+ agentProfileID: 'test-agent-profile-id',
2224
+ isAgentLoggedIn: false,
2225
+ } as Profile;
2226
+
2227
+ jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
2228
+ data: {
2229
+ auxCodeId: 'auxCodeId',
2230
+ agentId: 'agentId',
2231
+ deviceType: LoginOption.EXTENSION,
2232
+ dn: '1001',
2233
+ teamId: 'teamId',
2234
+ lastStateChangeTimestamp: 1738575135188,
2235
+ lastIdleCodeChangeTimestamp: 1738575135189,
2236
+ },
2237
+ });
2238
+
2239
+ const publishSpy = jest
2240
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2241
+ .mockResolvedValue(undefined);
2242
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
2243
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
2244
+
2245
+ await webex.cc['silentRelogin']();
2246
+
2247
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
2248
+ expect(teardownSpy).toHaveBeenCalled();
2249
+ expect(unsubscribeSpy).toHaveBeenCalled();
2250
+ });
1303
2251
  });
1304
2252
 
1305
2253
  describe('setupEventListeners()', () => {
@@ -1439,6 +2387,27 @@ describe('webex.cc', () => {
1439
2387
  });
1440
2388
  });
1441
2389
 
2390
+ describe('getQueues', () => {
2391
+ it('delegates to the queue service when successful', async () => {
2392
+ const mockQueuesResponse = [{queueId: 'queue1', queueName: 'Queue 1'}];
2393
+ const queueSpy = jest
2394
+ .spyOn(webex.cc.queue, 'getQueues')
2395
+ .mockResolvedValue(mockQueuesResponse as any);
2396
+
2397
+ const result = await webex.cc.getQueues({page: 1});
2398
+
2399
+ expect(queueSpy).toHaveBeenCalledWith({page: 1});
2400
+ expect(result).toBe(mockQueuesResponse);
2401
+ });
2402
+
2403
+ it('propagates queue service errors', async () => {
2404
+ const error = new Error('Test error.');
2405
+ jest.spyOn(webex.cc.queue, 'getQueues').mockRejectedValue(error);
2406
+
2407
+ await expect(webex.cc.getQueues()).rejects.toThrow('Test error.');
2408
+ });
2409
+ });
2410
+
1442
2411
  describe('uploadLogs', () => {
1443
2412
  it('should upload logs successfully', async () => {
1444
2413
  const uploadLogsMock = jest.spyOn(webex.cc.webexRequest, 'uploadLogs').mockResolvedValue({
@@ -1468,6 +2437,7 @@ describe('webex.cc', () => {
1468
2437
 
1469
2438
  describe('unregister', () => {
1470
2439
  let mockWebSocketManager;
2440
+ let mockRTDWebSocketManager;
1471
2441
  let mercuryDisconnectSpy;
1472
2442
  let deviceUnregisterSpy;
1473
2443
 
@@ -1484,8 +2454,15 @@ describe('webex.cc', () => {
1484
2454
  off: jest.fn(),
1485
2455
  on: jest.fn(),
1486
2456
  };
2457
+ mockRTDWebSocketManager = {
2458
+ isSocketClosed: false,
2459
+ close: jest.fn(),
2460
+ off: jest.fn(),
2461
+ on: jest.fn(),
2462
+ };
1487
2463
 
1488
2464
  webex.cc.services.webSocketManager = mockWebSocketManager;
2465
+ webex.cc.services.rtdWebSocketManager = mockRTDWebSocketManager;
1489
2466
 
1490
2467
  webex.internal = webex.internal || {};
1491
2468
  webex.internal.mercury = {
@@ -1502,6 +2479,25 @@ describe('webex.cc', () => {
1502
2479
  });
1503
2480
 
1504
2481
  it('should unregister successfully and clean up all resources when webrtc is enabled', async () => {
2482
+ webex.cc.services.rtdWebSocketManager = {
2483
+ isSocketClosed: false,
2484
+ close: jest.fn(),
2485
+ off: jest.fn(),
2486
+ on: jest.fn(),
2487
+ } as any;
2488
+
2489
+ webex.internal.device = {
2490
+ ...webex.internal.device,
2491
+ userId: 'user-123',
2492
+ unregister: jest.fn().mockResolvedValue(undefined),
2493
+ };
2494
+ deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
2495
+ const publishSpy = jest
2496
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2497
+ .mockResolvedValue(undefined);
2498
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
2499
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
2500
+
1505
2501
  await webex.cc.deregister();
1506
2502
 
1507
2503
  expect(mockTaskManager.off).toHaveBeenCalledWith(
@@ -1512,13 +2508,25 @@ describe('webex.cc', () => {
1512
2508
  TASK_EVENTS.TASK_HYDRATE,
1513
2509
  expect.any(Function)
1514
2510
  );
2511
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
2512
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
2513
+ expect.any(Function)
2514
+ );
1515
2515
  expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
2516
+ expect(webex.cc.services.rtdWebSocketManager.off).toHaveBeenCalledWith(
2517
+ 'message',
2518
+ expect.any(Function)
2519
+ );
1516
2520
  expect(webex.cc.services.connectionService.off).toHaveBeenCalledWith(
1517
2521
  'connectionLost',
1518
2522
  expect.any(Function)
1519
2523
  );
1520
2524
 
1521
2525
  expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
2526
+ expect(webex.cc.services.rtdWebSocketManager.close).toHaveBeenCalledWith(
2527
+ false,
2528
+ 'Unregistering the RTD websocket'
2529
+ );
1522
2530
  expect(webex.cc.agentConfig).toBeNull();
1523
2531
 
1524
2532
  expect(webex.internal.mercury.off).toHaveBeenCalledWith('online');
@@ -1560,6 +2568,13 @@ describe('webex.cc', () => {
1560
2568
  const [, hydrateCallback] = hydrateCalls[0];
1561
2569
  expect(hydrateCallback).toBe(webex.cc['handleTaskHydrate']);
1562
2570
 
2571
+ const multiLoginHydrateCalls = mockTaskManager.off.mock.calls.filter(
2572
+ ([evt]) => evt === TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE
2573
+ );
2574
+ expect(multiLoginHydrateCalls).toHaveLength(1);
2575
+ const [, multiLoginHydrateCallback] = multiLoginHydrateCalls[0];
2576
+ expect(multiLoginHydrateCallback).toBe(webex.cc['handleTaskMultiLoginHydrate']);
2577
+
1563
2578
  const messageCalls = mockWebSocketManager.off.mock.calls.filter(([evt]) => evt === 'message');
1564
2579
  expect(messageCalls).toHaveLength(1);
1565
2580
  const [, messageCallback] = messageCalls[0];
@@ -1571,6 +2586,39 @@ describe('webex.cc', () => {
1571
2586
  expect(connectionCalls).toHaveLength(1);
1572
2587
  const [, connectionCallback] = connectionCalls[0];
1573
2588
  expect(connectionCallback).toBe(webex.cc['handleConnectionLost']);
2589
+
2590
+ expect(publishSpy).not.toHaveBeenCalled();
2591
+ expect(teardownSpy).toHaveBeenCalled();
2592
+ expect(unsubscribeSpy).toHaveBeenCalled();
2593
+ });
2594
+
2595
+ it('should publish usersub false before device unregister when wxApp is active on deregister', async () => {
2596
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2597
+ webex.cc['webexCrossClientService'].answerCallsState = true;
2598
+ webex.internal.device = {
2599
+ userId: 'user-123',
2600
+ url: 'https://wdm.example.com/devices/dev-1',
2601
+ unregister: jest.fn().mockResolvedValue(undefined),
2602
+ };
2603
+ deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
2604
+
2605
+ const callOrder: string[] = [];
2606
+ jest
2607
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2608
+ .mockImplementation(async (enable: boolean) => {
2609
+ if (enable === false && !webex.internal.device.url) {
2610
+ throw new Error('Device URL is unavailable for cross-client publish');
2611
+ }
2612
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
2613
+ });
2614
+ deviceUnregisterSpy.mockImplementation(async () => {
2615
+ callOrder.push('device.unregister');
2616
+ webex.internal.device.url = undefined;
2617
+ });
2618
+
2619
+ await webex.cc.deregister();
2620
+
2621
+ expect(callOrder).toEqual(['publish:false', 'device.unregister']);
1574
2622
  });
1575
2623
 
1576
2624
  it('should skip webCallingService and internal cleanup when webrtc is disabled', async () => {
@@ -1585,6 +2633,10 @@ describe('webex.cc', () => {
1585
2633
  TASK_EVENTS.TASK_HYDRATE,
1586
2634
  expect.any(Function)
1587
2635
  );
2636
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
2637
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
2638
+ expect.any(Function)
2639
+ );
1588
2640
  expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1589
2641
  expect(webex.cc.services.connectionService.off).toHaveBeenCalledWith(
1590
2642
  'connectionLost',
@@ -1594,24 +2646,155 @@ describe('webex.cc', () => {
1594
2646
  expect(webex.internal.mercury.off).not.toHaveBeenCalled();
1595
2647
  expect(mercuryDisconnectSpy).not.toHaveBeenCalled();
1596
2648
  expect(deviceUnregisterSpy).not.toHaveBeenCalled();
2649
+ expect(mockRTDWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
2650
+ });
2651
+
2652
+ it('should skip internal mercury cleanup when loginVoiceOptions does not include BROWSER', async () => {
2653
+ webex.cc.agentConfig = {
2654
+ agentId: 'agentId',
2655
+ webRtcEnabled: true,
2656
+ loginVoiceOptions: ['EXTENSION'],
2657
+ };
2658
+
2659
+ await webex.cc.deregister();
2660
+
2661
+ // mercury listeners & disconnect should not run
2662
+ expect(webex.internal.mercury.off).not.toHaveBeenCalled();
2663
+ expect(mercuryDisconnectSpy).not.toHaveBeenCalled();
2664
+ expect(deviceUnregisterSpy).not.toHaveBeenCalled();
2665
+
2666
+ expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
2667
+ expect(mockRTDWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
2668
+ expect(webex.cc.agentConfig).toBeNull();
2669
+ });
2670
+
2671
+ it('should teardown wxApp state on deregister even when usersub publish fails', async () => {
2672
+ webex.cc.agentConfig = {
2673
+ agentId: 'agentId',
2674
+ webRtcEnabled: false,
2675
+ loginVoiceOptions: [LoginOption.EXTENSION],
2676
+ };
2677
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2678
+ webex.cc['webexCrossClientService'].answerCallsState = true;
2679
+ webex.internal.device = {
2680
+ userId: 'user-123',
2681
+ unregister: jest.fn().mockResolvedValue(undefined),
2682
+ };
2683
+
2684
+ jest
2685
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
2686
+ .mockRejectedValue(new Error('usersub publish failed'));
2687
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
2688
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
2689
+
2690
+ await expect(webex.cc.deregister()).rejects.toThrow('usersub publish failed');
2691
+
2692
+ expect(teardownSpy).toHaveBeenCalled();
2693
+ expect(unsubscribeSpy).toHaveBeenCalled();
2694
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
2695
+ expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
2696
+ expect(webex.cc.agentConfig).toBeNull();
2697
+ });
2698
+
2699
+ it('should publish forced false during teardown when enable publish is still in flight', async () => {
2700
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2701
+ webex.internal.device = {
2702
+ userId: 'user-123',
2703
+ url: 'https://wdm.example.com/devices/dev-1',
2704
+ };
2705
+
2706
+ let resolveEnable: () => void = () => {};
2707
+ const enableGate = new Promise<void>((resolve) => {
2708
+ resolveEnable = resolve;
2709
+ });
2710
+
2711
+ const publishSpy = jest
2712
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2713
+ .mockImplementation(async (enable: boolean) => {
2714
+ if (enable) {
2715
+ await enableGate;
2716
+ }
2717
+ });
2718
+ jest
2719
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
2720
+ .mockResolvedValue(undefined);
2721
+
2722
+ expect(webex.cc['webexCrossClientService'].answerCallsState).toBe(false);
2723
+
2724
+ const enableInFlight = (webex.cc as any).publishAnswerOnWebexCrossClientState(true);
2725
+
2726
+ await (webex.cc as any).teardownWxAppLocalState();
2727
+
2728
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
2729
+
2730
+ resolveEnable();
2731
+ await enableInFlight;
1597
2732
  });
1598
2733
 
1599
- it('should skip internal mercury cleanup when loginVoiceOptions does not include BROWSER', async () => {
2734
+ it('should release wxApp-owned Mercury and device on deregister', async () => {
1600
2735
  webex.cc.agentConfig = {
1601
2736
  agentId: 'agentId',
1602
- webRtcEnabled: true,
1603
- loginVoiceOptions: ['EXTENSION'],
2737
+ webRtcEnabled: false,
2738
+ loginVoiceOptions: [LoginOption.EXTENSION],
1604
2739
  };
2740
+ webex.cc['wxAppMercuryConnectedByCc'] = true;
2741
+ webex.cc['wxAppDeviceRegisteredByCc'] = true;
2742
+ webex.internal.mercury.connected = true;
2743
+ webex.internal.device.registered = true;
2744
+
2745
+ const mercuryDisconnectSpy = jest
2746
+ .spyOn(webex.internal.mercury, 'disconnect')
2747
+ .mockResolvedValue(undefined);
2748
+ const deviceUnregisterSpy = jest
2749
+ .spyOn(webex.internal.device, 'unregister')
2750
+ .mockResolvedValue(undefined);
1605
2751
 
1606
2752
  await webex.cc.deregister();
1607
2753
 
1608
- // mercury listeners & disconnect should not run
1609
- expect(webex.internal.mercury.off).not.toHaveBeenCalled();
1610
- expect(mercuryDisconnectSpy).not.toHaveBeenCalled();
1611
- expect(deviceUnregisterSpy).not.toHaveBeenCalled();
2754
+ expect(mercuryDisconnectSpy).toHaveBeenCalled();
2755
+ expect(deviceUnregisterSpy).toHaveBeenCalled();
2756
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(false);
2757
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(false);
2758
+ });
1612
2759
 
1613
- expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
1614
- expect(webex.cc.agentConfig).toBeNull();
2760
+ it('should retain wxApp Mercury ownership when disconnect fails and retry cleanup', async () => {
2761
+ webex.cc['wxAppMercuryConnectedByCc'] = true;
2762
+ webex.internal.mercury.connected = true;
2763
+
2764
+ const mercuryDisconnectSpy = jest
2765
+ .spyOn(webex.internal.mercury, 'disconnect')
2766
+ .mockRejectedValueOnce(new Error('disconnect failed'))
2767
+ .mockResolvedValueOnce(undefined);
2768
+
2769
+ await webex.cc['releaseWxAppMercuryResources']();
2770
+
2771
+ expect(mercuryDisconnectSpy).toHaveBeenCalledTimes(1);
2772
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(true);
2773
+
2774
+ await webex.cc['releaseWxAppMercuryResources']();
2775
+
2776
+ expect(mercuryDisconnectSpy).toHaveBeenCalledTimes(2);
2777
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(false);
2778
+ });
2779
+
2780
+ it('should retain wxApp device ownership when unregister fails and retry cleanup', async () => {
2781
+ webex.cc['wxAppDeviceRegisteredByCc'] = true;
2782
+ webex.internal.device.registered = true;
2783
+
2784
+ const deviceUnregisterSpy = jest
2785
+ .spyOn(webex.internal.device, 'unregister')
2786
+ .mockRejectedValueOnce(new Error('unregister failed'))
2787
+ .mockResolvedValueOnce(undefined);
2788
+
2789
+ await webex.cc['releaseWxAppMercuryResources']();
2790
+
2791
+ expect(deviceUnregisterSpy).toHaveBeenCalledTimes(1);
2792
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(true);
2793
+
2794
+ await webex.cc['releaseWxAppMercuryResources']();
2795
+
2796
+ expect(deviceUnregisterSpy).toHaveBeenCalledTimes(2);
2797
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(false);
1615
2798
  });
1616
2799
 
1617
2800
  it('should handle errors during unregister and track metrics', async () => {
@@ -1628,6 +2811,10 @@ describe('webex.cc', () => {
1628
2811
  TASK_EVENTS.TASK_HYDRATE,
1629
2812
  expect.any(Function)
1630
2813
  );
2814
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
2815
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
2816
+ expect.any(Function)
2817
+ );
1631
2818
 
1632
2819
  expect(LoggerProxy.error).toHaveBeenCalledWith(`Error during deregister: ${mockError}`, {
1633
2820
  module: CC_FILE,
@@ -1650,7 +2837,7 @@ describe('webex.cc', () => {
1650
2837
 
1651
2838
  beforeEach(() => {
1652
2839
  emitSpy = jest.spyOn(webex.cc, 'emit');
1653
- messageCallback = mockWebSocketManager.on.mock.calls.find((c) => c[0] === 'message')[1];
2840
+ messageCallback = webex.cc['handleWebsocketMessage'];
1654
2841
  });
1655
2842
 
1656
2843
  it('should emit AGENT_STATION_LOGIN_SUCCESS on CC_EVENTS.AGENT_STATION_LOGIN_SUCCESS with mapped payload', () => {
@@ -1850,7 +3037,7 @@ describe('webex.cc', () => {
1850
3037
  );
1851
3038
 
1852
3039
  expect(webex.cc.stationLogout).toHaveBeenCalledWith({
1853
- logoutReason: 'User requested agent device change',
3040
+ logoutReason: 'User requested agent profile update',
1854
3041
  });
1855
3042
  expect(webex.cc.stationLogin).toHaveBeenCalledWith({
1856
3043
  teamId: 'teamId',
@@ -1860,6 +3047,54 @@ describe('webex.cc', () => {
1860
3047
  expect(result).toEqual(mockResp);
1861
3048
  });
1862
3049
 
3050
+ it('should preserve enableWxBetterTogether init flag across profile update logout/login', async () => {
3051
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3052
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
3053
+ webex.cc.agentConfig = {
3054
+ ...webex.cc.agentConfig,
3055
+ agentId: 'agentId',
3056
+ webRtcEnabled: false,
3057
+ loginVoiceOptions: ['BROWSER', 'EXTENSION'],
3058
+ } as Profile;
3059
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
3060
+ webex.cc.webCallingService.setLoginOption = jest.fn((loginOption: LoginOption) => {
3061
+ webex.cc.webCallingService.loginOption = loginOption;
3062
+ });
3063
+ webex.cc['webexCrossClientService'].answerCallsState = true;
3064
+
3065
+ const publishSpy = jest
3066
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3067
+ .mockResolvedValue(undefined);
3068
+ jest
3069
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
3070
+ .mockImplementation(() => {});
3071
+ jest.spyOn(webex.cc as any, 'releaseWxAppMercuryResources').mockResolvedValue(undefined);
3072
+
3073
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
3074
+ trackingId: 'track-logout',
3075
+ } as StationLogoutResponse);
3076
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
3077
+ data: {
3078
+ agentId: 'agentId',
3079
+ teamId: 'teamId',
3080
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
3081
+ },
3082
+ trackingId: 'track-login',
3083
+ } as StationLoginSuccess);
3084
+
3085
+ webex.internal.mercury.connected = true;
3086
+
3087
+ await webex.cc.updateAgentProfile({
3088
+ teamId: 'teamId',
3089
+ loginOption: LoginOption.EXTENSION,
3090
+ dialNumber: '1001',
3091
+ });
3092
+
3093
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
3094
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
3095
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
3096
+ });
3097
+
1863
3098
  it('should use provided teamId if passed in payload', async () => {
1864
3099
  const dataWithTeam = {
1865
3100
  teamId: 'newTeam',
@@ -1934,24 +3169,26 @@ describe('webex.cc', () => {
1934
3169
  });
1935
3170
  });
1936
3171
 
1937
- it('should throw with detailed error when loginOption equals current device type', async () => {
1938
- webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
3172
+ it('should allow update when loginOption and teamId are unchanged (e.g. dialNumber or profile refresh)', async () => {
3173
+ webex.cc.webCallingService.loginOption = LoginOption.AGENT_DN;
1939
3174
  const data = {
1940
3175
  teamId: 'teamId',
1941
- loginOption: LoginOption.BROWSER,
1942
- dialNumber: '',
1943
- };
1944
- const expectedMessage =
1945
- 'Will not proceed with device update as new Device type is same as current device type and teamId is same as current teamId';
1946
-
1947
- await expect(webex.cc.updateAgentProfile(data)).rejects.toMatchObject({
1948
- message: expectedMessage,
1949
- details: expect.objectContaining({
1950
- data: expect.objectContaining({
1951
- agentId: webex.cc.agentConfig.agentId,
1952
- reason: expectedMessage,
1953
- }),
1954
- }),
3176
+ loginOption: LoginOption.AGENT_DN,
3177
+ dialNumber: '1234',
3178
+ };
3179
+ const logoutSpy = jest.spyOn(webex.cc, 'stationLogout').mockResolvedValue({});
3180
+ const loginSpy = jest.spyOn(webex.cc, 'stationLogin').mockResolvedValue({
3181
+ type: 'AgentDeviceTypeUpdateSuccess',
3182
+ } as any);
3183
+
3184
+ await expect(webex.cc.updateAgentProfile(data)).resolves.toBeDefined();
3185
+ expect(logoutSpy).toHaveBeenCalledWith({
3186
+ logoutReason: 'User requested agent profile update',
3187
+ });
3188
+ expect(loginSpy).toHaveBeenCalledWith({
3189
+ teamId: data.teamId,
3190
+ loginOption: data.loginOption,
3191
+ dialNumber: data.dialNumber,
1955
3192
  });
1956
3193
  });
1957
3194
 
@@ -2104,7 +3341,9 @@ describe('webex.cc', () => {
2104
3341
  const detailedError = new Error('Detailed service error');
2105
3342
  getErrorDetailsSpy.mockReturnValue({error: detailedError});
2106
3343
 
2107
- await expect(webex.cc.getOutdialAniEntries(mockParams)).rejects.toThrow('Detailed service error');
3344
+ await expect(webex.cc.getOutdialAniEntries(mockParams)).rejects.toThrow(
3345
+ 'Detailed service error'
3346
+ );
2108
3347
 
2109
3348
  // Verify failure metrics are tracked
2110
3349
  expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
@@ -2128,11 +3367,7 @@ describe('webex.cc', () => {
2128
3367
  );
2129
3368
 
2130
3369
  // Verify getErrorDetails was called
2131
- expect(getErrorDetailsSpy).toHaveBeenCalledWith(
2132
- mockError,
2133
- 'getOutdialAniEntries',
2134
- CC_FILE
2135
- );
3370
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(mockError, 'getOutdialAniEntries', CC_FILE);
2136
3371
  });
2137
3372
 
2138
3373
  it('should throw error when orgId is not found', async () => {
@@ -2180,4 +3415,503 @@ describe('webex.cc', () => {
2180
3415
  );
2181
3416
  });
2182
3417
  });
3418
+
3419
+ describe('acceptPreviewContact', () => {
3420
+ const previewPayload = {
3421
+ interactionId: 'interaction-123',
3422
+ campaignId: 'campaign-456',
3423
+ };
3424
+
3425
+ it('should accept preview contact successfully', async () => {
3426
+ const mockResponse = {trackingId: 'track-123'} as AgentContact;
3427
+
3428
+ const acceptPreviewContactMock = jest
3429
+ .spyOn(webex.cc.services.dialer, 'acceptPreviewContact')
3430
+ .mockResolvedValue(mockResponse);
3431
+
3432
+ const result = await webex.cc.acceptPreviewContact(previewPayload);
3433
+
3434
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Accepting campaign preview contact', {
3435
+ module: CC_FILE,
3436
+ method: 'acceptPreviewContact',
3437
+ });
3438
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
3439
+ 'Campaign preview contact accepted successfully',
3440
+ {
3441
+ module: CC_FILE,
3442
+ method: 'acceptPreviewContact',
3443
+ trackingId: 'track-123',
3444
+ interactionId: previewPayload.interactionId,
3445
+ }
3446
+ );
3447
+
3448
+ expect(acceptPreviewContactMock).toHaveBeenCalledWith({data: previewPayload});
3449
+ expect(result).toEqual(mockResponse);
3450
+ });
3451
+
3452
+ it('should handle error during acceptPreviewContact', async () => {
3453
+ getErrorDetailsSpy.mockRestore();
3454
+ getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
3455
+
3456
+ const error = {
3457
+ details: {
3458
+ trackingId: '1234',
3459
+ data: {
3460
+ reason: 'Error while performing acceptPreviewContact',
3461
+ },
3462
+ },
3463
+ };
3464
+
3465
+ jest.spyOn(webex.cc.services.dialer, 'acceptPreviewContact').mockRejectedValue(error);
3466
+
3467
+ await expect(webex.cc.acceptPreviewContact(previewPayload)).rejects.toThrow(
3468
+ error.details.data.reason
3469
+ );
3470
+
3471
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Accepting campaign preview contact', {
3472
+ module: CC_FILE,
3473
+ method: 'acceptPreviewContact',
3474
+ });
3475
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
3476
+ `acceptPreviewContact failed with reason: ${error.details.data.reason}`,
3477
+ {module: CC_FILE, method: 'acceptPreviewContact', trackingId: error.details.trackingId}
3478
+ );
3479
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'acceptPreviewContact', CC_FILE);
3480
+ });
3481
+ });
3482
+
3483
+ describe('skipPreviewContact', () => {
3484
+ const previewPayload = {
3485
+ interactionId: 'interaction-123',
3486
+ campaignId: 'campaign-456',
3487
+ };
3488
+
3489
+ it('should skip preview contact successfully', async () => {
3490
+ const mockResponse = {trackingId: 'track-123'} as AgentContact;
3491
+
3492
+ const skipPreviewContactMock = jest
3493
+ .spyOn(webex.cc.services.dialer, 'skipPreviewContact')
3494
+ .mockResolvedValue(mockResponse);
3495
+
3496
+ const result = await webex.cc.skipPreviewContact(previewPayload);
3497
+
3498
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Skipping campaign preview contact', {
3499
+ module: CC_FILE,
3500
+ method: 'skipPreviewContact',
3501
+ });
3502
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
3503
+ 'Campaign preview contact skipped successfully',
3504
+ {
3505
+ module: CC_FILE,
3506
+ method: 'skipPreviewContact',
3507
+ trackingId: 'track-123',
3508
+ interactionId: previewPayload.interactionId,
3509
+ }
3510
+ );
3511
+
3512
+ expect(skipPreviewContactMock).toHaveBeenCalledWith({data: previewPayload});
3513
+ expect(result).toEqual(mockResponse);
3514
+ });
3515
+
3516
+ it('should handle error during skipPreviewContact', async () => {
3517
+ getErrorDetailsSpy.mockRestore();
3518
+ getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
3519
+
3520
+ const error = {
3521
+ details: {
3522
+ trackingId: '1234',
3523
+ data: {
3524
+ reason: 'Error while performing skipPreviewContact',
3525
+ },
3526
+ },
3527
+ };
3528
+
3529
+ jest.spyOn(webex.cc.services.dialer, 'skipPreviewContact').mockRejectedValue(error);
3530
+
3531
+ await expect(webex.cc.skipPreviewContact(previewPayload)).rejects.toThrow(
3532
+ error.details.data.reason
3533
+ );
3534
+
3535
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Skipping campaign preview contact', {
3536
+ module: CC_FILE,
3537
+ method: 'skipPreviewContact',
3538
+ });
3539
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
3540
+ `skipPreviewContact failed with reason: ${error.details.data.reason}`,
3541
+ {module: CC_FILE, method: 'skipPreviewContact', trackingId: error.details.trackingId}
3542
+ );
3543
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'skipPreviewContact', CC_FILE);
3544
+ });
3545
+ });
3546
+
3547
+ describe('removePreviewContact', () => {
3548
+ const previewPayload = {
3549
+ interactionId: 'interaction-123',
3550
+ campaignId: 'campaign-456',
3551
+ };
3552
+
3553
+ it('should remove preview contact successfully', async () => {
3554
+ const mockResponse = {trackingId: 'track-123'} as AgentContact;
3555
+
3556
+ const removePreviewContactMock = jest
3557
+ .spyOn(webex.cc.services.dialer, 'removePreviewContact')
3558
+ .mockResolvedValue(mockResponse);
3559
+
3560
+ const result = await webex.cc.removePreviewContact(previewPayload);
3561
+
3562
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Removing campaign preview contact', {
3563
+ module: CC_FILE,
3564
+ method: 'removePreviewContact',
3565
+ });
3566
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
3567
+ 'Campaign preview contact removed successfully',
3568
+ {
3569
+ module: CC_FILE,
3570
+ method: 'removePreviewContact',
3571
+ trackingId: 'track-123',
3572
+ interactionId: previewPayload.interactionId,
3573
+ }
3574
+ );
3575
+
3576
+ expect(removePreviewContactMock).toHaveBeenCalledWith({data: previewPayload});
3577
+ expect(result).toEqual(mockResponse);
3578
+ });
3579
+
3580
+ it('should handle error during removePreviewContact', async () => {
3581
+ getErrorDetailsSpy.mockRestore();
3582
+ getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
3583
+
3584
+ const error = {
3585
+ details: {
3586
+ trackingId: '1234',
3587
+ data: {
3588
+ reason: 'Error while performing removePreviewContact',
3589
+ },
3590
+ },
3591
+ };
3592
+
3593
+ jest.spyOn(webex.cc.services.dialer, 'removePreviewContact').mockRejectedValue(error);
3594
+
3595
+ await expect(webex.cc.removePreviewContact(previewPayload)).rejects.toThrow(
3596
+ error.details.data.reason
3597
+ );
3598
+
3599
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Removing campaign preview contact', {
3600
+ module: CC_FILE,
3601
+ method: 'removePreviewContact',
3602
+ });
3603
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
3604
+ `removePreviewContact failed with reason: ${error.details.data.reason}`,
3605
+ {module: CC_FILE, method: 'removePreviewContact', trackingId: error.details.trackingId}
3606
+ );
3607
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'removePreviewContact', CC_FILE);
3608
+ });
3609
+ });
3610
+
3611
+ describe('private setManageWebexCallingInWxcc (runtime toggle impl)', () => {
3612
+ beforeEach(() => {
3613
+ webex.internal.device = {
3614
+ userId: 'user-123',
3615
+ registered: false,
3616
+ register: jest.fn().mockResolvedValue(undefined),
3617
+ unregister: jest.fn().mockResolvedValue(undefined),
3618
+ };
3619
+ webex.internal.mercury = {
3620
+ connected: false,
3621
+ connect: jest.fn().mockResolvedValue(undefined),
3622
+ disconnect: jest.fn().mockResolvedValue(undefined),
3623
+ };
3624
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
3625
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
3626
+ webex.cc['agentConfig'] = {
3627
+ agentId: 'agent-123',
3628
+ deviceType: LoginOption.EXTENSION,
3629
+ } as Profile;
3630
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
3631
+ webex.cc.webCallingService.setLoginOption = jest.fn((loginOption: LoginOption) => {
3632
+ webex.cc.webCallingService.loginOption = loginOption;
3633
+ });
3634
+ });
3635
+
3636
+ afterEach(() => {
3637
+ jest.restoreAllMocks();
3638
+ });
3639
+
3640
+ it('should update config, task manager, and publish usersub when enabled', async () => {
3641
+ const publishSpy = jest
3642
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3643
+ .mockResolvedValue(undefined);
3644
+
3645
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
3646
+
3647
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
3648
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(true);
3649
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
3650
+ });
3651
+
3652
+ it('should publish false and update config when disabled after wxApp was enabled', async () => {
3653
+ webex.internal.device = {
3654
+ userId: 'user-123',
3655
+ url: 'https://wdm.example.com/devices/dev-1',
3656
+ };
3657
+ webex.request = jest.fn().mockResolvedValue({body: {}});
3658
+
3659
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
3660
+
3661
+ const publishSpy = jest
3662
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3663
+ .mockResolvedValue(undefined);
3664
+
3665
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
3666
+
3667
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3668
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
3669
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
3670
+ });
3671
+
3672
+ it('should skip usersub publish when disabling without prior wxApp publish', async () => {
3673
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3674
+ const publishSpy = jest
3675
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3676
+ .mockResolvedValue(undefined);
3677
+
3678
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
3679
+
3680
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3681
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
3682
+ expect(publishSpy).not.toHaveBeenCalled();
3683
+ });
3684
+
3685
+ it('should throw when enabling before login', async () => {
3686
+ webex.internal.device = undefined;
3687
+
3688
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3689
+ 'Cannot publish answer-calls-on-wxcc: user is not logged in'
3690
+ );
3691
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3692
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
3693
+ });
3694
+
3695
+ it('should throw when enabling on BROWSER (Desktop) login', async () => {
3696
+ webex.cc['agentConfig'] = {deviceType: LoginOption.BROWSER} as Profile;
3697
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
3698
+ const publishSpy = jest
3699
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3700
+ .mockResolvedValue(undefined);
3701
+
3702
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3703
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
3704
+ );
3705
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3706
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
3707
+ expect(publishSpy).not.toHaveBeenCalled();
3708
+ });
3709
+
3710
+ it('should throw when enabling before station login', async () => {
3711
+ webex.cc['agentConfig'] = undefined as unknown as Profile;
3712
+ webex.cc.webCallingService.loginOption = undefined as unknown as LoginOption;
3713
+ const publishSpy = jest
3714
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3715
+ .mockResolvedValue(undefined);
3716
+
3717
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3718
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
3719
+ );
3720
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3721
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
3722
+ expect(publishSpy).not.toHaveBeenCalled();
3723
+ });
3724
+
3725
+ it('should throw when webCallingService is BROWSER even if agentConfig deviceType is stale', async () => {
3726
+ webex.cc['agentConfig'] = {deviceType: LoginOption.EXTENSION} as Profile;
3727
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
3728
+ const publishSpy = jest
3729
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3730
+ .mockResolvedValue(undefined);
3731
+
3732
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3733
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
3734
+ );
3735
+ expect(publishSpy).not.toHaveBeenCalled();
3736
+ });
3737
+
3738
+ it('should publish usersub when enabling on AGENT_DN login', async () => {
3739
+ webex.cc['agentConfig'] = {
3740
+ agentId: 'agent-123',
3741
+ deviceType: LoginOption.AGENT_DN,
3742
+ } as Profile;
3743
+ const publishSpy = jest
3744
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3745
+ .mockResolvedValue(undefined);
3746
+
3747
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
3748
+
3749
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
3750
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
3751
+ });
3752
+
3753
+ it('should no-op usersub publish when disabling before login', async () => {
3754
+ webex.internal.device = undefined;
3755
+ const publishSpy = jest
3756
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3757
+ .mockResolvedValue(undefined);
3758
+
3759
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
3760
+
3761
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3762
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
3763
+ expect(publishSpy).not.toHaveBeenCalled();
3764
+ });
3765
+
3766
+ it('should subscribe to wxApp Mercury mute sync when enabled', async () => {
3767
+ webex.cc['agentConfig'] = {agentId: 'agent-123'} as Profile;
3768
+ webex.internal.device = {
3769
+ ...webex.internal.device,
3770
+ registered: true,
3771
+ register: jest.fn().mockResolvedValue(undefined),
3772
+ };
3773
+ webex.internal.mercury.connected = true;
3774
+ webex.internal.mercury.connect = jest.fn().mockResolvedValue(undefined);
3775
+ jest
3776
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3777
+ .mockResolvedValue(undefined);
3778
+ const subscribeSpy = jest
3779
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
3780
+ .mockImplementation(() => {});
3781
+
3782
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
3783
+
3784
+ expect(subscribeSpy).toHaveBeenCalledWith('agent-123', expect.any(Function));
3785
+ });
3786
+
3787
+ it('should unsubscribe from wxApp Mercury mute sync when disabled', async () => {
3788
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3789
+ jest
3790
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3791
+ .mockResolvedValue(undefined);
3792
+ const unsubscribeSpy = jest
3793
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe')
3794
+ .mockImplementation(() => {});
3795
+
3796
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
3797
+
3798
+ expect(unsubscribeSpy).toHaveBeenCalled();
3799
+ });
3800
+
3801
+ it('should register device and connect mercury when enabling wxApp sync', async () => {
3802
+ webex.cc['agentConfig'] = {agentId: 'agent-123'} as Profile;
3803
+ webex.internal.device = {
3804
+ userId: 'user-123',
3805
+ registered: false,
3806
+ register: jest.fn().mockResolvedValue(undefined),
3807
+ };
3808
+ webex.internal.mercury = {
3809
+ connected: false,
3810
+ connect: jest.fn().mockResolvedValue(undefined),
3811
+ };
3812
+ jest
3813
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3814
+ .mockResolvedValue(undefined);
3815
+ const subscribeSpy = jest
3816
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
3817
+ .mockImplementation(() => {});
3818
+
3819
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
3820
+
3821
+ expect(webex.internal.device.register).toHaveBeenCalled();
3822
+ expect(webex.internal.mercury.connect).toHaveBeenCalled();
3823
+ expect(subscribeSpy).toHaveBeenCalledWith('agent-123', expect.any(Function));
3824
+ });
3825
+
3826
+ it('should rollback config and task manager when usersub publish fails', async () => {
3827
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
3828
+ jest
3829
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3830
+ .mockRejectedValue(new Error('usersub failed'));
3831
+
3832
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow('usersub failed');
3833
+
3834
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3835
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(true);
3836
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenLastCalledWith(false);
3837
+ });
3838
+
3839
+ it('should rollback config and release Mercury resources when Mercury connect fails', async () => {
3840
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
3841
+ const publishSpy = jest
3842
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3843
+ .mockResolvedValue(undefined);
3844
+ jest
3845
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
3846
+ .mockRejectedValue(new Error('mercury connect failed'));
3847
+ const releaseSpy = jest
3848
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
3849
+ .mockResolvedValue(undefined);
3850
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
3851
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
3852
+
3853
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3854
+ 'mercury connect failed'
3855
+ );
3856
+
3857
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3858
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenLastCalledWith(false);
3859
+ expect(publishSpy).not.toHaveBeenCalled();
3860
+ expect(teardownSpy).not.toHaveBeenCalled();
3861
+ expect(unsubscribeSpy).toHaveBeenCalled();
3862
+ expect(releaseSpy).toHaveBeenCalled();
3863
+ });
3864
+
3865
+ it('should schedule compensating false retry when rollback publish fails after enable error', async () => {
3866
+ jest.useFakeTimers();
3867
+ try {
3868
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3869
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
3870
+ webex.cc.agentConfig = {
3871
+ agentId: 'agent-123',
3872
+ deviceType: LoginOption.EXTENSION,
3873
+ };
3874
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
3875
+
3876
+ const publishSpy = jest
3877
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3878
+ .mockResolvedValueOnce(undefined)
3879
+ .mockRejectedValueOnce(new Error('compensating false failed'))
3880
+ .mockResolvedValue(undefined);
3881
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
3882
+ jest
3883
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
3884
+ .mockResolvedValue(undefined);
3885
+ jest
3886
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
3887
+ .mockResolvedValue(undefined);
3888
+ jest
3889
+ .spyOn(mockTaskManager, 'syncWxAppMuteFromCallDetailsForAllTasks')
3890
+ .mockImplementation(() => {
3891
+ throw new Error('sync failed');
3892
+ });
3893
+ const scheduleRetrySpy = jest.spyOn(
3894
+ webex.cc as any,
3895
+ 'scheduleCompensatingFalsePublishRetry'
3896
+ );
3897
+
3898
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
3899
+ 'sync failed'
3900
+ );
3901
+
3902
+ expect(scheduleRetrySpy).toHaveBeenCalled();
3903
+ expect(publishSpy).toHaveBeenCalledTimes(2);
3904
+ expect(publishSpy).toHaveBeenNthCalledWith(1, true, {userId: 'user-123'});
3905
+ expect(publishSpy).toHaveBeenNthCalledWith(2, false, {userId: 'user-123'});
3906
+
3907
+ jest.advanceTimersByTime(30_000);
3908
+ await publishSpy.mock.results[2]?.value;
3909
+
3910
+ expect(publishSpy).toHaveBeenCalledTimes(3);
3911
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123'});
3912
+ } finally {
3913
+ jest.useRealTimers();
3914
+ }
3915
+ });
3916
+ });
2183
3917
  });