@webex/contact-center 3.12.0-next.14 → 3.12.0-next.140

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 (317) hide show
  1. package/.sdd/manifest.json +885 -0
  2. package/AGENTS.md +94 -0
  3. package/ai-docs/ARCHITECTURE.md +168 -0
  4. package/ai-docs/CONTRACTS.md +59 -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 +52 -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 +382 -0
  17. package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
  18. package/ai-docs/features/contact-owner-change-refactor.md +78 -0
  19. package/ai-docs/features/generated-spec-conformance-fidelity-remediation/spec/feature-spec.md +117 -0
  20. package/ai-docs/features/residual-warning-coverage-completion/spec/feature-spec.md +203 -0
  21. package/ai-docs/features/validator-code-fidelity-drift-fix/spec/feature-spec.md +315 -0
  22. package/ai-docs/patterns/event-driven-patterns.md +485 -0
  23. package/ai-docs/patterns/testing-patterns.md +480 -0
  24. package/ai-docs/patterns/typescript-patterns.md +365 -0
  25. package/ai-docs/templates/README.md +102 -0
  26. package/ai-docs/templates/documentation/create-agents-md.md +240 -0
  27. package/ai-docs/templates/documentation/create-architecture-md.md +295 -0
  28. package/ai-docs/templates/existing-service/bug-fix.md +254 -0
  29. package/ai-docs/templates/existing-service/feature-enhancement.md +450 -0
  30. package/ai-docs/templates/new-method/00-master.md +80 -0
  31. package/ai-docs/templates/new-method/01-requirements.md +232 -0
  32. package/ai-docs/templates/new-method/02-implementation.md +295 -0
  33. package/ai-docs/templates/new-method/03-tests.md +201 -0
  34. package/ai-docs/templates/new-method/04-validation.md +141 -0
  35. package/ai-docs/templates/new-service/00-master.md +109 -0
  36. package/ai-docs/templates/new-service/01-pre-questions.md +159 -0
  37. package/ai-docs/templates/new-service/02-code-generation.md +346 -0
  38. package/ai-docs/templates/new-service/03-integration.md +178 -0
  39. package/ai-docs/templates/new-service/04-test-generation.md +205 -0
  40. package/ai-docs/templates/new-service/05-validation.md +145 -0
  41. package/dist/cc.js +1004 -43
  42. package/dist/cc.js.map +1 -1
  43. package/dist/config.js +13 -0
  44. package/dist/config.js.map +1 -1
  45. package/dist/constants.js +31 -3
  46. package/dist/constants.js.map +1 -1
  47. package/dist/index.js +45 -15
  48. package/dist/index.js.map +1 -1
  49. package/dist/metrics/MetricsManager.js +16 -0
  50. package/dist/metrics/MetricsManager.js.map +1 -1
  51. package/dist/metrics/behavioral-events.js +211 -0
  52. package/dist/metrics/behavioral-events.js.map +1 -1
  53. package/dist/metrics/constants.js +49 -4
  54. package/dist/metrics/constants.js.map +1 -1
  55. package/dist/services/AddressBook.js +18 -15
  56. package/dist/services/AddressBook.js.map +1 -1
  57. package/dist/services/AnswerCallOnWebexService.js +202 -0
  58. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  59. package/dist/services/ApiAiAssistant.js +269 -9
  60. package/dist/services/ApiAiAssistant.js.map +1 -1
  61. package/dist/services/EntryPoint.js +46 -68
  62. package/dist/services/EntryPoint.js.map +1 -1
  63. package/dist/services/Queue.js +27 -22
  64. package/dist/services/Queue.js.map +1 -1
  65. package/dist/services/UserPreference.js +427 -0
  66. package/dist/services/UserPreference.js.map +1 -0
  67. package/dist/services/WebexCrossClientService.js +225 -0
  68. package/dist/services/WebexCrossClientService.js.map +1 -0
  69. package/dist/services/WxAppTelephonyMercurySync.js +111 -0
  70. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  71. package/dist/services/config/Util.js +11 -4
  72. package/dist/services/config/Util.js.map +1 -1
  73. package/dist/services/config/constants.js +41 -7
  74. package/dist/services/config/constants.js.map +1 -1
  75. package/dist/services/config/index.js +51 -0
  76. package/dist/services/config/index.js.map +1 -1
  77. package/dist/services/config/types.js +57 -12
  78. package/dist/services/config/types.js.map +1 -1
  79. package/dist/services/core/Err.js.map +1 -1
  80. package/dist/services/core/Utils.js +111 -32
  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 +28 -14
  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 +2 -1
  88. package/dist/services/core/websocket/WebSocketManager.js.map +1 -1
  89. package/dist/services/core/websocket/types.js.map +1 -1
  90. package/dist/services/index.js +1 -1
  91. package/dist/services/index.js.map +1 -1
  92. package/dist/services/task/Task.js +754 -0
  93. package/dist/services/task/Task.js.map +1 -0
  94. package/dist/services/task/TaskFactory.js +50 -0
  95. package/dist/services/task/TaskFactory.js.map +1 -0
  96. package/dist/services/task/TaskManager.js +1183 -521
  97. package/dist/services/task/TaskManager.js.map +1 -1
  98. package/dist/services/task/TaskUtils.js +211 -27
  99. package/dist/services/task/TaskUtils.js.map +1 -1
  100. package/dist/services/task/WebexCallingUtils.js +70 -0
  101. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  102. package/dist/services/task/constants.js +12 -2
  103. package/dist/services/task/constants.js.map +1 -1
  104. package/dist/services/task/contact.js +29 -0
  105. package/dist/services/task/contact.js.map +1 -1
  106. package/dist/services/task/dialer.js +78 -0
  107. package/dist/services/task/dialer.js.map +1 -1
  108. package/dist/services/task/digital/Digital.js +78 -0
  109. package/dist/services/task/digital/Digital.js.map +1 -0
  110. package/dist/services/task/state-machine/TaskStateMachine.js +982 -0
  111. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -0
  112. package/dist/services/task/state-machine/actions.js +572 -0
  113. package/dist/services/task/state-machine/actions.js.map +1 -0
  114. package/dist/services/task/state-machine/constants.js +161 -0
  115. package/dist/services/task/state-machine/constants.js.map +1 -0
  116. package/dist/services/task/state-machine/guards.js +379 -0
  117. package/dist/services/task/state-machine/guards.js.map +1 -0
  118. package/dist/services/task/state-machine/index.js +53 -0
  119. package/dist/services/task/state-machine/index.js.map +1 -0
  120. package/dist/services/task/state-machine/types.js +54 -0
  121. package/dist/services/task/state-machine/types.js.map +1 -0
  122. package/dist/services/task/state-machine/uiControlsComputer.js +703 -0
  123. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -0
  124. package/dist/services/task/taskDataNormalizer.js +99 -0
  125. package/dist/services/task/taskDataNormalizer.js.map +1 -0
  126. package/dist/services/task/types.js +228 -5
  127. package/dist/services/task/types.js.map +1 -1
  128. package/dist/services/task/voice/Voice.js +1504 -0
  129. package/dist/services/task/voice/Voice.js.map +1 -0
  130. package/dist/services/task/voice/WebRTC.js +152 -0
  131. package/dist/services/task/voice/WebRTC.js.map +1 -0
  132. package/dist/services/task/voice/wxAppOfferObservability.js +145 -0
  133. package/dist/services/task/voice/wxAppOfferObservability.js.map +1 -0
  134. package/dist/services/task/voice/wxAppVoiceMethods.js +395 -0
  135. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  136. package/dist/services/wxAppDiagnosticLogging.js +159 -0
  137. package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
  138. package/dist/services/wxAppTelephonyUtils.js +19 -0
  139. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  140. package/dist/types/cc.d.ts +193 -6
  141. package/dist/types/config.d.ts +13 -0
  142. package/dist/types/constants.d.ts +29 -1
  143. package/dist/types/index.d.ts +35 -8
  144. package/dist/types/metrics/MetricsManager.d.ts +6 -0
  145. package/dist/types/metrics/constants.d.ts +44 -1
  146. package/dist/types/services/AddressBook.d.ts +2 -1
  147. package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -0
  148. package/dist/types/services/ApiAiAssistant.d.ts +56 -5
  149. package/dist/types/services/EntryPoint.d.ts +7 -5
  150. package/dist/types/services/Queue.d.ts +5 -3
  151. package/dist/types/services/UserPreference.d.ts +118 -0
  152. package/dist/types/services/WebexCrossClientService.d.ts +32 -0
  153. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  154. package/dist/types/services/config/constants.d.ts +34 -5
  155. package/dist/types/services/config/types.d.ts +199 -12
  156. package/dist/types/services/core/Err.d.ts +6 -0
  157. package/dist/types/services/core/Utils.d.ts +35 -13
  158. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  159. package/dist/types/services/core/types.d.ts +2 -0
  160. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
  161. package/dist/types/services/core/websocket/types.d.ts +1 -1
  162. package/dist/types/services/index.d.ts +1 -1
  163. package/dist/types/services/task/Task.d.ts +175 -0
  164. package/dist/types/services/task/TaskFactory.d.ts +13 -0
  165. package/dist/types/services/task/TaskUtils.d.ts +48 -2
  166. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  167. package/dist/types/services/task/constants.d.ts +10 -0
  168. package/dist/types/services/task/contact.d.ts +4 -0
  169. package/dist/types/services/task/dialer.d.ts +30 -0
  170. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  171. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1442 -0
  172. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  173. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  174. package/dist/types/services/task/state-machine/guards.d.ts +103 -0
  175. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  176. package/dist/types/services/task/state-machine/types.d.ts +277 -0
  177. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  178. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  179. package/dist/types/services/task/types.d.ts +682 -66
  180. package/dist/types/services/task/voice/Voice.d.ts +240 -0
  181. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  182. package/dist/types/services/task/voice/wxAppOfferObservability.d.ts +39 -0
  183. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +68 -0
  184. package/dist/types/services/wxAppDiagnosticLogging.d.ts +63 -0
  185. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  186. package/dist/types/types.d.ts +205 -9
  187. package/dist/types/utils/PageCache.d.ts +20 -3
  188. package/dist/types/webex.d.ts +1 -0
  189. package/dist/types.js +162 -4
  190. package/dist/types.js.map +1 -1
  191. package/dist/utils/PageCache.js +19 -5
  192. package/dist/utils/PageCache.js.map +1 -1
  193. package/dist/webex.js +22 -2
  194. package/dist/webex.js.map +1 -1
  195. package/package.json +16 -12
  196. package/src/cc.ts +1330 -117
  197. package/src/config.ts +13 -0
  198. package/src/constants.ts +29 -1
  199. package/src/index.ts +36 -5
  200. package/src/metrics/MetricsManager.ts +16 -0
  201. package/src/metrics/ai-docs/AGENTS.md +350 -0
  202. package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
  203. package/src/metrics/ai-docs/metrics-spec.md +963 -0
  204. package/src/metrics/behavioral-events.ts +218 -0
  205. package/src/metrics/constants.ts +53 -4
  206. package/src/services/AddressBook.ts +17 -6
  207. package/src/services/AnswerCallOnWebexService.ts +239 -0
  208. package/src/services/ApiAiAssistant.ts +356 -9
  209. package/src/services/EntryPoint.ts +59 -60
  210. package/src/services/Queue.ts +29 -12
  211. package/src/services/UserPreference.ts +509 -0
  212. package/src/services/WebexCrossClientService.ts +298 -0
  213. package/src/services/WxAppTelephonyMercurySync.ts +142 -0
  214. package/src/services/agent/ai-docs/AGENTS.md +240 -0
  215. package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
  216. package/src/services/agent/ai-docs/agent-spec.md +505 -0
  217. package/src/services/ai-docs/AGENTS.md +386 -0
  218. package/src/services/ai-docs/services-spec.md +505 -0
  219. package/src/services/config/Util.ts +12 -3
  220. package/src/services/config/ai-docs/AGENTS.md +255 -0
  221. package/src/services/config/ai-docs/ARCHITECTURE.md +426 -0
  222. package/src/services/config/ai-docs/config-spec.md +686 -0
  223. package/src/services/config/constants.ts +46 -6
  224. package/src/services/config/index.ts +65 -0
  225. package/src/services/config/types.ts +199 -13
  226. package/src/services/core/Err.ts +3 -0
  227. package/src/services/core/Utils.ts +127 -37
  228. package/src/services/core/WebexRequest.ts +3 -1
  229. package/src/services/core/ai-docs/AGENTS.md +381 -0
  230. package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
  231. package/src/services/core/ai-docs/core-spec.md +787 -0
  232. package/src/services/core/aqm-reqs.ts +30 -15
  233. package/src/services/core/types.ts +2 -0
  234. package/src/services/core/websocket/WebSocketManager.ts +2 -0
  235. package/src/services/core/websocket/types.ts +1 -1
  236. package/src/services/index.ts +1 -1
  237. package/src/services/task/Task.ts +908 -0
  238. package/src/services/task/TaskFactory.ts +61 -0
  239. package/src/services/task/TaskManager.ts +1430 -608
  240. package/src/services/task/TaskUtils.ts +303 -25
  241. package/src/services/task/WebexCallingUtils.ts +136 -0
  242. package/src/services/task/ai-docs/AGENTS.md +457 -0
  243. package/src/services/task/ai-docs/ARCHITECTURE.md +595 -0
  244. package/src/services/task/ai-docs/task-spec.md +1476 -0
  245. package/src/services/task/constants.ts +10 -0
  246. package/src/services/task/contact.ts +30 -0
  247. package/src/services/task/dialer.ts +80 -0
  248. package/src/services/task/digital/Digital.ts +97 -0
  249. package/src/services/task/state-machine/TaskStateMachine.ts +1320 -0
  250. package/src/services/task/state-machine/actions.ts +741 -0
  251. package/src/services/task/state-machine/ai-docs/AGENTS.md +460 -0
  252. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1156 -0
  253. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2247 -0
  254. package/src/services/task/state-machine/constants.ts +172 -0
  255. package/src/services/task/state-machine/guards.ts +442 -0
  256. package/src/services/task/state-machine/index.ts +28 -0
  257. package/src/services/task/state-machine/types.ts +258 -0
  258. package/src/services/task/state-machine/uiControlsComputer.ts +1135 -0
  259. package/src/services/task/taskDataNormalizer.ts +137 -0
  260. package/src/services/task/types.ts +835 -70
  261. package/src/services/task/voice/Voice.ts +1873 -0
  262. package/src/services/task/voice/WebRTC.ts +191 -0
  263. package/src/services/task/voice/wxAppOfferObservability.ts +204 -0
  264. package/src/services/task/voice/wxAppVoiceMethods.ts +575 -0
  265. package/src/services/wxAppDiagnosticLogging.ts +286 -0
  266. package/src/services/wxAppTelephonyUtils.ts +14 -0
  267. package/src/types.ts +264 -11
  268. package/src/utils/AGENTS.md +289 -0
  269. package/src/utils/PageCache.ts +38 -5
  270. package/src/utils/ai-docs/utils-spec.md +391 -0
  271. package/src/webex.js +10 -0
  272. package/test/unit/spec/cc.ts +2371 -98
  273. package/test/unit/spec/logger-proxy.ts +70 -0
  274. package/test/unit/spec/metrics/MetricsManager.ts +25 -0
  275. package/test/unit/spec/metrics/behavioral-events.ts +60 -0
  276. package/test/unit/spec/services/AddressBook.ts +37 -6
  277. package/test/unit/spec/services/AnswerCallOnWebexService.ts +264 -0
  278. package/test/unit/spec/services/ApiAiAssistant.ts +311 -21
  279. package/test/unit/spec/services/EntryPoint.ts +87 -40
  280. package/test/unit/spec/services/Queue.ts +123 -12
  281. package/test/unit/spec/services/UserPreference.ts +401 -0
  282. package/test/unit/spec/services/WebCallingService.ts +7 -1
  283. package/test/unit/spec/services/WebexCrossClientService.ts +509 -0
  284. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +136 -0
  285. package/test/unit/spec/services/config/Util.ts +129 -0
  286. package/test/unit/spec/services/config/index.ts +97 -29
  287. package/test/unit/spec/services/core/Utils.ts +425 -8
  288. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  289. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  290. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +66 -40
  291. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  292. package/test/unit/spec/services/task/Task.ts +745 -0
  293. package/test/unit/spec/services/task/TaskFactory.ts +99 -0
  294. package/test/unit/spec/services/task/TaskManager.ts +2539 -1490
  295. package/test/unit/spec/services/task/TaskUtils.ts +235 -0
  296. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  297. package/test/unit/spec/services/task/contact.ts +33 -0
  298. package/test/unit/spec/services/task/dialer.ts +190 -0
  299. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  300. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +4060 -0
  301. package/test/unit/spec/services/task/state-machine/guards.ts +839 -0
  302. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  303. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +3101 -0
  304. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  305. package/test/unit/spec/services/task/voice/Voice.ts +2020 -0
  306. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  307. package/test/unit/spec/services/task/voice/wxAppOfferObservability.ts +361 -0
  308. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +719 -0
  309. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +179 -0
  310. package/test/unit/spec/wellnessSampleUtils.ts +288 -0
  311. package/umd/contact-center.min.js +2 -2
  312. package/umd/contact-center.min.js.map +1 -1
  313. package/dist/services/task/index.js +0 -1530
  314. package/dist/services/task/index.js.map +0 -1
  315. package/dist/types/services/task/index.d.ts +0 -650
  316. package/src/services/task/index.ts +0 -1806
  317. package/test/unit/spec/services/task/index.ts +0 -2205
@@ -59,6 +59,7 @@ global.URL.createObjectURL = jest.fn(() => 'blob:http://localhost:3000/12345');
59
59
 
60
60
  describe('webex.cc', () => {
61
61
  let webex;
62
+ let mockApiAIAssistant;
62
63
  let mockContact;
63
64
  let mockTaskManager;
64
65
  let mockMetricsManager;
@@ -109,6 +110,14 @@ describe('webex.cc', () => {
109
110
  cancelCtq: jest.fn(),
110
111
  };
111
112
 
113
+ mockApiAIAssistant = {
114
+ sendEvent: jest.fn(),
115
+ getRealTimeAssistance: jest.fn(),
116
+ fetchHistoricTranscripts: jest.fn(),
117
+ setAIFeatureFlags: jest.fn(),
118
+ setAgentId: jest.fn(),
119
+ };
120
+
112
121
  // Mock Services instance
113
122
  const mockServicesInstance = {
114
123
  agent: {
@@ -121,6 +130,7 @@ describe('webex.cc', () => {
121
130
  config: {
122
131
  getAgentConfig: jest.fn(),
123
132
  getOutdialAniEntries: jest.fn(),
133
+ getWellbeingBreakIdleCode: jest.fn(),
124
134
  },
125
135
  webSocketManager: mockWebSocketManager,
126
136
  rtdWebSocketManager: {
@@ -139,6 +149,8 @@ describe('webex.cc', () => {
139
149
  dialer: {
140
150
  startOutdial: jest.fn(),
141
151
  acceptPreviewContact: jest.fn(),
152
+ skipPreviewContact: jest.fn(),
153
+ removePreviewContact: jest.fn(),
142
154
  },
143
155
  apiAIAssistant: {
144
156
  sendEvent: jest.fn(),
@@ -147,21 +159,26 @@ describe('webex.cc', () => {
147
159
  };
148
160
 
149
161
  mockTaskManager = {
162
+ apiAIAssistant: mockApiAIAssistant,
150
163
  contact: mockContact,
151
164
  call: undefined,
152
165
  taskCollection: {},
153
166
  webCallingService: undefined,
154
167
  webSocketManager: mockWebSocketManager,
155
168
  task: undefined,
169
+ setConfigFlags: jest.fn(),
156
170
  setWrapupData: jest.fn(),
157
171
  setAgentId: jest.fn(),
158
172
  setWebRtcEnabled: jest.fn(),
159
- handleRealtimeTranscriptEvent: jest.fn(),
160
- setApiAIAssistant: jest.fn(),
173
+ setAnswerCallOnWebexService: jest.fn(),
174
+ handleRealtimeWebsocketEvent: jest.fn(),
161
175
  registerIncomingCallEvent: jest.fn(),
162
176
  registerTaskListeners: jest.fn(),
163
177
  getTask: jest.fn(),
164
178
  getActiveTasks: jest.fn(),
179
+ applyEnableWxBetterTogether: jest.fn(),
180
+ syncWxAppMuteFromCallDetailsForAllTasks: jest.fn(),
181
+ refreshWxAppOfferObservabilityForAllTasks: jest.fn(),
165
182
  on: jest.fn(),
166
183
  off: jest.fn(),
167
184
  emit: jest.fn(),
@@ -200,6 +217,35 @@ describe('webex.cc', () => {
200
217
  webex.emit('ready');
201
218
  });
202
219
 
220
+ it('should throw when WebRTC registration is disabled without multi-login', () => {
221
+ const invalidWebex = MockWebex({
222
+ children: {
223
+ mercury: Mercury,
224
+ },
225
+ logger: {
226
+ log: jest.fn(),
227
+ error: jest.fn(),
228
+ info: jest.fn(),
229
+ },
230
+ credentials: {
231
+ getOrgId: jest.fn(() => 'mockOrgId'),
232
+ },
233
+ config: {
234
+ ...config,
235
+ cc: {
236
+ ...config.cc,
237
+ allowMultiLogin: false,
238
+ disableWebRTCRegistration: true,
239
+ },
240
+ },
241
+ once: jest.fn((event, callback) => callback()),
242
+ }) as unknown as WebexSDK;
243
+
244
+ expect(() => new ContactCenter({parent: invalidWebex})).toThrow(
245
+ '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.'
246
+ );
247
+ });
248
+
203
249
  describe('cc.getDeviceId', () => {
204
250
  it('should return dialNumber when loginOption is EXTENSION', () => {
205
251
  const loginOption = LoginOption.EXTENSION;
@@ -267,14 +313,18 @@ describe('webex.cc', () => {
267
313
  isAgentAvailableAfterOutdial: false,
268
314
  isCampaignManagementEnabled: false,
269
315
  outDialEp: '',
270
- isEndCallEnabled: false,
316
+ isEndTaskEnabled: false,
271
317
  isEndConsultEnabled: false,
272
318
  agentDbId: '',
273
319
  allowConsultToQueue: false,
320
+ accessQueue: 'SPECIFIC',
321
+ accessEntryPoint: 'ALL',
322
+ accessBuddyTeam: 'ALL',
274
323
  agentPersonalStatsEnabled: false,
275
324
  isTimeoutDesktopInactivityEnabled: false,
276
325
  webRtcEnabled: true,
277
326
  lostConnectionRecoveryTimeout: 0,
327
+ isWellnessBreakEnabled: false,
278
328
  };
279
329
 
280
330
  it('should register successfully and return agent profile', async () => {
@@ -286,6 +336,7 @@ describe('webex.cc', () => {
286
336
  data: {
287
337
  auxCodeId: 'auxCodeId',
288
338
  agentId: 'agentId',
339
+ agentSessionId: 'session-after-refresh',
289
340
  deviceType: LoginOption.EXTENSION,
290
341
  dn: '12345',
291
342
  },
@@ -300,7 +351,7 @@ describe('webex.cc', () => {
300
351
  const result = await webex.cc.register();
301
352
 
302
353
  // Verify logging calls
303
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting CC SDK registration', {
354
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting CC SDK registration', {
304
355
  module: CC_FILE,
305
356
  method: 'register',
306
357
  });
@@ -334,6 +385,10 @@ describe('webex.cc', () => {
334
385
  TASK_EVENTS.TASK_HYDRATE,
335
386
  expect.any(Function)
336
387
  );
388
+ expect(mockTaskManager.on).toHaveBeenCalledWith(
389
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
390
+ expect.any(Function)
391
+ );
337
392
  expect(mockWebSocketManager.on).toHaveBeenCalledWith('message', expect.any(Function));
338
393
  expect(webex.cc.services.rtdWebSocketManager.initWebSocket).toHaveBeenCalledWith({
339
394
  body: {
@@ -354,8 +409,25 @@ describe('webex.cc', () => {
354
409
  module: CC_FILE,
355
410
  method: 'connectWebsocket',
356
411
  });
412
+ expect(mockTaskManager.setConfigFlags).toHaveBeenCalledWith(
413
+ expect.objectContaining({
414
+ isEndTaskEnabled: mockAgentProfile.isEndTaskEnabled,
415
+ isEndConsultEnabled: mockAgentProfile.isEndConsultEnabled,
416
+ webRtcEnabled: mockAgentProfile.webRtcEnabled,
417
+ autoWrapup: mockAgentProfile.wrapUpData.wrapUpProps.autoWrapup ?? false,
418
+ aiFeature: mockAgentProfile.aiFeature,
419
+ consultTransfer: {
420
+ allowConsultToQueue: mockAgentProfile.allowConsultToQueue,
421
+ accessQueue: mockAgentProfile.accessQueue,
422
+ accessEntryPoint: mockAgentProfile.accessEntryPoint,
423
+ accessBuddyTeam: mockAgentProfile.accessBuddyTeam,
424
+ },
425
+ enableWxBetterTogether: false,
426
+ getWxAppUsersubPublished: expect.any(Function),
427
+ })
428
+ );
357
429
  expect(reloadSpy).toHaveBeenCalled();
358
- expect(result).toEqual(mockAgentProfile);
430
+ expect(result).toEqual({...mockAgentProfile, agentSessionId: 'session-after-refresh'});
359
431
  expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
360
432
  METRIC_EVENT_NAMES.WEBSOCKET_REGISTER_SUCCESS,
361
433
  METRIC_EVENT_NAMES.WEBSOCKET_REGISTER_FAILED,
@@ -413,7 +485,7 @@ describe('webex.cc', () => {
413
485
 
414
486
  await expect(webex.cc.register()).rejects.toThrow('Error while performing register');
415
487
 
416
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting CC SDK registration', {
488
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting CC SDK registration', {
417
489
  module: CC_FILE,
418
490
  method: 'register',
419
491
  });
@@ -432,6 +504,25 @@ describe('webex.cc', () => {
432
504
  );
433
505
  });
434
506
 
507
+ it('clears wellness session context when registration fails after relogin', async () => {
508
+ const mockError = new Error('Error after silent relogin');
509
+ webex.cc.$config = {...webex.cc.$config, allowAutomatedRelogin: true};
510
+ jest.spyOn(webex.cc.services.config, 'getAgentConfig').mockResolvedValue({
511
+ ...mockAgentProfile,
512
+ webRtcEnabled: false,
513
+ aiFeature: undefined,
514
+ } as Profile);
515
+ mockWebSocketManager.initWebSocket.mockResolvedValue({agentId: 'agent123'});
516
+ jest.spyOn(webex.cc as any, 'silentRelogin').mockImplementation(async () => {
517
+ webex.cc['updateWellnessSession']('partial-session');
518
+ throw mockError;
519
+ });
520
+
521
+ await expect(webex.cc.register()).rejects.toThrow(mockError);
522
+
523
+ expect(webex.cc['currentAgentSessionId']).toBeUndefined();
524
+ });
525
+
435
526
  it('should log error if mercury connect fails but cc.register() should not fail', async () => {
436
527
  const mockError = new Error('Error while performing mercury connect');
437
528
  jest.spyOn(webex.internal.mercury, 'connect').mockRejectedValue(mockError);
@@ -482,6 +573,10 @@ describe('webex.cc', () => {
482
573
  TASK_EVENTS.TASK_HYDRATE,
483
574
  expect.any(Function)
484
575
  );
576
+ expect(mockTaskManager.on).toHaveBeenCalledWith(
577
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
578
+ expect.any(Function)
579
+ );
485
580
  expect(mockWebSocketManager.on).toHaveBeenCalledWith('message', expect.any(Function));
486
581
 
487
582
  expect(configSpy).toHaveBeenCalled();
@@ -493,6 +588,36 @@ describe('webex.cc', () => {
493
588
  expect(result).toEqual(mockAgentProfile);
494
589
  });
495
590
 
591
+ it('should skip mercury connection when disableWebRTCRegistration is enabled', async () => {
592
+ webex.cc.$config = {
593
+ ...webex.cc.$config,
594
+ allowAutomatedRelogin: false,
595
+ disableWebRTCRegistration: true,
596
+ };
597
+ mockAgentProfile.webRtcEnabled = true;
598
+ const mercurySpy = jest.spyOn(webex.internal.mercury, 'connect');
599
+ const connectWebsocketSpy = jest.spyOn(webex.cc, 'connectWebsocket');
600
+ const setupEventListenersSpy = jest.spyOn(webex.cc, 'setupEventListeners');
601
+ jest.spyOn(webex.cc.services.config, 'getAgentConfig').mockResolvedValue(mockAgentProfile);
602
+ mockWebSocketManager.initWebSocket.mockResolvedValue({
603
+ agentId: 'agent123',
604
+ });
605
+
606
+ const result = await webex.cc.register();
607
+
608
+ expect(connectWebsocketSpy).toHaveBeenCalled();
609
+ expect(setupEventListenersSpy).toHaveBeenCalled();
610
+ expect(mercurySpy).not.toHaveBeenCalled();
611
+ expect(LoggerProxy.info).toHaveBeenCalledWith(
612
+ 'Skipping Mobius registration because disableWebRTCRegistration is enabled',
613
+ {
614
+ module: CC_FILE,
615
+ method: 'connectWebsocket',
616
+ }
617
+ );
618
+ expect(result).toEqual(mockAgentProfile);
619
+ });
620
+
496
621
  it('should not attempt for mercury connection when webrtc is disabled', async () => {
497
622
  mockAgentProfile.webRtcEnabled = false;
498
623
  mockAgentProfile.aiFeature = {realtimeTranscripts: {enable: false}} as any;
@@ -534,8 +659,47 @@ describe('webex.cc', () => {
534
659
  expect(result).toEqual(mockAgentProfile);
535
660
  });
536
661
 
537
- it('should not connect RTD websocket when realtime transcripts feature is disabled', async () => {
538
- mockAgentProfile.aiFeature = {realtimeTranscripts: {enable: false}} as any;
662
+ it('should connect RTD websocket when only wellness breaks are enabled', async () => {
663
+ mockAgentProfile.aiFeature = {
664
+ realtimeTranscripts: {enable: false},
665
+ suggestedResponses: {enable: false},
666
+ } as any;
667
+ mockAgentProfile.isWellnessBreakEnabled = true;
668
+ jest.spyOn(webex.internal.mercury, 'connect').mockResolvedValue(true);
669
+ jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
670
+ data: {
671
+ auxCodeId: 'auxCodeId',
672
+ agentId: 'agentId',
673
+ deviceType: LoginOption.EXTENSION,
674
+ dn: '12345',
675
+ },
676
+ });
677
+ jest.spyOn(webex.cc.services.config, 'getAgentConfig').mockResolvedValue(mockAgentProfile);
678
+ mockWebSocketManager.initWebSocket.mockResolvedValue({agentId: 'agent123'});
679
+
680
+ await webex.cc.register();
681
+
682
+ expect(webex.cc.services.rtdWebSocketManager.initWebSocket).toHaveBeenCalledWith({
683
+ body: {
684
+ force: true,
685
+ isKeepAliveEnabled: false,
686
+ clientType: 'WebexCCSDK',
687
+ allowMultiLogin: false,
688
+ },
689
+ resource: 'v1/realtime/subscribe',
690
+ });
691
+ expect(webex.cc.services.rtdWebSocketManager.on).toHaveBeenCalledWith(
692
+ 'message',
693
+ expect.any(Function)
694
+ );
695
+ });
696
+
697
+ it('should not connect RTD websocket when transcripts, suggestions, and wellness are disabled', async () => {
698
+ mockAgentProfile.aiFeature = {
699
+ realtimeTranscripts: {enable: false},
700
+ suggestedResponses: {enable: false},
701
+ } as any;
702
+ mockAgentProfile.isWellnessBreakEnabled = false;
539
703
  jest.spyOn(webex.internal.mercury, 'connect').mockResolvedValue(true);
540
704
  jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
541
705
  data: {
@@ -631,7 +795,7 @@ describe('webex.cc', () => {
631
795
  expect(registerWebCallingLineSpy).toHaveBeenCalled();
632
796
  expect(stationLoginMock).toHaveBeenCalledWith({
633
797
  data: {
634
- dialNumber: 'agentId',
798
+ dialNumber: `${WEB_RTC_PREFIX}agentId`,
635
799
  teamId: 'teamId',
636
800
  deviceType: LoginOption.BROWSER,
637
801
  isExtension: false,
@@ -649,6 +813,7 @@ describe('webex.cc', () => {
649
813
  METRIC_EVENT_NAMES.STATION_LOGIN_FAILED,
650
814
  ]);
651
815
  expect(result).toEqual(responseMock);
816
+ expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.BROWSER);
652
817
 
653
818
  const onSpy = jest.spyOn(mockTaskManager, 'on');
654
819
  const emitSpy = jest.spyOn(webex.cc, 'trigger');
@@ -661,10 +826,8 @@ describe('webex.cc', () => {
661
826
 
662
827
  expect(emitSpy).toHaveBeenCalledTimes(1);
663
828
  expect(emitSpy).toHaveBeenCalledWith(TASK_EVENTS.TASK_INCOMING, mockTask);
664
- // Verify message event listener
665
- const messageCallback = mockWebSocketManager.on.mock.calls.find(
666
- (call) => call[0] === 'message'
667
- )[1];
829
+ // Verify websocket message handling
830
+ const messageCallback = webex.cc['handleWebsocketMessage'];
668
831
  const agentStateChangeEventData = {
669
832
  type: CC_EVENTS.AGENT_STATE_CHANGE,
670
833
  data: {some: 'data'},
@@ -692,6 +855,59 @@ describe('webex.cc', () => {
692
855
  );
693
856
  });
694
857
 
858
+ it('should skip web calling line registration when disableWebRTCRegistration is enabled', async () => {
859
+ const options = {
860
+ teamId: 'teamId',
861
+ loginOption: LoginOption.BROWSER,
862
+ };
863
+
864
+ webex.cc.$config = {
865
+ ...webex.cc.$config,
866
+ disableWebRTCRegistration: true,
867
+ };
868
+ webex.cc.agentConfig = {
869
+ agentId: 'agentId',
870
+ webRtcEnabled: true,
871
+ loginVoiceOptions: ['BROWSER', 'EXTENSION', 'AGENT_DN'],
872
+ };
873
+
874
+ const registerWebCallingLineSpy = jest.spyOn(
875
+ webex.cc.webCallingService,
876
+ 'registerWebCallingLine'
877
+ );
878
+
879
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
880
+ data: {
881
+ loginOption: LoginOption.BROWSER,
882
+ agentId: 'agentId',
883
+ teamId: 'teamId',
884
+ siteId: 'siteId',
885
+ roles: [AGENT],
886
+ channelsMap: {
887
+ chat: [],
888
+ email: [],
889
+ social: [],
890
+ telephony: [],
891
+ },
892
+ },
893
+ trackingId: 'notifs_52628',
894
+ orgId: 'orgId',
895
+ type: 'StationLoginSuccess',
896
+ eventType: 'STATION_LOGIN',
897
+ } as unknown as StationLoginSuccess);
898
+
899
+ await webex.cc.stationLogin(options);
900
+
901
+ expect(registerWebCallingLineSpy).not.toHaveBeenCalled();
902
+ expect(LoggerProxy.info).toHaveBeenCalledWith(
903
+ 'Skipping web calling line registration because disableWebRTCRegistration is enabled',
904
+ {
905
+ module: CC_FILE,
906
+ method: 'stationLogin',
907
+ }
908
+ );
909
+ });
910
+
695
911
  it('should not attempt mobius registration for LoginOption.BROWSER if webrtc is disabled', async () => {
696
912
  const options = {
697
913
  teamId: 'teamId',
@@ -739,7 +955,7 @@ describe('webex.cc', () => {
739
955
  expect(registerWebCallingLineSpy).not.toHaveBeenCalled();
740
956
  expect(stationLoginSpy).toHaveBeenCalledWith({
741
957
  data: {
742
- dialNumber: 'agentId',
958
+ dialNumber: `${WEB_RTC_PREFIX}agentId`,
743
959
  teamId: 'teamId',
744
960
  deviceType: LoginOption.BROWSER,
745
961
  isExtension: false,
@@ -814,10 +1030,13 @@ describe('webex.cc', () => {
814
1030
  const result = await webex.cc.stationLogin(options);
815
1031
 
816
1032
  // Verify logging calls
817
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station login', {
818
- module: CC_FILE,
819
- method: 'stationLogin',
820
- });
1033
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
1034
+ `Starting agent station login | loginOption: ${options.loginOption} teamId: ${options.teamId}`,
1035
+ {
1036
+ module: CC_FILE,
1037
+ method: 'stationLogin',
1038
+ }
1039
+ );
821
1040
  expect(LoggerProxy.log).toHaveBeenCalledWith(
822
1041
  `Agent station login completed successfully agentId: ${mockData.data.agentId} loginOption: ${mockData.data.loginOption} teamId: ${mockData.data.teamId}`,
823
1042
  {
@@ -868,97 +1087,1048 @@ describe('webex.cc', () => {
868
1087
 
869
1088
  await expect(webex.cc.stationLogin(options)).rejects.toThrow(error.details.data.reason);
870
1089
 
871
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station login', {
872
- module: CC_FILE,
873
- method: 'stationLogin',
874
- });
1090
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
1091
+ `Starting agent station login | loginOption: ${options.loginOption} teamId: ${options.teamId}`,
1092
+ {
1093
+ module: CC_FILE,
1094
+ method: 'stationLogin',
1095
+ }
1096
+ );
875
1097
  expect(LoggerProxy.error).toHaveBeenCalledWith(
876
1098
  `stationLogin failed with reason: ${error.details.data.reason}`,
877
1099
  {module: CC_FILE, method: 'stationLogin', trackingId: error.details.trackingId}
878
1100
  );
879
1101
  });
880
- });
881
1102
 
882
- describe('stationLogout', () => {
883
- it('should logout successfully', async () => {
884
- const data = {logoutReason: 'Logout reason'};
885
- const response = {};
1103
+ it('should publish usersub and subscribe Mercury on station login when enableWxBetterTogether is true at init', async () => {
1104
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1105
+ webex.internal.device = {
1106
+ userId: 'user-123',
1107
+ url: 'https://wdm.example.com/devices/dev-1',
1108
+ registered: true,
1109
+ register: jest.fn().mockResolvedValue(undefined),
1110
+ };
1111
+ webex.internal.mercury.connected = true;
1112
+ webex.internal.mercury.connect = jest.fn().mockResolvedValue(undefined);
1113
+ webex.cc.agentConfig = {
1114
+ agentId: 'agentId',
1115
+ webRtcEnabled: false,
1116
+ loginVoiceOptions: ['EXTENSION'],
1117
+ };
1118
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
886
1119
 
887
- const stationLogoutMock = jest
888
- .spyOn(webex.cc.services.agent, 'logout')
889
- .mockResolvedValue({} as StationLogoutResponse);
1120
+ const publishSpy = jest
1121
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1122
+ .mockResolvedValue(undefined);
1123
+ const subscribeSpy = jest
1124
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
1125
+ .mockImplementation(() => {});
1126
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
890
1127
 
891
- const result = await webex.cc.stationLogout(data);
1128
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1129
+ data: {
1130
+ agentId: 'agentId',
1131
+ teamId: 'teamId',
1132
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1133
+ },
1134
+ trackingId: 'track-1',
1135
+ } as StationLoginSuccess);
892
1136
 
893
- // Verify logging calls
894
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
895
- module: CC_FILE,
896
- method: 'stationLogout',
897
- });
898
- expect(LoggerProxy.log).toHaveBeenCalledWith('Agent station logout completed successfully', {
899
- module: CC_FILE,
900
- method: 'stationLogout',
1137
+ await webex.cc.stationLogin({
1138
+ teamId: 'teamId',
1139
+ loginOption: LoginOption.EXTENSION,
1140
+ dialNumber: '1001',
901
1141
  });
902
1142
 
903
- expect(stationLogoutMock).toHaveBeenCalledWith({data: data});
904
- // TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
905
- // expect(mockTaskManager.unregisterIncomingCallEvent).toHaveBeenCalledWith();
906
- // expect(mockTaskManager.off).toHaveBeenCalledWith(
907
- // TASK_EVENTS.TASK_INCOMING,
908
- // expect.any(Function)
909
- // );
910
- // expect(mockTaskManager.off).toHaveBeenCalledWith(
911
- // TASK_EVENTS.TASK_HYDRATE,
912
- // expect.any(Function)
913
- // );
914
- // expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
915
- expect(result).toEqual(response);
916
- expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
917
- METRIC_EVENT_NAMES.STATION_LOGOUT_SUCCESS,
918
- METRIC_EVENT_NAMES.STATION_LOGOUT_FAILED,
919
- ]);
1143
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
1144
+ expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
1145
+ expect(mockTaskManager.refreshWxAppOfferObservabilityForAllTasks).toHaveBeenCalled();
920
1146
  });
921
1147
 
922
- it('should handle error during stationLogout', async () => {
923
- const data = {logoutReason: 'Logout reason'};
924
- const error = {
925
- details: {
926
- trackingId: '1234',
927
- data: {
928
- reason: 'Error while performing station logout',
929
- },
930
- },
1148
+ it('should rollback wxApp config when post-station-login init fails', async () => {
1149
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1150
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1151
+ webex.cc.agentConfig = {
1152
+ agentId: 'agentId',
1153
+ webRtcEnabled: false,
1154
+ loginVoiceOptions: ['EXTENSION'],
931
1155
  };
1156
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1157
+ mockTaskManager.applyEnableWxBetterTogether(true);
932
1158
 
933
- jest.spyOn(webex.cc.services.agent, 'logout').mockRejectedValue(error);
1159
+ jest
1160
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
1161
+ .mockRejectedValue(new Error('usersub publish failed'));
1162
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
934
1163
 
935
- await expect(webex.cc.stationLogout(data)).rejects.toThrow(error.details.data.reason);
1164
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1165
+ data: {
1166
+ agentId: 'agentId',
1167
+ teamId: 'teamId',
1168
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1169
+ },
1170
+ trackingId: 'track-1',
1171
+ } as StationLoginSuccess);
936
1172
 
937
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
938
- module: CC_FILE,
939
- method: 'stationLogout',
1173
+ await webex.cc.stationLogin({
1174
+ teamId: 'teamId',
1175
+ loginOption: LoginOption.EXTENSION,
1176
+ dialNumber: '1001',
940
1177
  });
941
- expect(LoggerProxy.error).toHaveBeenCalledWith(
942
- `stationLogout failed with reason: ${error.details.data.reason}`,
943
- {module: CC_FILE, method: 'stationLogout', trackingId: error.details.trackingId}
944
- );
1178
+
1179
+ expect(unsubscribeSpy).toHaveBeenCalled();
1180
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1181
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
945
1182
  });
946
- });
947
1183
 
948
- describe('setAgentStatus', () => {
949
- it('should set agent status successfully when status is Available', async () => {
950
- const expectedPayload = {
951
- state: 'Available',
952
- auxCodeId: '0',
953
- agentId: '123',
954
- lastStateChangeReason: 'Agent is available',
1184
+ it('should reset wxApp config when Mercury fails before usersub publish on station login', async () => {
1185
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1186
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1187
+ webex.cc.agentConfig = {
1188
+ agentId: 'agentId',
1189
+ webRtcEnabled: false,
1190
+ loginVoiceOptions: ['EXTENSION'],
955
1191
  };
1192
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1193
+ mockTaskManager.applyEnableWxBetterTogether(true);
1194
+
1195
+ const publishSpy = jest
1196
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1197
+ .mockResolvedValue(undefined);
1198
+ jest
1199
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
1200
+ .mockRejectedValue(new Error('mercury connect failed'));
1201
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1202
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1203
+ jest
1204
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1205
+ .mockResolvedValue(undefined);
1206
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
956
1207
 
957
- const setAgentStatusMock = jest
958
- .spyOn(webex.cc.services.agent, 'stateChange')
959
- .mockResolvedValue({data: expectedPayload});
1208
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1209
+ data: {
1210
+ agentId: 'agentId',
1211
+ teamId: 'teamId',
1212
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1213
+ },
1214
+ trackingId: 'track-1',
1215
+ } as StationLoginSuccess);
960
1216
 
961
- const result = await webex.cc.setAgentState(expectedPayload);
1217
+ await webex.cc.stationLogin({
1218
+ teamId: 'teamId',
1219
+ loginOption: LoginOption.EXTENSION,
1220
+ dialNumber: '1001',
1221
+ });
1222
+
1223
+ expect(publishSpy).not.toHaveBeenCalled();
1224
+ expect(teardownSpy).not.toHaveBeenCalled();
1225
+ expect(unsubscribeSpy).toHaveBeenCalled();
1226
+ expect(metricSpy).toHaveBeenCalledWith(
1227
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1228
+ expect.objectContaining({skipReason: 'mercury_subscribe_failed'}),
1229
+ ['operational', 'behavioral']
1230
+ );
1231
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1232
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
1233
+ });
1234
+
1235
+ it('should record publish_failed when usersub publish fails after Mercury init', async () => {
1236
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1237
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1238
+ webex.cc.agentConfig = {
1239
+ agentId: 'agentId',
1240
+ webRtcEnabled: false,
1241
+ loginVoiceOptions: ['EXTENSION'],
1242
+ };
1243
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1244
+ mockTaskManager.applyEnableWxBetterTogether(true);
1245
+
1246
+ jest.spyOn(webex.cc as any, 'ensureWxAppMercuryConnected').mockResolvedValue(undefined);
1247
+ jest
1248
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
1249
+ .mockImplementation(() => {});
1250
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1251
+ jest
1252
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1253
+ .mockRejectedValue(new Error('publish failed'));
1254
+ jest
1255
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1256
+ .mockResolvedValue(undefined);
1257
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1258
+
1259
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1260
+ data: {
1261
+ agentId: 'agentId',
1262
+ teamId: 'teamId',
1263
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1264
+ },
1265
+ trackingId: 'track-1',
1266
+ } as StationLoginSuccess);
1267
+
1268
+ await webex.cc.stationLogin({
1269
+ teamId: 'teamId',
1270
+ loginOption: LoginOption.EXTENSION,
1271
+ dialNumber: '1001',
1272
+ });
1273
+
1274
+ expect(metricSpy).toHaveBeenCalledWith(
1275
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1276
+ expect.objectContaining({skipReason: 'publish_failed', usersubPublished: false}),
1277
+ ['operational', 'behavioral']
1278
+ );
1279
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1280
+ });
1281
+
1282
+ it('should report retained usersub state when publish fails during re-init', async () => {
1283
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1284
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1285
+ webex.cc.agentConfig = {
1286
+ agentId: 'agentId',
1287
+ webRtcEnabled: false,
1288
+ loginVoiceOptions: ['EXTENSION'],
1289
+ };
1290
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1291
+ mockTaskManager.applyEnableWxBetterTogether(true);
1292
+
1293
+ jest.spyOn(webex.cc as any, 'ensureWxAppMercuryConnected').mockResolvedValue(undefined);
1294
+ jest
1295
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
1296
+ .mockImplementation(() => {});
1297
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1298
+ jest
1299
+ .spyOn(webex.cc['webexCrossClientService'], 'isAnswerCallsStateActive')
1300
+ .mockReturnValue(true);
1301
+ jest
1302
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1303
+ .mockRejectedValue(new Error('publish failed'));
1304
+ jest
1305
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1306
+ .mockResolvedValue(undefined);
1307
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1308
+
1309
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1310
+ data: {
1311
+ agentId: 'agentId',
1312
+ teamId: 'teamId',
1313
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1314
+ },
1315
+ trackingId: 'track-1',
1316
+ } as StationLoginSuccess);
1317
+
1318
+ await webex.cc.stationLogin({
1319
+ teamId: 'teamId',
1320
+ loginOption: LoginOption.EXTENSION,
1321
+ dialNumber: '1001',
1322
+ });
1323
+
1324
+ expect(metricSpy).toHaveBeenCalledWith(
1325
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1326
+ expect.objectContaining({skipReason: 'publish_failed', usersubPublished: true}),
1327
+ ['operational', 'behavioral']
1328
+ );
1329
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1330
+ });
1331
+
1332
+ it('should register device before publishing usersub true on extension station login', async () => {
1333
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1334
+ webex.internal.device = {
1335
+ userId: 'user-123',
1336
+ registered: false,
1337
+ register: jest.fn().mockImplementation(async function (this: {
1338
+ url?: string;
1339
+ registered?: boolean;
1340
+ }) {
1341
+ this.url = 'https://wdm.example.com/devices/dev-1';
1342
+ this.registered = true;
1343
+ }),
1344
+ };
1345
+ webex.internal.mercury = {
1346
+ connected: false,
1347
+ connect: jest.fn().mockResolvedValue(undefined),
1348
+ };
1349
+ webex.cc.agentConfig = {
1350
+ agentId: 'agentId',
1351
+ webRtcEnabled: false,
1352
+ loginVoiceOptions: ['EXTENSION'],
1353
+ };
1354
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1355
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1356
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1357
+
1358
+ const callOrder: string[] = [];
1359
+ jest.spyOn(webex.internal.device, 'register').mockImplementation(async function (this: {
1360
+ url?: string;
1361
+ registered?: boolean;
1362
+ }) {
1363
+ callOrder.push('device.register');
1364
+ this.url = 'https://wdm.example.com/devices/dev-1';
1365
+ this.registered = true;
1366
+ });
1367
+ jest
1368
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1369
+ .mockImplementation(async (enable: boolean) => {
1370
+ if (enable === true && !webex.internal.device.url) {
1371
+ throw new Error('Device URL is unavailable for cross-client publish');
1372
+ }
1373
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
1374
+ });
1375
+
1376
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1377
+ data: {
1378
+ agentId: 'agentId',
1379
+ teamId: 'teamId',
1380
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1381
+ },
1382
+ trackingId: 'track-1',
1383
+ } as StationLoginSuccess);
1384
+
1385
+ await webex.cc.stationLogin({
1386
+ teamId: 'teamId',
1387
+ loginOption: LoginOption.EXTENSION,
1388
+ dialNumber: '1001',
1389
+ });
1390
+
1391
+ expect(callOrder).toEqual(['device.register', 'publish:true']);
1392
+ });
1393
+
1394
+ it('should register device before publishing forced usersub false when wxApp is disabled at station login', async () => {
1395
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1396
+ webex.internal.device = {
1397
+ userId: 'user-123',
1398
+ registered: false,
1399
+ register: jest.fn().mockImplementation(async function (this: {
1400
+ url?: string;
1401
+ registered?: boolean;
1402
+ }) {
1403
+ this.url = 'https://wdm.example.com/devices/dev-1';
1404
+ this.registered = true;
1405
+ }),
1406
+ };
1407
+ webex.internal.mercury = {
1408
+ connected: false,
1409
+ connect: jest.fn().mockResolvedValue(undefined),
1410
+ };
1411
+ webex.cc.agentConfig = {
1412
+ agentId: 'agentId',
1413
+ webRtcEnabled: false,
1414
+ loginVoiceOptions: ['EXTENSION'],
1415
+ };
1416
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1417
+ jest
1418
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1419
+ .mockResolvedValue(undefined);
1420
+
1421
+ const callOrder: string[] = [];
1422
+ jest.spyOn(webex.internal.device, 'register').mockImplementation(async function (this: {
1423
+ url?: string;
1424
+ registered?: boolean;
1425
+ }) {
1426
+ callOrder.push('device.register');
1427
+ this.url = 'https://wdm.example.com/devices/dev-1';
1428
+ this.registered = true;
1429
+ });
1430
+ jest
1431
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1432
+ .mockImplementation(async (enable: boolean) => {
1433
+ if (!webex.internal.device.url) {
1434
+ throw new Error('Device URL is unavailable for cross-client publish');
1435
+ }
1436
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
1437
+ });
1438
+
1439
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1440
+ data: {
1441
+ agentId: 'agentId',
1442
+ teamId: 'teamId',
1443
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1444
+ },
1445
+ trackingId: 'track-1',
1446
+ } as StationLoginSuccess);
1447
+
1448
+ await webex.cc.stationLogin({
1449
+ teamId: 'teamId',
1450
+ loginOption: LoginOption.EXTENSION,
1451
+ dialNumber: '1001',
1452
+ });
1453
+
1454
+ expect(callOrder).toEqual(['device.register', 'publish:false']);
1455
+ });
1456
+
1457
+ it('should retry forced false publish when wxApp is disabled at station login', async () => {
1458
+ jest.useFakeTimers();
1459
+ try {
1460
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1461
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1462
+ webex.cc.agentConfig = {
1463
+ agentId: 'agentId',
1464
+ webRtcEnabled: false,
1465
+ loginVoiceOptions: ['EXTENSION'],
1466
+ };
1467
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1468
+
1469
+ const publishSpy = jest
1470
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1471
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
1472
+ .mockResolvedValue(undefined);
1473
+ const scheduleRetrySpy = jest.spyOn(
1474
+ webex.cc as any,
1475
+ 'scheduleForcedFalsePublishRetry'
1476
+ );
1477
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1478
+ jest
1479
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1480
+ .mockResolvedValue(undefined);
1481
+
1482
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1483
+ data: {
1484
+ agentId: 'agentId',
1485
+ teamId: 'teamId',
1486
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1487
+ },
1488
+ trackingId: 'track-1',
1489
+ } as StationLoginSuccess);
1490
+
1491
+ await webex.cc.stationLogin({
1492
+ teamId: 'teamId',
1493
+ loginOption: LoginOption.EXTENSION,
1494
+ dialNumber: '1001',
1495
+ });
1496
+
1497
+ expect(publishSpy).toHaveBeenCalledTimes(1);
1498
+ expect(scheduleRetrySpy).toHaveBeenCalled();
1499
+ expect(teardownSpy).not.toHaveBeenCalled();
1500
+
1501
+ jest.advanceTimersByTime(30_000);
1502
+ await publishSpy.mock.results[1]?.value;
1503
+
1504
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1505
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1506
+ } finally {
1507
+ jest.useRealTimers();
1508
+ }
1509
+ });
1510
+
1511
+ it('should not publish usersub on BROWSER station login even when enableWxBetterTogether is true at init', async () => {
1512
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1513
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1514
+ webex.cc.agentConfig = {
1515
+ agentId: 'agentId',
1516
+ webRtcEnabled: true,
1517
+ loginVoiceOptions: ['BROWSER'],
1518
+ };
1519
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
1520
+
1521
+ const publishSpy = jest.spyOn(
1522
+ webex.cc['webexCrossClientService'],
1523
+ 'setManageWebexCallingInWxcc'
1524
+ );
1525
+
1526
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1527
+ data: {
1528
+ agentId: 'agentId',
1529
+ teamId: 'teamId',
1530
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1531
+ },
1532
+ trackingId: 'track-1',
1533
+ } as StationLoginSuccess);
1534
+
1535
+ await webex.cc.stationLogin({
1536
+ teamId: 'teamId',
1537
+ loginOption: LoginOption.BROWSER,
1538
+ });
1539
+
1540
+ expect(publishSpy).not.toHaveBeenCalled();
1541
+ });
1542
+
1543
+ it('should force-publish usersub false on station login when enableWxBetterTogether is false at init', async () => {
1544
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1545
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1546
+ webex.cc.agentConfig = {
1547
+ agentId: 'agentId',
1548
+ webRtcEnabled: false,
1549
+ loginVoiceOptions: ['EXTENSION'],
1550
+ };
1551
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1552
+
1553
+ const publishSpy = jest
1554
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1555
+ .mockResolvedValue(undefined);
1556
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1557
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1558
+
1559
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1560
+ data: {
1561
+ agentId: 'agentId',
1562
+ teamId: 'teamId',
1563
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1564
+ },
1565
+ trackingId: 'track-1',
1566
+ } as StationLoginSuccess);
1567
+
1568
+ await webex.cc.stationLogin({
1569
+ teamId: 'teamId',
1570
+ loginOption: LoginOption.EXTENSION,
1571
+ dialNumber: '1001',
1572
+ });
1573
+
1574
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1575
+ expect(teardownSpy).toHaveBeenCalled();
1576
+ expect(unsubscribeSpy).toHaveBeenCalled();
1577
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
1578
+ METRIC_EVENT_NAMES.WXAPP_SESSION_SKIPPED,
1579
+ expect.objectContaining({
1580
+ enableWxBetterTogether: false,
1581
+ skipReason: 'flag_disabled',
1582
+ }),
1583
+ ['operational', 'behavioral']
1584
+ );
1585
+ });
1586
+
1587
+ it('should not emit WXAPP_SESSION_INIT_FAILED when flag-disabled cleanup fails', async () => {
1588
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1589
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1590
+ webex.cc.agentConfig = {
1591
+ agentId: 'agentId',
1592
+ webRtcEnabled: false,
1593
+ loginVoiceOptions: ['EXTENSION'],
1594
+ };
1595
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1596
+
1597
+ jest
1598
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1599
+ .mockRejectedValue(new Error('forced false publish failed'));
1600
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1601
+
1602
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1603
+ data: {
1604
+ agentId: 'agentId',
1605
+ teamId: 'teamId',
1606
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1607
+ },
1608
+ trackingId: 'track-1',
1609
+ } as StationLoginSuccess);
1610
+
1611
+ await webex.cc.stationLogin({
1612
+ teamId: 'teamId',
1613
+ loginOption: LoginOption.EXTENSION,
1614
+ dialNumber: '1001',
1615
+ });
1616
+
1617
+ expect(metricSpy).not.toHaveBeenCalledWith(
1618
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1619
+ expect.anything(),
1620
+ expect.anything()
1621
+ );
1622
+ });
1623
+
1624
+ it('should still publish usersub but record session init failed when Mercury is unavailable', async () => {
1625
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1626
+ webex.internal.device = {
1627
+ userId: 'user-123',
1628
+ url: 'https://wdm.example.com/devices/dev-1',
1629
+ registered: true,
1630
+ register: jest.fn().mockResolvedValue(undefined),
1631
+ };
1632
+ delete webex.internal.mercury;
1633
+ webex.cc.agentConfig = {
1634
+ agentId: 'agentId',
1635
+ webRtcEnabled: false,
1636
+ loginVoiceOptions: ['EXTENSION'],
1637
+ };
1638
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1639
+ mockTaskManager.applyEnableWxBetterTogether(true);
1640
+
1641
+ const publishSpy = jest
1642
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1643
+ .mockResolvedValue(undefined);
1644
+ jest
1645
+ .spyOn(webex.cc['webexCrossClientService'], 'isAnswerCallsStateActive')
1646
+ .mockReturnValue(true);
1647
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1648
+
1649
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1650
+ data: {
1651
+ agentId: 'agentId',
1652
+ teamId: 'teamId',
1653
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1654
+ },
1655
+ trackingId: 'track-1',
1656
+ } as StationLoginSuccess);
1657
+
1658
+ await webex.cc.stationLogin({
1659
+ teamId: 'teamId',
1660
+ loginOption: LoginOption.EXTENSION,
1661
+ dialNumber: '1001',
1662
+ });
1663
+
1664
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
1665
+ expect(metricSpy).toHaveBeenCalledWith(
1666
+ METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_FAILED,
1667
+ expect.objectContaining({error: 'Mercury is unavailable for wxApp mute sync'}),
1668
+ ['operational', 'behavioral']
1669
+ );
1670
+ expect(metricSpy).not.toHaveBeenCalledWith(
1671
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_SUCCESS,
1672
+ expect.anything(),
1673
+ expect.anything()
1674
+ );
1675
+ expect(metricSpy).toHaveBeenCalledWith(
1676
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1677
+ expect.objectContaining({skipReason: 'mercury_not_subscribed'}),
1678
+ ['operational', 'behavioral']
1679
+ );
1680
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
1681
+ });
1682
+
1683
+ it('should record session init failed when userId is missing but not emit success', async () => {
1684
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1685
+ webex.internal.device = {
1686
+ url: 'https://wdm.example.com/devices/dev-1',
1687
+ registered: true,
1688
+ register: jest.fn().mockResolvedValue(undefined),
1689
+ };
1690
+ webex.internal.mercury = {
1691
+ connected: true,
1692
+ connect: jest.fn().mockResolvedValue(undefined),
1693
+ on: jest.fn(),
1694
+ off: jest.fn(),
1695
+ };
1696
+ webex.cc.agentConfig = {
1697
+ agentId: 'agentId',
1698
+ webRtcEnabled: false,
1699
+ loginVoiceOptions: ['EXTENSION'],
1700
+ };
1701
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1702
+ mockTaskManager.applyEnableWxBetterTogether(true);
1703
+
1704
+ const publishSpy = jest
1705
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1706
+ .mockResolvedValue(undefined);
1707
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1708
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1709
+
1710
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1711
+ data: {
1712
+ agentId: 'agentId',
1713
+ teamId: 'teamId',
1714
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1715
+ },
1716
+ trackingId: 'track-1',
1717
+ } as StationLoginSuccess);
1718
+
1719
+ await webex.cc.stationLogin({
1720
+ teamId: 'teamId',
1721
+ loginOption: LoginOption.EXTENSION,
1722
+ dialNumber: '1001',
1723
+ });
1724
+
1725
+ expect(publishSpy).not.toHaveBeenCalledWith(true, expect.anything());
1726
+ expect(metricSpy).not.toHaveBeenCalledWith(
1727
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_SUCCESS,
1728
+ expect.anything(),
1729
+ expect.anything()
1730
+ );
1731
+ expect(metricSpy).toHaveBeenCalledWith(
1732
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1733
+ expect.objectContaining({skipReason: 'usersub_not_published'}),
1734
+ ['operational', 'behavioral']
1735
+ );
1736
+ expect(metricSpy).toHaveBeenCalledWith(
1737
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
1738
+ expect.objectContaining({skipReason: 'user_id_unavailable', usersubPublished: false}),
1739
+ ['operational', 'behavioral']
1740
+ );
1741
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
1742
+ });
1743
+
1744
+ it('should report retained usersub state when userId is missing during re-init', async () => {
1745
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1746
+ webex.internal.device = {
1747
+ url: 'https://wdm.example.com/devices/dev-1',
1748
+ registered: true,
1749
+ register: jest.fn().mockResolvedValue(undefined),
1750
+ };
1751
+ webex.internal.mercury = {
1752
+ connected: true,
1753
+ connect: jest.fn().mockResolvedValue(undefined),
1754
+ on: jest.fn(),
1755
+ off: jest.fn(),
1756
+ };
1757
+ webex.cc.agentConfig = {
1758
+ agentId: 'agentId',
1759
+ webRtcEnabled: false,
1760
+ loginVoiceOptions: ['EXTENSION'],
1761
+ };
1762
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1763
+ mockTaskManager.applyEnableWxBetterTogether(true);
1764
+
1765
+ jest
1766
+ .spyOn(webex.cc['webexCrossClientService'], 'isAnswerCallsStateActive')
1767
+ .mockReturnValue(true);
1768
+ const publishSpy = jest
1769
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1770
+ .mockResolvedValue(undefined);
1771
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1772
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1773
+
1774
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1775
+ data: {
1776
+ agentId: 'agentId',
1777
+ teamId: 'teamId',
1778
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1779
+ },
1780
+ trackingId: 'track-1',
1781
+ } as StationLoginSuccess);
1782
+
1783
+ await webex.cc.stationLogin({
1784
+ teamId: 'teamId',
1785
+ loginOption: LoginOption.EXTENSION,
1786
+ dialNumber: '1001',
1787
+ });
1788
+
1789
+ expect(publishSpy).not.toHaveBeenCalledWith(true, expect.anything());
1790
+ expect(metricSpy).toHaveBeenCalledWith(
1791
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
1792
+ expect.objectContaining({skipReason: 'user_id_unavailable', usersubPublished: true}),
1793
+ ['operational', 'behavioral']
1794
+ );
1795
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
1796
+ });
1797
+
1798
+ it('should clear stale usersub on login after prior session enabled wxApp then hard refresh', async () => {
1799
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1800
+ webex.cc.agentConfig = {
1801
+ agentId: 'agentId',
1802
+ webRtcEnabled: false,
1803
+ loginVoiceOptions: ['EXTENSION'],
1804
+ };
1805
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1806
+ webex.request = jest.fn().mockResolvedValue({body: {}});
1807
+
1808
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1809
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1810
+ data: {
1811
+ agentId: 'agentId',
1812
+ teamId: 'teamId',
1813
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1814
+ },
1815
+ trackingId: 'track-1',
1816
+ } as StationLoginSuccess);
1817
+
1818
+ await webex.cc.stationLogin({
1819
+ teamId: 'teamId',
1820
+ loginOption: LoginOption.EXTENSION,
1821
+ dialNumber: '1001',
1822
+ });
1823
+
1824
+ webex.cc['webexCrossClientService'].teardown();
1825
+ webex.cc['wxAppTelephonyMercurySync'].unsubscribe();
1826
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1827
+
1828
+ const publishSpy = jest
1829
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1830
+ .mockResolvedValue(undefined);
1831
+
1832
+ await webex.cc.stationLogin({
1833
+ teamId: 'teamId',
1834
+ loginOption: LoginOption.EXTENSION,
1835
+ dialNumber: '1001',
1836
+ });
1837
+
1838
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1839
+ });
1840
+ });
1841
+
1842
+ describe('stationLogout', () => {
1843
+ it('should logout successfully', async () => {
1844
+ const data = {logoutReason: 'Logout reason'};
1845
+ const response = {};
1846
+
1847
+ const stationLogoutMock = jest
1848
+ .spyOn(webex.cc.services.agent, 'logout')
1849
+ .mockResolvedValue({} as StationLogoutResponse);
1850
+
1851
+ const result = await webex.cc.stationLogout(data);
1852
+
1853
+ // Verify logging calls
1854
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
1855
+ module: CC_FILE,
1856
+ method: 'stationLogout',
1857
+ });
1858
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Agent station logout completed successfully', {
1859
+ module: CC_FILE,
1860
+ method: 'stationLogout',
1861
+ });
1862
+
1863
+ expect(stationLogoutMock).toHaveBeenCalledWith({data: data});
1864
+ // TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
1865
+ // expect(mockTaskManager.unregisterIncomingCallEvent).toHaveBeenCalledWith();
1866
+ // expect(mockTaskManager.off).toHaveBeenCalledWith(
1867
+ // TASK_EVENTS.TASK_INCOMING,
1868
+ // expect.any(Function)
1869
+ // );
1870
+ // expect(mockTaskManager.off).toHaveBeenCalledWith(
1871
+ // TASK_EVENTS.TASK_HYDRATE,
1872
+ // expect.any(Function)
1873
+ // );
1874
+ // expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1875
+ expect(result).toEqual(response);
1876
+ expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
1877
+ METRIC_EVENT_NAMES.STATION_LOGOUT_SUCCESS,
1878
+ METRIC_EVENT_NAMES.STATION_LOGOUT_FAILED,
1879
+ ]);
1880
+ });
1881
+
1882
+ it('should not publish usersub false on logout when wxApp was never enabled', async () => {
1883
+ webex.internal.device = {userId: 'user-123'};
1884
+ const publishSpy = jest
1885
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1886
+ .mockResolvedValue(undefined);
1887
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1888
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1889
+
1890
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1891
+ trackingId: 'track-1',
1892
+ } as StationLogoutResponse);
1893
+
1894
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1895
+
1896
+ expect(publishSpy).not.toHaveBeenCalled();
1897
+ expect(teardownSpy).toHaveBeenCalled();
1898
+ expect(unsubscribeSpy).toHaveBeenCalled();
1899
+ });
1900
+
1901
+ it('should publish usersub false and teardown wxApp sync on logout when wxApp was enabled', async () => {
1902
+ webex.internal.device = {
1903
+ userId: 'user-123',
1904
+ url: 'https://wdm.example.com/devices/dev-1',
1905
+ registered: false,
1906
+ register: jest.fn().mockResolvedValue(undefined),
1907
+ unregister: jest.fn().mockResolvedValue(undefined),
1908
+ };
1909
+ webex.internal.mercury = {
1910
+ connected: false,
1911
+ connect: jest.fn().mockResolvedValue(undefined),
1912
+ disconnect: jest.fn().mockResolvedValue(undefined),
1913
+ };
1914
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1915
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1916
+ webex.request = jest.fn().mockResolvedValue({body: {}});
1917
+ webex.cc['agentConfig'] = {
1918
+ agentId: 'agent-123',
1919
+ deviceType: LoginOption.EXTENSION,
1920
+ } as Profile;
1921
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1922
+
1923
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
1924
+
1925
+ const publishSpy = jest
1926
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1927
+ .mockResolvedValue(undefined);
1928
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1929
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1930
+
1931
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1932
+ trackingId: 'track-1',
1933
+ } as StationLogoutResponse);
1934
+
1935
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1936
+
1937
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1938
+ expect(teardownSpy).toHaveBeenCalled();
1939
+ expect(unsubscribeSpy).toHaveBeenCalled();
1940
+ });
1941
+
1942
+ it('should teardown wxApp state on logout even when usersub publish fails', async () => {
1943
+ webex.internal.device = {userId: 'user-123'};
1944
+ webex.cc['agentConfig'] = {
1945
+ agentId: 'agent-123',
1946
+ deviceType: LoginOption.EXTENSION,
1947
+ } as Profile;
1948
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1949
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1950
+ mockTaskManager.applyEnableWxBetterTogether(true);
1951
+
1952
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1953
+ trackingId: 'track-1',
1954
+ } as StationLogoutResponse);
1955
+ jest
1956
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
1957
+ .mockRejectedValue(new Error('usersub publish failed'));
1958
+ const scheduleRetrySpy = jest.spyOn(
1959
+ webex.cc as any,
1960
+ 'scheduleCompensatingFalsePublishRetry'
1961
+ );
1962
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1963
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1964
+
1965
+ const result = await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1966
+
1967
+ expect(result).toEqual({trackingId: 'track-1'});
1968
+ expect(teardownSpy).toHaveBeenCalled();
1969
+ expect(unsubscribeSpy).toHaveBeenCalled();
1970
+ expect(scheduleRetrySpy).toHaveBeenCalled();
1971
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
1972
+ });
1973
+
1974
+ it('should retry compensating false publish when teardown usersub publish fails on logout', async () => {
1975
+ jest.useFakeTimers();
1976
+ try {
1977
+ webex.internal.device = {
1978
+ userId: 'user-123',
1979
+ url: 'https://wdm.example.com/devices/dev-1',
1980
+ };
1981
+ webex.cc['agentConfig'] = {
1982
+ agentId: 'agent-123',
1983
+ deviceType: LoginOption.EXTENSION,
1984
+ } as Profile;
1985
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1986
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1987
+ mockTaskManager.applyEnableWxBetterTogether(true);
1988
+ webex.cc['webexCrossClientService'].answerCallsState = true;
1989
+
1990
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1991
+ trackingId: 'track-1',
1992
+ } as StationLogoutResponse);
1993
+
1994
+ const publishSpy = jest
1995
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1996
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
1997
+ .mockResolvedValue(undefined);
1998
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1999
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
2000
+ jest
2001
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
2002
+ .mockResolvedValue(undefined);
2003
+
2004
+ const result = await webex.cc.stationLogout({logoutReason: 'Logout reason'});
2005
+
2006
+ expect(result).toEqual({trackingId: 'track-1'});
2007
+ expect(publishSpy).toHaveBeenCalledTimes(1);
2008
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
2009
+ expect(teardownSpy).toHaveBeenCalled();
2010
+ expect(unsubscribeSpy).toHaveBeenCalled();
2011
+
2012
+ jest.advanceTimersByTime(30_000);
2013
+ await publishSpy.mock.results[1]?.value;
2014
+
2015
+ expect(publishSpy).toHaveBeenCalledTimes(2);
2016
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
2017
+ } finally {
2018
+ jest.useRealTimers();
2019
+ }
2020
+ });
2021
+
2022
+ it('should cancel compensating retry when station login succeeds before retry fires', async () => {
2023
+ jest.useFakeTimers();
2024
+ try {
2025
+ webex.internal.device = {
2026
+ userId: 'user-123',
2027
+ url: 'https://wdm.example.com/devices/dev-1',
2028
+ registered: false,
2029
+ register: jest.fn().mockResolvedValue(undefined),
2030
+ unregister: jest.fn().mockResolvedValue(undefined),
2031
+ };
2032
+ webex.internal.mercury = {
2033
+ connected: false,
2034
+ connect: jest.fn().mockResolvedValue(undefined),
2035
+ disconnect: jest.fn().mockResolvedValue(undefined),
2036
+ };
2037
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
2038
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
2039
+ webex.cc['agentConfig'] = {
2040
+ agentId: 'agent-123',
2041
+ deviceType: LoginOption.EXTENSION,
2042
+ webRtcEnabled: false,
2043
+ loginVoiceOptions: ['EXTENSION'],
2044
+ } as Profile;
2045
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
2046
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2047
+ mockTaskManager.applyEnableWxBetterTogether(true);
2048
+ webex.cc['webexCrossClientService'].answerCallsState = true;
2049
+
2050
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
2051
+ trackingId: 'track-1',
2052
+ } as StationLogoutResponse);
2053
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
2054
+ data: {
2055
+ agentId: 'agent-123',
2056
+ teamId: 'team-123',
2057
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
2058
+ },
2059
+ trackingId: 'track-2',
2060
+ } as StationLoginSuccess);
2061
+
2062
+ const publishSpy = jest
2063
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2064
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
2065
+ .mockResolvedValue(undefined);
2066
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
2067
+ jest
2068
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
2069
+ .mockResolvedValue(undefined);
2070
+
2071
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
2072
+ await webex.cc.stationLogin({
2073
+ teamId: 'team-123',
2074
+ loginOption: LoginOption.EXTENSION,
2075
+ dialNumber: '1001',
2076
+ });
2077
+
2078
+ expect(publishSpy).toHaveBeenCalledTimes(2);
2079
+ expect(publishSpy).toHaveBeenNthCalledWith(1, false, {userId: 'user-123', trackPublishMetrics: true});
2080
+ expect(publishSpy).toHaveBeenNthCalledWith(2, true, {userId: 'user-123', trackPublishMetrics: true});
2081
+ expect(teardownSpy).toHaveBeenCalledTimes(1);
2082
+
2083
+ jest.advanceTimersByTime(30_000);
2084
+
2085
+ expect(publishSpy).toHaveBeenCalledTimes(2);
2086
+ expect(teardownSpy).toHaveBeenCalledTimes(1);
2087
+ } finally {
2088
+ jest.useRealTimers();
2089
+ }
2090
+ });
2091
+
2092
+ it('should handle error during stationLogout', async () => {
2093
+ const data = {logoutReason: 'Logout reason'};
2094
+ const error = {
2095
+ details: {
2096
+ trackingId: '1234',
2097
+ data: {
2098
+ reason: 'Error while performing station logout',
2099
+ },
2100
+ },
2101
+ };
2102
+
2103
+ jest.spyOn(webex.cc.services.agent, 'logout').mockRejectedValue(error);
2104
+
2105
+ await expect(webex.cc.stationLogout(data)).rejects.toThrow(error.details.data.reason);
2106
+
2107
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
2108
+ module: CC_FILE,
2109
+ method: 'stationLogout',
2110
+ });
2111
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
2112
+ `stationLogout failed with reason: ${error.details.data.reason}`,
2113
+ {module: CC_FILE, method: 'stationLogout', trackingId: error.details.trackingId}
2114
+ );
2115
+ });
2116
+ });
2117
+
2118
+ describe('setAgentStatus', () => {
2119
+ it('should set agent status successfully when status is Available', async () => {
2120
+ const expectedPayload = {
2121
+ state: 'Available',
2122
+ auxCodeId: '0',
2123
+ agentId: '123',
2124
+ lastStateChangeReason: 'Agent is available',
2125
+ };
2126
+
2127
+ const setAgentStatusMock = jest
2128
+ .spyOn(webex.cc.services.agent, 'stateChange')
2129
+ .mockResolvedValue({data: expectedPayload});
2130
+
2131
+ const result = await webex.cc.setAgentState(expectedPayload);
962
2132
 
963
2133
  // Verify logging calls
964
2134
  expect(LoggerProxy.info).toHaveBeenCalledWith('Setting agent state', {
@@ -1071,6 +2241,257 @@ describe('webex.cc', () => {
1071
2241
  });
1072
2242
  });
1073
2243
 
2244
+ describe('Agent Wellness Break SDK contracts', () => {
2245
+ const wellbeingCode = {
2246
+ id: 'wellbeing-code',
2247
+ name: 'WellbeingBreak',
2248
+ isSystem: true,
2249
+ isDefault: false,
2250
+ };
2251
+
2252
+ beforeEach(() => {
2253
+ webex.cc.agentConfig = {
2254
+ agentId: 'agent-1',
2255
+ isWellnessBreakEnabled: true,
2256
+ } as Profile;
2257
+ webex.cc['updateWellnessSession']('session-1');
2258
+ });
2259
+
2260
+ it('fetches the system idle code once per registration', async () => {
2261
+ const lookup = jest
2262
+ .spyOn(webex.cc.services.config, 'getWellbeingBreakIdleCode')
2263
+ .mockResolvedValue(wellbeingCode);
2264
+
2265
+ await expect(webex.cc.getWellbeingBreakIdleCode()).resolves.toEqual(wellbeingCode);
2266
+ await expect(webex.cc.getWellbeingBreakIdleCode()).resolves.toEqual(wellbeingCode);
2267
+
2268
+ expect(lookup).toHaveBeenCalledTimes(1);
2269
+ expect(lookup).toHaveBeenCalledWith('mockOrgId');
2270
+ });
2271
+
2272
+ it('does not cache an idle-code lookup that completes after a new registration starts', async () => {
2273
+ let resolveLookup: (idleCode: typeof wellbeingCode) => void = () => undefined;
2274
+ jest
2275
+ .spyOn(webex.cc.services.config, 'getWellbeingBreakIdleCode')
2276
+ .mockImplementation(
2277
+ () =>
2278
+ new Promise((resolve) => {
2279
+ resolveLookup = resolve;
2280
+ })
2281
+ );
2282
+
2283
+ const lookupPromise = webex.cc.getWellbeingBreakIdleCode();
2284
+ const staleLookup = expect(lookupPromise).rejects.toThrow(
2285
+ 'WELLNESS_BREAK_REGISTRATION_CHANGED'
2286
+ );
2287
+ jest
2288
+ .spyOn(webex.cc, 'connectWebsocket')
2289
+ .mockRejectedValue(new Error('replacement registration failed'));
2290
+
2291
+ await expect(webex.cc.register()).rejects.toThrow('replacement registration failed');
2292
+ resolveLookup(wellbeingCode);
2293
+
2294
+ await staleLookup;
2295
+ expect(webex.cc['wellbeingBreakIdleCode']).toBeUndefined();
2296
+ });
2297
+
2298
+ it('rejects the system idle-code lookup when wellness is disabled', async () => {
2299
+ webex.cc.agentConfig.isWellnessBreakEnabled = false;
2300
+
2301
+ await expect(webex.cc.getWellbeingBreakIdleCode()).rejects.toThrow(
2302
+ 'WELLNESS_BREAK_NOT_ENABLED'
2303
+ );
2304
+ expect(webex.cc.services.config.getWellbeingBreakIdleCode).not.toHaveBeenCalled();
2305
+ });
2306
+
2307
+ it('invalidates the wellness session only for a current-session multi-login close', () => {
2308
+ webex.cc['handleWebsocketMessage'](
2309
+ JSON.stringify({
2310
+ type: CC_EVENTS.AGENT_MULTI_LOGIN,
2311
+ data: {
2312
+ type: 'AgentMultiLoginCloseSession',
2313
+ agentSessionId: 'another-session',
2314
+ },
2315
+ })
2316
+ );
2317
+ expect(webex.cc['currentAgentSessionId']).toBe('session-1');
2318
+
2319
+ webex.cc['handleWebsocketMessage'](
2320
+ JSON.stringify({
2321
+ type: CC_EVENTS.AGENT_MULTI_LOGIN,
2322
+ data: {
2323
+ type: 'AgentMultiLoginCloseSession',
2324
+ agentSessionId: 'session-1',
2325
+ },
2326
+ })
2327
+ );
2328
+ expect(webex.cc['currentAgentSessionId']).toBeUndefined();
2329
+ });
2330
+
2331
+ it('emits primary wellness notifications regardless of notification session metadata', () => {
2332
+ const emitSpy = jest.spyOn(webex.cc, 'emit');
2333
+ const payload = {
2334
+ type: 'Wellness_Break_Handler',
2335
+ orgId: 'mockOrgId',
2336
+ trackingId: 'data-notification-tracking',
2337
+ data: {
2338
+ agentId: 'agent-1',
2339
+ orgId: 'mockOrgId',
2340
+ notifType: 'Wellness_Break_Handler',
2341
+ notifDetails: {
2342
+ actionEvent: 'SUGGEST_WELLNESS_BREAK',
2343
+ actionText: 'How about a break?',
2344
+ },
2345
+ data: {
2346
+ orgId: 'mockOrgId',
2347
+ agentSessionId: 'notification-session',
2348
+ interactionId: 'interaction-1',
2349
+ },
2350
+ },
2351
+ };
2352
+
2353
+ webex.cc['handleWebsocketMessage'](JSON.stringify(payload));
2354
+
2355
+ expect(emitSpy).toHaveBeenCalledWith(CC_EVENTS.WELLNESS_BREAK, {
2356
+ agentId: 'agent-1',
2357
+ orgId: 'mockOrgId',
2358
+ agentSessionId: 'notification-session',
2359
+ actionEvent: 'SUGGEST_WELLNESS_BREAK',
2360
+ actionText: 'How about a break?',
2361
+ interactionId: 'interaction-1',
2362
+ trackingId: 'data-notification-tracking',
2363
+ });
2364
+ });
2365
+
2366
+ it('routes data-socket wellness notifications without sending them to TaskManager', () => {
2367
+ const emitSpy = jest.spyOn(webex.cc, 'emit');
2368
+ const taskEventSpy = mockTaskManager.handleRealtimeWebsocketEvent;
2369
+ const wellnessMessage = JSON.stringify({
2370
+ type: 'Wellness_Break_Handler',
2371
+ orgId: 'mockOrgId',
2372
+ data: {
2373
+ agentId: 'agent-1',
2374
+ orgId: 'mockOrgId',
2375
+ notifType: 'Wellness_Break_Handler',
2376
+ notifDetails: {actionEvent: 'PROVIDE_WELLNESS_BREAK'},
2377
+ data: {orgId: 'mockOrgId', agentSessionId: 'notification-session'},
2378
+ },
2379
+ });
2380
+
2381
+ webex.cc['handleRTDWebsocketMessage'](wellnessMessage);
2382
+
2383
+ expect(emitSpy).toHaveBeenCalledWith(CC_EVENTS.WELLNESS_BREAK, {
2384
+ agentId: 'agent-1',
2385
+ orgId: 'mockOrgId',
2386
+ agentSessionId: 'notification-session',
2387
+ actionEvent: 'PROVIDE_WELLNESS_BREAK',
2388
+ });
2389
+ expect(taskEventSpy).not.toHaveBeenCalled();
2390
+
2391
+ const transcriptMessage = JSON.stringify({type: CC_EVENTS.REAL_TIME_TRANSCRIPTION});
2392
+ webex.cc['handleRTDWebsocketMessage'](transcriptMessage);
2393
+ expect(taskEventSpy).toHaveBeenCalledWith(transcriptMessage);
2394
+ });
2395
+
2396
+ it.each([
2397
+ ['SUGGEST_WELLNESS_BREAK', 'interactionId'],
2398
+ ['WELLNESS_BREAK_NOT_ALLOWED', 'InteractionId'],
2399
+ ])('normalizes the %s action and either interaction-id spelling', (actionEvent, key) => {
2400
+ const emitSpy = jest.spyOn(webex.cc, 'emit');
2401
+ webex.cc['handleWebsocketMessage'](
2402
+ JSON.stringify({
2403
+ type: 'Wellness_Break_Handler',
2404
+ orgId: 'mockOrgId',
2405
+ data: {
2406
+ agentId: 'agent-1',
2407
+ orgId: 'mockOrgId',
2408
+ notifType: 'Wellness_Break_Handler',
2409
+ notifDetails: {actionEvent},
2410
+ data: {
2411
+ orgId: 'mockOrgId',
2412
+ agentSessionId: 'session-1',
2413
+ [key]: 'interaction-2',
2414
+ },
2415
+ },
2416
+ })
2417
+ );
2418
+
2419
+ expect(emitSpy).toHaveBeenCalledWith(
2420
+ CC_EVENTS.WELLNESS_BREAK,
2421
+ expect.objectContaining({actionEvent, interactionId: 'interaction-2'})
2422
+ );
2423
+ });
2424
+
2425
+ it('does not promote uncontracted notification text fields', () => {
2426
+ const emitSpy = jest.spyOn(webex.cc, 'emit');
2427
+ webex.cc['handleWebsocketMessage'](
2428
+ JSON.stringify({
2429
+ type: 'Wellness_Break_Handler',
2430
+ orgId: 'mockOrgId',
2431
+ trackingId: 'notification-tracking',
2432
+ data: {
2433
+ agentId: 'agent-1',
2434
+ orgId: 'mockOrgId',
2435
+ notifType: 'Wellness_Break_Handler',
2436
+ notifDetails: {
2437
+ actionEvent: 'SUGGEST_WELLNESS_BREAK',
2438
+ actionText2: 'Uncontracted text',
2439
+ },
2440
+ data: {
2441
+ orgId: 'mockOrgId',
2442
+ agentSessionId: 'session-1',
2443
+ InteractionId: 'interaction-qa',
2444
+ },
2445
+ },
2446
+ })
2447
+ );
2448
+
2449
+ expect(emitSpy).toHaveBeenCalledWith(CC_EVENTS.WELLNESS_BREAK, {
2450
+ agentId: 'agent-1',
2451
+ orgId: 'mockOrgId',
2452
+ agentSessionId: 'session-1',
2453
+ actionEvent: 'SUGGEST_WELLNESS_BREAK',
2454
+ interactionId: 'interaction-qa',
2455
+ trackingId: 'notification-tracking',
2456
+ });
2457
+ });
2458
+
2459
+ it('ignores wellness events when the effective flag is disabled or identity is missing', () => {
2460
+ const emitSpy = jest.spyOn(webex.cc, 'emit');
2461
+ const payload = {
2462
+ type: 'Wellness_Break_Handler',
2463
+ orgId: 'mockOrgId',
2464
+ data: {
2465
+ agentId: 'agent-1',
2466
+ orgId: 'mockOrgId',
2467
+ notifType: 'Wellness_Break_Handler',
2468
+ notifDetails: {actionEvent: 'SUGGEST_WELLNESS_BREAK'},
2469
+ data: {orgId: 'mockOrgId', agentSessionId: 'session-1'},
2470
+ },
2471
+ };
2472
+
2473
+ webex.cc.agentConfig.isWellnessBreakEnabled = false;
2474
+ webex.cc['handleWebsocketMessage'](JSON.stringify(payload));
2475
+ webex.cc.agentConfig.isWellnessBreakEnabled = true;
2476
+ const {orgId: omittedOrgId, ...payloadWithoutEnvelopeOrg} = payload;
2477
+ expect(omittedOrgId).toBe('mockOrgId');
2478
+ webex.cc['handleWebsocketMessage'](JSON.stringify(payloadWithoutEnvelopeOrg));
2479
+
2480
+ expect(emitSpy).not.toHaveBeenCalledWith(CC_EVENTS.WELLNESS_BREAK, expect.anything());
2481
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
2482
+ METRIC_EVENT_NAMES.AI_ASSISTANT_WELLNESS_EVENT_INVALID,
2483
+ {reason: 'wellness_disabled'},
2484
+ ['operational']
2485
+ );
2486
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
2487
+ METRIC_EVENT_NAMES.AI_ASSISTANT_WELLNESS_EVENT_INVALID,
2488
+ {reason: 'missing_wellness_identity'},
2489
+ ['operational']
2490
+ );
2491
+ });
2492
+
2493
+ });
2494
+
1074
2495
  describe('getBuddyAgents', () => {
1075
2496
  it('should return buddy agents response when successful', async () => {
1076
2497
  const data: BuddyAgents = {state: 'Available', mediaType: 'telephony'};
@@ -1134,6 +2555,41 @@ describe('webex.cc', () => {
1134
2555
  ]);
1135
2556
  });
1136
2557
 
2558
+ it('should map Transfer action to an Available backend state filter', async () => {
2559
+ webex.cc.agentConfig = {agentProfileID: 'test-agent-profile-id'};
2560
+ const buddyAgentsResponse = {data: {agentList: []}};
2561
+ const buddyAgentsSpy = jest
2562
+ .spyOn(webex.cc.services.agent, 'buddyAgents')
2563
+ .mockResolvedValue(buddyAgentsResponse);
2564
+
2565
+ await webex.cc.getBuddyAgents({action: 'Transfer', mediaType: 'chat'});
2566
+
2567
+ expect(buddyAgentsSpy).toHaveBeenCalledWith({
2568
+ data: {
2569
+ agentProfileId: 'test-agent-profile-id',
2570
+ mediaType: 'chat',
2571
+ state: 'Available',
2572
+ },
2573
+ });
2574
+ });
2575
+
2576
+ it('should omit the backend state filter for Consult and default media type to telephony', async () => {
2577
+ webex.cc.agentConfig = {agentProfileID: 'test-agent-profile-id'};
2578
+ const buddyAgentsResponse = {data: {agentList: []}};
2579
+ const buddyAgentsSpy = jest
2580
+ .spyOn(webex.cc.services.agent, 'buddyAgents')
2581
+ .mockResolvedValue(buddyAgentsResponse);
2582
+
2583
+ await webex.cc.getBuddyAgents({action: 'Consult'});
2584
+
2585
+ expect(buddyAgentsSpy).toHaveBeenCalledWith({
2586
+ data: {
2587
+ agentProfileId: 'test-agent-profile-id',
2588
+ mediaType: 'telephony',
2589
+ },
2590
+ });
2591
+ });
2592
+
1137
2593
  it('should handle error', async () => {
1138
2594
  const data: BuddyAgents = {state: 'Available', mediaType: 'telephony'};
1139
2595
  webex.cc.agentConfig = {
@@ -1210,11 +2666,11 @@ describe('webex.cc', () => {
1210
2666
  const webSocketManagerOnSpy = jest.spyOn(webex.cc.services.webSocketManager, 'on');
1211
2667
  await webex.cc['silentRelogin']();
1212
2668
 
1213
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2669
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1214
2670
  module: CC_FILE,
1215
2671
  method: 'silentRelogin',
1216
2672
  });
1217
- expect(LoggerProxy.info).toHaveBeenCalledWith(
2673
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
1218
2674
  'event=requestAutoStateChange | Requesting state change to available on socket reconnect',
1219
2675
  {module: CC_FILE, method: 'silentRelogin'}
1220
2676
  );
@@ -1259,7 +2715,7 @@ describe('webex.cc', () => {
1259
2715
 
1260
2716
  jest.spyOn(webex.cc.services.agent, 'reload').mockRejectedValue(error);
1261
2717
  await webex.cc['silentRelogin']();
1262
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2718
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1263
2719
  module: CC_FILE,
1264
2720
  method: 'silentRelogin',
1265
2721
  });
@@ -1274,7 +2730,7 @@ describe('webex.cc', () => {
1274
2730
  jest.spyOn(webex.cc.services.agent, 'reload').mockRejectedValue(error);
1275
2731
 
1276
2732
  await expect(webex.cc['silentRelogin']()).rejects.toThrow(error);
1277
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2733
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1278
2734
  module: CC_FILE,
1279
2735
  method: 'silentRelogin',
1280
2736
  });
@@ -1316,7 +2772,7 @@ describe('webex.cc', () => {
1316
2772
 
1317
2773
  await webex.cc['silentRelogin']();
1318
2774
 
1319
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2775
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1320
2776
  module: CC_FILE,
1321
2777
  method: 'silentRelogin',
1322
2778
  });
@@ -1361,6 +2817,83 @@ describe('webex.cc', () => {
1361
2817
  expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.AGENT_DN);
1362
2818
  expect(webex.cc.agentConfig.dn).toBe('67890');
1363
2819
  });
2820
+
2821
+ it('should publish usersub true on silent relogin when enableWxBetterTogether is true at init', async () => {
2822
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2823
+ webex.internal.device = {
2824
+ userId: 'user-123',
2825
+ url: 'https://wdm.example.com/devices/dev-1',
2826
+ registered: true,
2827
+ register: jest.fn().mockResolvedValue(undefined),
2828
+ };
2829
+ webex.internal.mercury.connected = true;
2830
+ webex.internal.mercury.connect = jest.fn().mockResolvedValue(undefined);
2831
+ webex.cc.agentConfig = {
2832
+ agentId: 'agentId',
2833
+ agentProfileID: 'test-agent-profile-id',
2834
+ isAgentLoggedIn: false,
2835
+ } as Profile;
2836
+
2837
+ jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
2838
+ data: {
2839
+ auxCodeId: 'auxCodeId',
2840
+ agentId: 'agentId',
2841
+ deviceType: LoginOption.EXTENSION,
2842
+ dn: '1001',
2843
+ teamId: 'teamId',
2844
+ lastStateChangeTimestamp: 1738575135188,
2845
+ lastIdleCodeChangeTimestamp: 1738575135189,
2846
+ },
2847
+ });
2848
+
2849
+ const publishSpy = jest
2850
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2851
+ .mockResolvedValue(undefined);
2852
+ const subscribeSpy = jest
2853
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
2854
+ .mockImplementation(() => {});
2855
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
2856
+
2857
+ await webex.cc['silentRelogin']();
2858
+
2859
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
2860
+ expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
2861
+ expect(mockTaskManager.refreshWxAppOfferObservabilityForAllTasks).toHaveBeenCalled();
2862
+ });
2863
+
2864
+ it('should force-publish usersub false on silent relogin when enableWxBetterTogether is false at init', async () => {
2865
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
2866
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
2867
+ webex.cc.agentConfig = {
2868
+ agentId: 'agentId',
2869
+ agentProfileID: 'test-agent-profile-id',
2870
+ isAgentLoggedIn: false,
2871
+ } as Profile;
2872
+
2873
+ jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
2874
+ data: {
2875
+ auxCodeId: 'auxCodeId',
2876
+ agentId: 'agentId',
2877
+ deviceType: LoginOption.EXTENSION,
2878
+ dn: '1001',
2879
+ teamId: 'teamId',
2880
+ lastStateChangeTimestamp: 1738575135188,
2881
+ lastIdleCodeChangeTimestamp: 1738575135189,
2882
+ },
2883
+ });
2884
+
2885
+ const publishSpy = jest
2886
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2887
+ .mockResolvedValue(undefined);
2888
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
2889
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
2890
+
2891
+ await webex.cc['silentRelogin']();
2892
+
2893
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
2894
+ expect(teardownSpy).toHaveBeenCalled();
2895
+ expect(unsubscribeSpy).toHaveBeenCalled();
2896
+ });
1364
2897
  });
1365
2898
 
1366
2899
  describe('setupEventListeners()', () => {
@@ -1500,6 +3033,27 @@ describe('webex.cc', () => {
1500
3033
  });
1501
3034
  });
1502
3035
 
3036
+ describe('getQueues', () => {
3037
+ it('delegates to the queue service when successful', async () => {
3038
+ const mockQueuesResponse = [{queueId: 'queue1', queueName: 'Queue 1'}];
3039
+ const queueSpy = jest
3040
+ .spyOn(webex.cc.queue, 'getQueues')
3041
+ .mockResolvedValue(mockQueuesResponse as any);
3042
+
3043
+ const result = await webex.cc.getQueues({page: 1});
3044
+
3045
+ expect(queueSpy).toHaveBeenCalledWith({page: 1});
3046
+ expect(result).toBe(mockQueuesResponse);
3047
+ });
3048
+
3049
+ it('propagates queue service errors', async () => {
3050
+ const error = new Error('Test error.');
3051
+ jest.spyOn(webex.cc.queue, 'getQueues').mockRejectedValue(error);
3052
+
3053
+ await expect(webex.cc.getQueues()).rejects.toThrow('Test error.');
3054
+ });
3055
+ });
3056
+
1503
3057
  describe('uploadLogs', () => {
1504
3058
  it('should upload logs successfully', async () => {
1505
3059
  const uploadLogsMock = jest.spyOn(webex.cc.webexRequest, 'uploadLogs').mockResolvedValue({
@@ -1529,6 +3083,7 @@ describe('webex.cc', () => {
1529
3083
 
1530
3084
  describe('unregister', () => {
1531
3085
  let mockWebSocketManager;
3086
+ let mockRTDWebSocketManager;
1532
3087
  let mercuryDisconnectSpy;
1533
3088
  let deviceUnregisterSpy;
1534
3089
 
@@ -1545,8 +3100,15 @@ describe('webex.cc', () => {
1545
3100
  off: jest.fn(),
1546
3101
  on: jest.fn(),
1547
3102
  };
3103
+ mockRTDWebSocketManager = {
3104
+ isSocketClosed: false,
3105
+ close: jest.fn(),
3106
+ off: jest.fn(),
3107
+ on: jest.fn(),
3108
+ };
1548
3109
 
1549
3110
  webex.cc.services.webSocketManager = mockWebSocketManager;
3111
+ webex.cc.services.rtdWebSocketManager = mockRTDWebSocketManager;
1550
3112
 
1551
3113
  webex.internal = webex.internal || {};
1552
3114
  webex.internal.mercury = {
@@ -1562,6 +3124,32 @@ describe('webex.cc', () => {
1562
3124
  deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
1563
3125
  });
1564
3126
 
3127
+ it('invalidates wellness context before deregistration cleanup can fail', async () => {
3128
+ const cleanupError = new Error('cleanup failed');
3129
+ let rejectCleanup: (error: Error) => void = () => undefined;
3130
+ const cleanup = new Promise((resolve, reject) => {
3131
+ rejectCleanup = reject;
3132
+ });
3133
+ webex.cc.agentConfig = {
3134
+ ...webex.cc.agentConfig,
3135
+ isWellnessBreakEnabled: true,
3136
+ };
3137
+ webex.cc['updateWellnessSession']('session-1');
3138
+ jest.spyOn(webex.cc as any, 'teardownWxAppLocalState').mockReturnValue(cleanup);
3139
+
3140
+ const deregistration = webex.cc.deregister();
3141
+ const rejection = expect(deregistration).rejects.toThrow(cleanupError);
3142
+
3143
+ expect(webex.cc['currentAgentSessionId']).toBeUndefined();
3144
+ await expect(webex.cc.apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
3145
+ 'WELLNESS_BREAK_AGENT_SESSION_REQUIRED'
3146
+ );
3147
+
3148
+ rejectCleanup(cleanupError);
3149
+ await rejection;
3150
+ expect(webex.cc['currentAgentSessionId']).toBeUndefined();
3151
+ });
3152
+
1565
3153
  it('should unregister successfully and clean up all resources when webrtc is enabled', async () => {
1566
3154
  webex.cc.services.rtdWebSocketManager = {
1567
3155
  isSocketClosed: false,
@@ -1570,6 +3158,18 @@ describe('webex.cc', () => {
1570
3158
  on: jest.fn(),
1571
3159
  } as any;
1572
3160
 
3161
+ webex.internal.device = {
3162
+ ...webex.internal.device,
3163
+ userId: 'user-123',
3164
+ unregister: jest.fn().mockResolvedValue(undefined),
3165
+ };
3166
+ deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
3167
+ const publishSpy = jest
3168
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3169
+ .mockResolvedValue(undefined);
3170
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
3171
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
3172
+
1573
3173
  await webex.cc.deregister();
1574
3174
 
1575
3175
  expect(mockTaskManager.off).toHaveBeenCalledWith(
@@ -1580,6 +3180,10 @@ describe('webex.cc', () => {
1580
3180
  TASK_EVENTS.TASK_HYDRATE,
1581
3181
  expect.any(Function)
1582
3182
  );
3183
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
3184
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
3185
+ expect.any(Function)
3186
+ );
1583
3187
  expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1584
3188
  expect(webex.cc.services.rtdWebSocketManager.off).toHaveBeenCalledWith(
1585
3189
  'message',
@@ -1636,17 +3240,57 @@ describe('webex.cc', () => {
1636
3240
  const [, hydrateCallback] = hydrateCalls[0];
1637
3241
  expect(hydrateCallback).toBe(webex.cc['handleTaskHydrate']);
1638
3242
 
3243
+ const multiLoginHydrateCalls = mockTaskManager.off.mock.calls.filter(
3244
+ ([evt]) => evt === TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE
3245
+ );
3246
+ expect(multiLoginHydrateCalls).toHaveLength(1);
3247
+ const [, multiLoginHydrateCallback] = multiLoginHydrateCalls[0];
3248
+ expect(multiLoginHydrateCallback).toBe(webex.cc['handleTaskMultiLoginHydrate']);
3249
+
1639
3250
  const messageCalls = mockWebSocketManager.off.mock.calls.filter(([evt]) => evt === 'message');
1640
3251
  expect(messageCalls).toHaveLength(1);
1641
3252
  const [, messageCallback] = messageCalls[0];
1642
3253
  expect(messageCallback).toBe(webex.cc['handleWebsocketMessage']);
1643
3254
 
1644
- const connectionCalls = webex.cc.services.connectionService.off.mock.calls.filter(
1645
- ([evt]) => evt === 'connectionLost'
1646
- );
1647
- expect(connectionCalls).toHaveLength(1);
1648
- const [, connectionCallback] = connectionCalls[0];
1649
- expect(connectionCallback).toBe(webex.cc['handleConnectionLost']);
3255
+ const connectionCalls = webex.cc.services.connectionService.off.mock.calls.filter(
3256
+ ([evt]) => evt === 'connectionLost'
3257
+ );
3258
+ expect(connectionCalls).toHaveLength(1);
3259
+ const [, connectionCallback] = connectionCalls[0];
3260
+ expect(connectionCallback).toBe(webex.cc['handleConnectionLost']);
3261
+
3262
+ expect(publishSpy).not.toHaveBeenCalled();
3263
+ expect(teardownSpy).toHaveBeenCalled();
3264
+ expect(unsubscribeSpy).toHaveBeenCalled();
3265
+ });
3266
+
3267
+ it('should publish usersub false before device unregister when wxApp is active on deregister', async () => {
3268
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3269
+ webex.cc['webexCrossClientService'].answerCallsState = true;
3270
+ webex.internal.device = {
3271
+ userId: 'user-123',
3272
+ url: 'https://wdm.example.com/devices/dev-1',
3273
+ unregister: jest.fn().mockResolvedValue(undefined),
3274
+ };
3275
+ deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
3276
+
3277
+ const callOrder: string[] = [];
3278
+ jest
3279
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3280
+ .mockImplementation(async (enable: boolean) => {
3281
+ if (enable === false && !webex.internal.device.url) {
3282
+ throw new Error('Device URL is unavailable for cross-client publish');
3283
+ }
3284
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
3285
+ });
3286
+ deviceUnregisterSpy.mockImplementation(async () => {
3287
+ callOrder.push('device.unregister');
3288
+ webex.internal.device.url = undefined;
3289
+ });
3290
+
3291
+ await webex.cc.deregister();
3292
+
3293
+ expect(callOrder).toEqual(['publish:false', 'device.unregister']);
1650
3294
  });
1651
3295
 
1652
3296
  it('should skip webCallingService and internal cleanup when webrtc is disabled', async () => {
@@ -1661,6 +3305,10 @@ describe('webex.cc', () => {
1661
3305
  TASK_EVENTS.TASK_HYDRATE,
1662
3306
  expect.any(Function)
1663
3307
  );
3308
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
3309
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
3310
+ expect.any(Function)
3311
+ );
1664
3312
  expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1665
3313
  expect(webex.cc.services.connectionService.off).toHaveBeenCalledWith(
1666
3314
  'connectionLost',
@@ -1670,6 +3318,7 @@ describe('webex.cc', () => {
1670
3318
  expect(webex.internal.mercury.off).not.toHaveBeenCalled();
1671
3319
  expect(mercuryDisconnectSpy).not.toHaveBeenCalled();
1672
3320
  expect(deviceUnregisterSpy).not.toHaveBeenCalled();
3321
+ expect(mockRTDWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
1673
3322
  });
1674
3323
 
1675
3324
  it('should skip internal mercury cleanup when loginVoiceOptions does not include BROWSER', async () => {
@@ -1686,10 +3335,140 @@ describe('webex.cc', () => {
1686
3335
  expect(mercuryDisconnectSpy).not.toHaveBeenCalled();
1687
3336
  expect(deviceUnregisterSpy).not.toHaveBeenCalled();
1688
3337
 
3338
+ expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
3339
+ expect(mockRTDWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
3340
+ expect(webex.cc.agentConfig).toBeNull();
3341
+ });
3342
+
3343
+ it('should teardown wxApp state on deregister even when usersub publish fails', async () => {
3344
+ webex.cc.agentConfig = {
3345
+ agentId: 'agentId',
3346
+ webRtcEnabled: false,
3347
+ loginVoiceOptions: [LoginOption.EXTENSION],
3348
+ };
3349
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3350
+ webex.cc['webexCrossClientService'].answerCallsState = true;
3351
+ webex.internal.device = {
3352
+ userId: 'user-123',
3353
+ unregister: jest.fn().mockResolvedValue(undefined),
3354
+ };
3355
+
3356
+ jest
3357
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
3358
+ .mockRejectedValue(new Error('usersub publish failed'));
3359
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
3360
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
3361
+
3362
+ await expect(webex.cc.deregister()).rejects.toThrow('usersub publish failed');
3363
+
3364
+ expect(teardownSpy).toHaveBeenCalled();
3365
+ expect(unsubscribeSpy).toHaveBeenCalled();
3366
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1689
3367
  expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
1690
3368
  expect(webex.cc.agentConfig).toBeNull();
1691
3369
  });
1692
3370
 
3371
+ it('should publish forced false during teardown when enable publish is still in flight', async () => {
3372
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3373
+ webex.internal.device = {
3374
+ userId: 'user-123',
3375
+ url: 'https://wdm.example.com/devices/dev-1',
3376
+ };
3377
+
3378
+ let resolveEnable: () => void = () => {};
3379
+ const enableGate = new Promise<void>((resolve) => {
3380
+ resolveEnable = resolve;
3381
+ });
3382
+
3383
+ const publishSpy = jest
3384
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3385
+ .mockImplementation(async (enable: boolean) => {
3386
+ if (enable) {
3387
+ await enableGate;
3388
+ }
3389
+ });
3390
+ jest
3391
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
3392
+ .mockResolvedValue(undefined);
3393
+
3394
+ expect(webex.cc['webexCrossClientService'].answerCallsState).toBe(false);
3395
+
3396
+ const enableInFlight = (webex.cc as any).publishAnswerOnWebexCrossClientState(true);
3397
+
3398
+ await (webex.cc as any).teardownWxAppLocalState();
3399
+
3400
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
3401
+
3402
+ resolveEnable();
3403
+ await enableInFlight;
3404
+ });
3405
+
3406
+ it('should release wxApp-owned Mercury and device on deregister', async () => {
3407
+ webex.cc.agentConfig = {
3408
+ agentId: 'agentId',
3409
+ webRtcEnabled: false,
3410
+ loginVoiceOptions: [LoginOption.EXTENSION],
3411
+ };
3412
+ webex.cc['wxAppMercuryConnectedByCc'] = true;
3413
+ webex.cc['wxAppDeviceRegisteredByCc'] = true;
3414
+ webex.internal.mercury.connected = true;
3415
+ webex.internal.device.registered = true;
3416
+
3417
+ const mercuryDisconnectSpy = jest
3418
+ .spyOn(webex.internal.mercury, 'disconnect')
3419
+ .mockResolvedValue(undefined);
3420
+ const deviceUnregisterSpy = jest
3421
+ .spyOn(webex.internal.device, 'unregister')
3422
+ .mockResolvedValue(undefined);
3423
+
3424
+ await webex.cc.deregister();
3425
+
3426
+ expect(mercuryDisconnectSpy).toHaveBeenCalled();
3427
+ expect(deviceUnregisterSpy).toHaveBeenCalled();
3428
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(false);
3429
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(false);
3430
+ });
3431
+
3432
+ it('should retain wxApp Mercury ownership when disconnect fails and retry cleanup', async () => {
3433
+ webex.cc['wxAppMercuryConnectedByCc'] = true;
3434
+ webex.internal.mercury.connected = true;
3435
+
3436
+ const mercuryDisconnectSpy = jest
3437
+ .spyOn(webex.internal.mercury, 'disconnect')
3438
+ .mockRejectedValueOnce(new Error('disconnect failed'))
3439
+ .mockResolvedValueOnce(undefined);
3440
+
3441
+ await webex.cc['releaseWxAppMercuryResources']();
3442
+
3443
+ expect(mercuryDisconnectSpy).toHaveBeenCalledTimes(1);
3444
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(true);
3445
+
3446
+ await webex.cc['releaseWxAppMercuryResources']();
3447
+
3448
+ expect(mercuryDisconnectSpy).toHaveBeenCalledTimes(2);
3449
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(false);
3450
+ });
3451
+
3452
+ it('should retain wxApp device ownership when unregister fails and retry cleanup', async () => {
3453
+ webex.cc['wxAppDeviceRegisteredByCc'] = true;
3454
+ webex.internal.device.registered = true;
3455
+
3456
+ const deviceUnregisterSpy = jest
3457
+ .spyOn(webex.internal.device, 'unregister')
3458
+ .mockRejectedValueOnce(new Error('unregister failed'))
3459
+ .mockResolvedValueOnce(undefined);
3460
+
3461
+ await webex.cc['releaseWxAppMercuryResources']();
3462
+
3463
+ expect(deviceUnregisterSpy).toHaveBeenCalledTimes(1);
3464
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(true);
3465
+
3466
+ await webex.cc['releaseWxAppMercuryResources']();
3467
+
3468
+ expect(deviceUnregisterSpy).toHaveBeenCalledTimes(2);
3469
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(false);
3470
+ });
3471
+
1693
3472
  it('should handle errors during unregister and track metrics', async () => {
1694
3473
  const mockError = new Error('Failed to deregister device');
1695
3474
  webex.internal.device.unregister.mockRejectedValue(mockError);
@@ -1704,6 +3483,10 @@ describe('webex.cc', () => {
1704
3483
  TASK_EVENTS.TASK_HYDRATE,
1705
3484
  expect.any(Function)
1706
3485
  );
3486
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
3487
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
3488
+ expect.any(Function)
3489
+ );
1707
3490
 
1708
3491
  expect(LoggerProxy.error).toHaveBeenCalledWith(`Error during deregister: ${mockError}`, {
1709
3492
  module: CC_FILE,
@@ -1726,7 +3509,7 @@ describe('webex.cc', () => {
1726
3509
 
1727
3510
  beforeEach(() => {
1728
3511
  emitSpy = jest.spyOn(webex.cc, 'emit');
1729
- messageCallback = mockWebSocketManager.on.mock.calls.find((c) => c[0] === 'message')[1];
3512
+ messageCallback = webex.cc['handleWebsocketMessage'];
1730
3513
  });
1731
3514
 
1732
3515
  it('should emit AGENT_STATION_LOGIN_SUCCESS on CC_EVENTS.AGENT_STATION_LOGIN_SUCCESS with mapped payload', () => {
@@ -1936,6 +3719,55 @@ describe('webex.cc', () => {
1936
3719
  expect(result).toEqual(mockResp);
1937
3720
  });
1938
3721
 
3722
+ it('should preserve enableWxBetterTogether init flag across profile update logout/login', async () => {
3723
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3724
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
3725
+ webex.cc.agentConfig = {
3726
+ ...webex.cc.agentConfig,
3727
+ agentId: 'agentId',
3728
+ webRtcEnabled: false,
3729
+ loginVoiceOptions: ['BROWSER', 'EXTENSION'],
3730
+ } as Profile;
3731
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
3732
+ webex.cc.webCallingService.setLoginOption = jest.fn((loginOption: LoginOption) => {
3733
+ webex.cc.webCallingService.loginOption = loginOption;
3734
+ });
3735
+ webex.cc['webexCrossClientService'].answerCallsState = true;
3736
+
3737
+ const publishSpy = jest
3738
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3739
+ .mockResolvedValue(undefined);
3740
+ jest
3741
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
3742
+ .mockImplementation(() => {});
3743
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
3744
+ jest.spyOn(webex.cc as any, 'releaseWxAppMercuryResources').mockResolvedValue(undefined);
3745
+
3746
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
3747
+ trackingId: 'track-logout',
3748
+ } as StationLogoutResponse);
3749
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
3750
+ data: {
3751
+ agentId: 'agentId',
3752
+ teamId: 'teamId',
3753
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
3754
+ },
3755
+ trackingId: 'track-login',
3756
+ } as StationLoginSuccess);
3757
+
3758
+ webex.internal.mercury.connected = true;
3759
+
3760
+ await webex.cc.updateAgentProfile({
3761
+ teamId: 'teamId',
3762
+ loginOption: LoginOption.EXTENSION,
3763
+ dialNumber: '1001',
3764
+ });
3765
+
3766
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
3767
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
3768
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
3769
+ });
3770
+
1939
3771
  it('should use provided teamId if passed in payload', async () => {
1940
3772
  const dataWithTeam = {
1941
3773
  teamId: 'newTeam',
@@ -2320,4 +4152,445 @@ describe('webex.cc', () => {
2320
4152
  expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'acceptPreviewContact', CC_FILE);
2321
4153
  });
2322
4154
  });
4155
+
4156
+ describe('skipPreviewContact', () => {
4157
+ const previewPayload = {
4158
+ interactionId: 'interaction-123',
4159
+ campaignId: 'campaign-456',
4160
+ };
4161
+
4162
+ it('should skip preview contact successfully', async () => {
4163
+ const mockResponse = {trackingId: 'track-123'} as AgentContact;
4164
+
4165
+ const skipPreviewContactMock = jest
4166
+ .spyOn(webex.cc.services.dialer, 'skipPreviewContact')
4167
+ .mockResolvedValue(mockResponse);
4168
+
4169
+ const result = await webex.cc.skipPreviewContact(previewPayload);
4170
+
4171
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Skipping campaign preview contact', {
4172
+ module: CC_FILE,
4173
+ method: 'skipPreviewContact',
4174
+ });
4175
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
4176
+ 'Campaign preview contact skipped successfully',
4177
+ {
4178
+ module: CC_FILE,
4179
+ method: 'skipPreviewContact',
4180
+ trackingId: 'track-123',
4181
+ interactionId: previewPayload.interactionId,
4182
+ }
4183
+ );
4184
+
4185
+ expect(skipPreviewContactMock).toHaveBeenCalledWith({data: previewPayload});
4186
+ expect(result).toEqual(mockResponse);
4187
+ });
4188
+
4189
+ it('should handle error during skipPreviewContact', async () => {
4190
+ getErrorDetailsSpy.mockRestore();
4191
+ getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
4192
+
4193
+ const error = {
4194
+ details: {
4195
+ trackingId: '1234',
4196
+ data: {
4197
+ reason: 'Error while performing skipPreviewContact',
4198
+ },
4199
+ },
4200
+ };
4201
+
4202
+ jest.spyOn(webex.cc.services.dialer, 'skipPreviewContact').mockRejectedValue(error);
4203
+
4204
+ await expect(webex.cc.skipPreviewContact(previewPayload)).rejects.toThrow(
4205
+ error.details.data.reason
4206
+ );
4207
+
4208
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Skipping campaign preview contact', {
4209
+ module: CC_FILE,
4210
+ method: 'skipPreviewContact',
4211
+ });
4212
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
4213
+ `skipPreviewContact failed with reason: ${error.details.data.reason}`,
4214
+ {module: CC_FILE, method: 'skipPreviewContact', trackingId: error.details.trackingId}
4215
+ );
4216
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'skipPreviewContact', CC_FILE);
4217
+ });
4218
+ });
4219
+
4220
+ describe('removePreviewContact', () => {
4221
+ const previewPayload = {
4222
+ interactionId: 'interaction-123',
4223
+ campaignId: 'campaign-456',
4224
+ };
4225
+
4226
+ it('should remove preview contact successfully', async () => {
4227
+ const mockResponse = {trackingId: 'track-123'} as AgentContact;
4228
+
4229
+ const removePreviewContactMock = jest
4230
+ .spyOn(webex.cc.services.dialer, 'removePreviewContact')
4231
+ .mockResolvedValue(mockResponse);
4232
+
4233
+ const result = await webex.cc.removePreviewContact(previewPayload);
4234
+
4235
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Removing campaign preview contact', {
4236
+ module: CC_FILE,
4237
+ method: 'removePreviewContact',
4238
+ });
4239
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
4240
+ 'Campaign preview contact removed successfully',
4241
+ {
4242
+ module: CC_FILE,
4243
+ method: 'removePreviewContact',
4244
+ trackingId: 'track-123',
4245
+ interactionId: previewPayload.interactionId,
4246
+ }
4247
+ );
4248
+
4249
+ expect(removePreviewContactMock).toHaveBeenCalledWith({data: previewPayload});
4250
+ expect(result).toEqual(mockResponse);
4251
+ });
4252
+
4253
+ it('should handle error during removePreviewContact', async () => {
4254
+ getErrorDetailsSpy.mockRestore();
4255
+ getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
4256
+
4257
+ const error = {
4258
+ details: {
4259
+ trackingId: '1234',
4260
+ data: {
4261
+ reason: 'Error while performing removePreviewContact',
4262
+ },
4263
+ },
4264
+ };
4265
+
4266
+ jest.spyOn(webex.cc.services.dialer, 'removePreviewContact').mockRejectedValue(error);
4267
+
4268
+ await expect(webex.cc.removePreviewContact(previewPayload)).rejects.toThrow(
4269
+ error.details.data.reason
4270
+ );
4271
+
4272
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Removing campaign preview contact', {
4273
+ module: CC_FILE,
4274
+ method: 'removePreviewContact',
4275
+ });
4276
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
4277
+ `removePreviewContact failed with reason: ${error.details.data.reason}`,
4278
+ {module: CC_FILE, method: 'removePreviewContact', trackingId: error.details.trackingId}
4279
+ );
4280
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'removePreviewContact', CC_FILE);
4281
+ });
4282
+ });
4283
+
4284
+ describe('private setManageWebexCallingInWxcc (runtime toggle impl)', () => {
4285
+ beforeEach(() => {
4286
+ webex.internal.device = {
4287
+ userId: 'user-123',
4288
+ registered: false,
4289
+ register: jest.fn().mockResolvedValue(undefined),
4290
+ unregister: jest.fn().mockResolvedValue(undefined),
4291
+ };
4292
+ webex.internal.mercury = {
4293
+ connected: false,
4294
+ connect: jest.fn().mockResolvedValue(undefined),
4295
+ disconnect: jest.fn().mockResolvedValue(undefined),
4296
+ };
4297
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
4298
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
4299
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
4300
+ webex.cc['agentConfig'] = {
4301
+ agentId: 'agent-123',
4302
+ deviceType: LoginOption.EXTENSION,
4303
+ } as Profile;
4304
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
4305
+ webex.cc.webCallingService.setLoginOption = jest.fn((loginOption: LoginOption) => {
4306
+ webex.cc.webCallingService.loginOption = loginOption;
4307
+ });
4308
+ });
4309
+
4310
+ afterEach(() => {
4311
+ jest.restoreAllMocks();
4312
+ });
4313
+
4314
+ it('should update config, task manager, and publish usersub when enabled', async () => {
4315
+ const publishSpy = jest
4316
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4317
+ .mockResolvedValue(undefined);
4318
+
4319
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
4320
+
4321
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
4322
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(true);
4323
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
4324
+ expect(mockTaskManager.refreshWxAppOfferObservabilityForAllTasks).toHaveBeenCalled();
4325
+ });
4326
+
4327
+ it('should publish false and update config when disabled after wxApp was enabled', async () => {
4328
+ webex.internal.device = {
4329
+ userId: 'user-123',
4330
+ url: 'https://wdm.example.com/devices/dev-1',
4331
+ };
4332
+ webex.request = jest.fn().mockResolvedValue({body: {}});
4333
+
4334
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
4335
+
4336
+ const publishSpy = jest
4337
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4338
+ .mockResolvedValue(undefined);
4339
+
4340
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
4341
+
4342
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4343
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
4344
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
4345
+ });
4346
+
4347
+ it('should skip usersub publish when disabling without prior wxApp publish', async () => {
4348
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
4349
+ const publishSpy = jest
4350
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4351
+ .mockResolvedValue(undefined);
4352
+
4353
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
4354
+
4355
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4356
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
4357
+ expect(publishSpy).not.toHaveBeenCalled();
4358
+ });
4359
+
4360
+ it('should throw when enabling before login', async () => {
4361
+ webex.internal.device = undefined;
4362
+
4363
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
4364
+ 'Cannot publish answer-calls-on-wxcc: user is not logged in'
4365
+ );
4366
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4367
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
4368
+ });
4369
+
4370
+ it('should throw when enabling on BROWSER (Desktop) login', async () => {
4371
+ webex.cc['agentConfig'] = {deviceType: LoginOption.BROWSER} as Profile;
4372
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
4373
+ const publishSpy = jest
4374
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4375
+ .mockResolvedValue(undefined);
4376
+
4377
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
4378
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
4379
+ );
4380
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4381
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
4382
+ expect(publishSpy).not.toHaveBeenCalled();
4383
+ });
4384
+
4385
+ it('should throw when enabling before station login', async () => {
4386
+ webex.cc['agentConfig'] = undefined as unknown as Profile;
4387
+ webex.cc.webCallingService.loginOption = undefined as unknown as LoginOption;
4388
+ const publishSpy = jest
4389
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4390
+ .mockResolvedValue(undefined);
4391
+
4392
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
4393
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
4394
+ );
4395
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4396
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
4397
+ expect(publishSpy).not.toHaveBeenCalled();
4398
+ });
4399
+
4400
+ it('should throw when webCallingService is BROWSER even if agentConfig deviceType is stale', async () => {
4401
+ webex.cc['agentConfig'] = {deviceType: LoginOption.EXTENSION} as Profile;
4402
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
4403
+ const publishSpy = jest
4404
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4405
+ .mockResolvedValue(undefined);
4406
+
4407
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
4408
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
4409
+ );
4410
+ expect(publishSpy).not.toHaveBeenCalled();
4411
+ });
4412
+
4413
+ it('should publish usersub when enabling on AGENT_DN login', async () => {
4414
+ webex.cc['agentConfig'] = {
4415
+ agentId: 'agent-123',
4416
+ deviceType: LoginOption.AGENT_DN,
4417
+ } as Profile;
4418
+ const publishSpy = jest
4419
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4420
+ .mockResolvedValue(undefined);
4421
+
4422
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
4423
+
4424
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
4425
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
4426
+ });
4427
+
4428
+ it('should no-op usersub publish when disabling before login', async () => {
4429
+ webex.internal.device = undefined;
4430
+ const publishSpy = jest
4431
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4432
+ .mockResolvedValue(undefined);
4433
+
4434
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
4435
+
4436
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4437
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
4438
+ expect(publishSpy).not.toHaveBeenCalled();
4439
+ });
4440
+
4441
+ it('should subscribe to wxApp Mercury mute sync when enabled', async () => {
4442
+ webex.cc['agentConfig'] = {agentId: 'agent-123'} as Profile;
4443
+ webex.internal.device = {
4444
+ ...webex.internal.device,
4445
+ registered: true,
4446
+ register: jest.fn().mockResolvedValue(undefined),
4447
+ };
4448
+ webex.internal.mercury.connected = true;
4449
+ webex.internal.mercury.connect = jest.fn().mockResolvedValue(undefined);
4450
+ jest
4451
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4452
+ .mockResolvedValue(undefined);
4453
+ const subscribeSpy = jest
4454
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
4455
+ .mockImplementation(() => {});
4456
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
4457
+
4458
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
4459
+
4460
+ expect(subscribeSpy).toHaveBeenCalledWith('agent-123', expect.any(Function));
4461
+ });
4462
+
4463
+ it('should unsubscribe from wxApp Mercury mute sync when disabled', async () => {
4464
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
4465
+ jest
4466
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4467
+ .mockResolvedValue(undefined);
4468
+ const unsubscribeSpy = jest
4469
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe')
4470
+ .mockImplementation(() => {});
4471
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
4472
+
4473
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
4474
+
4475
+ expect(unsubscribeSpy).toHaveBeenCalled();
4476
+ });
4477
+
4478
+ it('should register device and connect mercury when enabling wxApp sync', async () => {
4479
+ webex.cc['agentConfig'] = {agentId: 'agent-123'} as Profile;
4480
+ webex.internal.device = {
4481
+ userId: 'user-123',
4482
+ registered: false,
4483
+ register: jest.fn().mockResolvedValue(undefined),
4484
+ };
4485
+ webex.internal.mercury = {
4486
+ connected: false,
4487
+ connect: jest.fn().mockResolvedValue(undefined),
4488
+ };
4489
+ jest
4490
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4491
+ .mockResolvedValue(undefined);
4492
+ const subscribeSpy = jest
4493
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
4494
+ .mockImplementation(() => {});
4495
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
4496
+
4497
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
4498
+
4499
+ expect(webex.internal.device.register).toHaveBeenCalled();
4500
+ expect(webex.internal.mercury.connect).toHaveBeenCalled();
4501
+ expect(subscribeSpy).toHaveBeenCalledWith('agent-123', expect.any(Function));
4502
+ });
4503
+
4504
+ it('should rollback config and task manager when usersub publish fails', async () => {
4505
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
4506
+ jest
4507
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4508
+ .mockRejectedValue(new Error('usersub failed'));
4509
+
4510
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow('usersub failed');
4511
+
4512
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4513
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(true);
4514
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenLastCalledWith(false);
4515
+ });
4516
+
4517
+ it('should rollback config and release Mercury resources when Mercury connect fails', async () => {
4518
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
4519
+ const publishSpy = jest
4520
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4521
+ .mockResolvedValue(undefined);
4522
+ jest
4523
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
4524
+ .mockRejectedValue(new Error('mercury connect failed'));
4525
+ const releaseSpy = jest
4526
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
4527
+ .mockResolvedValue(undefined);
4528
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
4529
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
4530
+
4531
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
4532
+ 'mercury connect failed'
4533
+ );
4534
+
4535
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4536
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenLastCalledWith(false);
4537
+ expect(publishSpy).not.toHaveBeenCalled();
4538
+ expect(teardownSpy).not.toHaveBeenCalled();
4539
+ expect(unsubscribeSpy).toHaveBeenCalled();
4540
+ expect(releaseSpy).toHaveBeenCalled();
4541
+ });
4542
+
4543
+ it('should schedule compensating false retry when rollback publish fails after enable error', async () => {
4544
+ jest.useFakeTimers();
4545
+ try {
4546
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
4547
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
4548
+ webex.cc.agentConfig = {
4549
+ agentId: 'agent-123',
4550
+ deviceType: LoginOption.EXTENSION,
4551
+ };
4552
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
4553
+
4554
+ const publishSpy = jest
4555
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4556
+ .mockResolvedValueOnce(undefined)
4557
+ .mockRejectedValueOnce(new Error('compensating false failed'))
4558
+ .mockResolvedValue(undefined);
4559
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
4560
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
4561
+ jest
4562
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
4563
+ .mockResolvedValue(undefined);
4564
+ jest
4565
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
4566
+ .mockResolvedValue(undefined);
4567
+ jest
4568
+ .spyOn(mockTaskManager, 'syncWxAppMuteFromCallDetailsForAllTasks')
4569
+ .mockImplementation(() => {
4570
+ throw new Error('sync failed');
4571
+ });
4572
+ const scheduleRetrySpy = jest.spyOn(
4573
+ webex.cc as any,
4574
+ 'scheduleCompensatingFalsePublishRetry'
4575
+ );
4576
+
4577
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
4578
+ 'sync failed'
4579
+ );
4580
+
4581
+ expect(scheduleRetrySpy).toHaveBeenCalled();
4582
+ expect(publishSpy).toHaveBeenCalledTimes(2);
4583
+ expect(publishSpy).toHaveBeenNthCalledWith(1, true, {userId: 'user-123', trackPublishMetrics: true});
4584
+ expect(publishSpy).toHaveBeenNthCalledWith(2, false, {userId: 'user-123', trackPublishMetrics: true});
4585
+
4586
+ jest.advanceTimersByTime(30_000);
4587
+ await publishSpy.mock.results[2]?.value;
4588
+
4589
+ expect(publishSpy).toHaveBeenCalledTimes(3);
4590
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
4591
+ } finally {
4592
+ jest.useRealTimers();
4593
+ }
4594
+ });
4595
+ });
2323
4596
  });