@webex/contact-center 3.12.0-next.13 → 3.12.0-next.130

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 (312) hide show
  1. package/.sdd/manifest.json +884 -0
  2. package/AGENTS.md +94 -0
  3. package/ai-docs/ARCHITECTURE.md +168 -0
  4. package/ai-docs/CONTRACTS.md +53 -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 +971 -42
  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 +205 -0
  52. package/dist/metrics/behavioral-events.js.map +1 -1
  53. package/dist/metrics/constants.js +48 -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 +222 -0
  68. package/dist/services/WebexCrossClientService.js.map +1 -0
  69. package/dist/services/WxAppTelephonyMercurySync.js +108 -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 +49 -0
  95. package/dist/services/task/TaskFactory.js.map +1 -0
  96. package/dist/services/task/TaskManager.js +1172 -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 +1485 -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/wxAppVoiceMethods.js +354 -0
  133. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  134. package/dist/services/wxAppDiagnosticLogging.js +122 -0
  135. package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
  136. package/dist/services/wxAppTelephonyUtils.js +19 -0
  137. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  138. package/dist/types/cc.d.ts +193 -6
  139. package/dist/types/config.d.ts +13 -0
  140. package/dist/types/constants.d.ts +29 -1
  141. package/dist/types/index.d.ts +35 -8
  142. package/dist/types/metrics/MetricsManager.d.ts +6 -0
  143. package/dist/types/metrics/constants.d.ts +43 -1
  144. package/dist/types/services/AddressBook.d.ts +2 -1
  145. package/dist/types/services/AnswerCallOnWebexService.d.ts +38 -0
  146. package/dist/types/services/ApiAiAssistant.d.ts +56 -5
  147. package/dist/types/services/EntryPoint.d.ts +7 -5
  148. package/dist/types/services/Queue.d.ts +5 -3
  149. package/dist/types/services/UserPreference.d.ts +118 -0
  150. package/dist/types/services/WebexCrossClientService.d.ts +32 -0
  151. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  152. package/dist/types/services/config/constants.d.ts +34 -5
  153. package/dist/types/services/config/types.d.ts +197 -12
  154. package/dist/types/services/core/Err.d.ts +6 -0
  155. package/dist/types/services/core/Utils.d.ts +35 -13
  156. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  157. package/dist/types/services/core/types.d.ts +2 -0
  158. package/dist/types/services/core/websocket/WebSocketManager.d.ts +1 -0
  159. package/dist/types/services/core/websocket/types.d.ts +1 -1
  160. package/dist/types/services/index.d.ts +1 -1
  161. package/dist/types/services/task/Task.d.ts +175 -0
  162. package/dist/types/services/task/TaskFactory.d.ts +13 -0
  163. package/dist/types/services/task/TaskUtils.d.ts +48 -2
  164. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  165. package/dist/types/services/task/constants.d.ts +10 -0
  166. package/dist/types/services/task/contact.d.ts +4 -0
  167. package/dist/types/services/task/dialer.d.ts +30 -0
  168. package/dist/types/services/task/digital/Digital.d.ts +22 -0
  169. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +1442 -0
  170. package/dist/types/services/task/state-machine/actions.d.ts +10 -0
  171. package/dist/types/services/task/state-machine/constants.d.ts +107 -0
  172. package/dist/types/services/task/state-machine/guards.d.ts +103 -0
  173. package/dist/types/services/task/state-machine/index.d.ts +13 -0
  174. package/dist/types/services/task/state-machine/types.d.ts +277 -0
  175. package/dist/types/services/task/state-machine/uiControlsComputer.d.ts +9 -0
  176. package/dist/types/services/task/taskDataNormalizer.d.ts +10 -0
  177. package/dist/types/services/task/types.d.ts +680 -66
  178. package/dist/types/services/task/voice/Voice.d.ts +227 -0
  179. package/dist/types/services/task/voice/WebRTC.d.ts +53 -0
  180. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +55 -0
  181. package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
  182. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  183. package/dist/types/types.d.ts +205 -9
  184. package/dist/types/utils/PageCache.d.ts +20 -3
  185. package/dist/types/webex.d.ts +1 -0
  186. package/dist/types.js +162 -4
  187. package/dist/types.js.map +1 -1
  188. package/dist/utils/PageCache.js +19 -5
  189. package/dist/utils/PageCache.js.map +1 -1
  190. package/dist/webex.js +22 -2
  191. package/dist/webex.js.map +1 -1
  192. package/package.json +16 -12
  193. package/src/cc.ts +1289 -112
  194. package/src/config.ts +13 -0
  195. package/src/constants.ts +29 -1
  196. package/src/index.ts +36 -5
  197. package/src/metrics/MetricsManager.ts +16 -0
  198. package/src/metrics/ai-docs/AGENTS.md +350 -0
  199. package/src/metrics/ai-docs/ARCHITECTURE.md +338 -0
  200. package/src/metrics/ai-docs/metrics-spec.md +921 -0
  201. package/src/metrics/behavioral-events.ts +212 -0
  202. package/src/metrics/constants.ts +52 -4
  203. package/src/services/AddressBook.ts +17 -6
  204. package/src/services/AnswerCallOnWebexService.ts +239 -0
  205. package/src/services/ApiAiAssistant.ts +356 -9
  206. package/src/services/EntryPoint.ts +59 -60
  207. package/src/services/Queue.ts +29 -12
  208. package/src/services/UserPreference.ts +509 -0
  209. package/src/services/WebexCrossClientService.ts +293 -0
  210. package/src/services/WxAppTelephonyMercurySync.ts +142 -0
  211. package/src/services/agent/ai-docs/AGENTS.md +240 -0
  212. package/src/services/agent/ai-docs/ARCHITECTURE.md +304 -0
  213. package/src/services/agent/ai-docs/agent-spec.md +505 -0
  214. package/src/services/ai-docs/AGENTS.md +386 -0
  215. package/src/services/ai-docs/services-spec.md +505 -0
  216. package/src/services/config/Util.ts +12 -3
  217. package/src/services/config/ai-docs/AGENTS.md +255 -0
  218. package/src/services/config/ai-docs/ARCHITECTURE.md +426 -0
  219. package/src/services/config/ai-docs/config-spec.md +686 -0
  220. package/src/services/config/constants.ts +46 -6
  221. package/src/services/config/index.ts +65 -0
  222. package/src/services/config/types.ts +197 -13
  223. package/src/services/core/Err.ts +3 -0
  224. package/src/services/core/Utils.ts +127 -37
  225. package/src/services/core/WebexRequest.ts +3 -1
  226. package/src/services/core/ai-docs/AGENTS.md +381 -0
  227. package/src/services/core/ai-docs/ARCHITECTURE.md +698 -0
  228. package/src/services/core/ai-docs/core-spec.md +787 -0
  229. package/src/services/core/aqm-reqs.ts +30 -15
  230. package/src/services/core/types.ts +2 -0
  231. package/src/services/core/websocket/WebSocketManager.ts +2 -0
  232. package/src/services/core/websocket/types.ts +1 -1
  233. package/src/services/index.ts +1 -1
  234. package/src/services/task/Task.ts +908 -0
  235. package/src/services/task/TaskFactory.ts +60 -0
  236. package/src/services/task/TaskManager.ts +1419 -608
  237. package/src/services/task/TaskUtils.ts +303 -25
  238. package/src/services/task/WebexCallingUtils.ts +136 -0
  239. package/src/services/task/ai-docs/AGENTS.md +457 -0
  240. package/src/services/task/ai-docs/ARCHITECTURE.md +595 -0
  241. package/src/services/task/ai-docs/task-spec.md +1474 -0
  242. package/src/services/task/constants.ts +10 -0
  243. package/src/services/task/contact.ts +30 -0
  244. package/src/services/task/dialer.ts +80 -0
  245. package/src/services/task/digital/Digital.ts +97 -0
  246. package/src/services/task/state-machine/TaskStateMachine.ts +1320 -0
  247. package/src/services/task/state-machine/actions.ts +741 -0
  248. package/src/services/task/state-machine/ai-docs/AGENTS.md +460 -0
  249. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +1156 -0
  250. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +2247 -0
  251. package/src/services/task/state-machine/constants.ts +172 -0
  252. package/src/services/task/state-machine/guards.ts +442 -0
  253. package/src/services/task/state-machine/index.ts +28 -0
  254. package/src/services/task/state-machine/types.ts +258 -0
  255. package/src/services/task/state-machine/uiControlsComputer.ts +1135 -0
  256. package/src/services/task/taskDataNormalizer.ts +137 -0
  257. package/src/services/task/types.ts +819 -70
  258. package/src/services/task/voice/Voice.ts +1851 -0
  259. package/src/services/task/voice/WebRTC.ts +191 -0
  260. package/src/services/task/voice/wxAppVoiceMethods.ts +516 -0
  261. package/src/services/wxAppDiagnosticLogging.ts +174 -0
  262. package/src/services/wxAppTelephonyUtils.ts +14 -0
  263. package/src/types.ts +262 -11
  264. package/src/utils/AGENTS.md +289 -0
  265. package/src/utils/PageCache.ts +38 -5
  266. package/src/utils/ai-docs/utils-spec.md +391 -0
  267. package/src/webex.js +10 -0
  268. package/test/unit/spec/cc.ts +2210 -119
  269. package/test/unit/spec/logger-proxy.ts +70 -0
  270. package/test/unit/spec/metrics/MetricsManager.ts +25 -0
  271. package/test/unit/spec/metrics/behavioral-events.ts +60 -0
  272. package/test/unit/spec/services/AddressBook.ts +37 -6
  273. package/test/unit/spec/services/AnswerCallOnWebexService.ts +264 -0
  274. package/test/unit/spec/services/ApiAiAssistant.ts +311 -21
  275. package/test/unit/spec/services/EntryPoint.ts +87 -40
  276. package/test/unit/spec/services/Queue.ts +123 -12
  277. package/test/unit/spec/services/UserPreference.ts +401 -0
  278. package/test/unit/spec/services/WebCallingService.ts +7 -1
  279. package/test/unit/spec/services/WebexCrossClientService.ts +467 -0
  280. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +136 -0
  281. package/test/unit/spec/services/config/Util.ts +129 -0
  282. package/test/unit/spec/services/config/index.ts +97 -29
  283. package/test/unit/spec/services/core/Utils.ts +425 -8
  284. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  285. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  286. package/test/unit/spec/services/core/websocket/WebSocketManager.ts +66 -40
  287. package/test/unit/spec/services/task/AutoWrapup.ts +63 -0
  288. package/test/unit/spec/services/task/Task.ts +745 -0
  289. package/test/unit/spec/services/task/TaskFactory.ts +99 -0
  290. package/test/unit/spec/services/task/TaskManager.ts +2518 -1494
  291. package/test/unit/spec/services/task/TaskUtils.ts +235 -0
  292. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  293. package/test/unit/spec/services/task/contact.ts +33 -0
  294. package/test/unit/spec/services/task/dialer.ts +190 -0
  295. package/test/unit/spec/services/task/digital/Digital.ts +105 -0
  296. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +4060 -0
  297. package/test/unit/spec/services/task/state-machine/guards.ts +839 -0
  298. package/test/unit/spec/services/task/state-machine/types.ts +18 -0
  299. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +3101 -0
  300. package/test/unit/spec/services/task/taskTestUtils.ts +87 -0
  301. package/test/unit/spec/services/task/voice/Voice.ts +2061 -0
  302. package/test/unit/spec/services/task/voice/WebRTC.ts +235 -0
  303. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +603 -0
  304. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
  305. package/test/unit/spec/wellnessSampleUtils.ts +288 -0
  306. package/umd/contact-center.min.js +2 -2
  307. package/umd/contact-center.min.js.map +1 -1
  308. package/dist/services/task/index.js +0 -1525
  309. package/dist/services/task/index.js.map +0 -1
  310. package/dist/types/services/task/index.d.ts +0 -650
  311. package/src/services/task/index.ts +0 -1801
  312. package/test/unit/spec/services/task/index.ts +0 -2184
@@ -59,6 +59,7 @@ global.URL.createObjectURL = jest.fn(() => 'blob:http://localhost:3000/12345');
59
59
 
60
60
  describe('webex.cc', () => {
61
61
  let webex;
62
+ let mockApiAIAssistant;
62
63
  let mockContact;
63
64
  let mockTaskManager;
64
65
  let mockMetricsManager;
@@ -109,6 +110,14 @@ describe('webex.cc', () => {
109
110
  cancelCtq: jest.fn(),
110
111
  };
111
112
 
113
+ mockApiAIAssistant = {
114
+ sendEvent: jest.fn(),
115
+ getRealTimeAssistance: jest.fn(),
116
+ fetchHistoricTranscripts: jest.fn(),
117
+ setAIFeatureFlags: jest.fn(),
118
+ setAgentId: jest.fn(),
119
+ };
120
+
112
121
  // Mock Services instance
113
122
  const mockServicesInstance = {
114
123
  agent: {
@@ -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,24 @@ 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(),
161
174
  registerIncomingCallEvent: jest.fn(),
162
175
  registerTaskListeners: jest.fn(),
163
176
  getTask: jest.fn(),
164
177
  getActiveTasks: jest.fn(),
178
+ applyEnableWxBetterTogether: jest.fn(),
179
+ syncWxAppMuteFromCallDetailsForAllTasks: jest.fn(),
165
180
  on: jest.fn(),
166
181
  off: jest.fn(),
167
182
  emit: jest.fn(),
@@ -200,6 +215,35 @@ describe('webex.cc', () => {
200
215
  webex.emit('ready');
201
216
  });
202
217
 
218
+ it('should throw when WebRTC registration is disabled without multi-login', () => {
219
+ const invalidWebex = MockWebex({
220
+ children: {
221
+ mercury: Mercury,
222
+ },
223
+ logger: {
224
+ log: jest.fn(),
225
+ error: jest.fn(),
226
+ info: jest.fn(),
227
+ },
228
+ credentials: {
229
+ getOrgId: jest.fn(() => 'mockOrgId'),
230
+ },
231
+ config: {
232
+ ...config,
233
+ cc: {
234
+ ...config.cc,
235
+ allowMultiLogin: false,
236
+ disableWebRTCRegistration: true,
237
+ },
238
+ },
239
+ once: jest.fn((event, callback) => callback()),
240
+ }) as unknown as WebexSDK;
241
+
242
+ expect(() => new ContactCenter({parent: invalidWebex})).toThrow(
243
+ '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.'
244
+ );
245
+ });
246
+
203
247
  describe('cc.getDeviceId', () => {
204
248
  it('should return dialNumber when loginOption is EXTENSION', () => {
205
249
  const loginOption = LoginOption.EXTENSION;
@@ -267,14 +311,18 @@ describe('webex.cc', () => {
267
311
  isAgentAvailableAfterOutdial: false,
268
312
  isCampaignManagementEnabled: false,
269
313
  outDialEp: '',
270
- isEndCallEnabled: false,
314
+ isEndTaskEnabled: false,
271
315
  isEndConsultEnabled: false,
272
316
  agentDbId: '',
273
317
  allowConsultToQueue: false,
318
+ accessQueue: 'SPECIFIC',
319
+ accessEntryPoint: 'ALL',
320
+ accessBuddyTeam: 'ALL',
274
321
  agentPersonalStatsEnabled: false,
275
322
  isTimeoutDesktopInactivityEnabled: false,
276
323
  webRtcEnabled: true,
277
324
  lostConnectionRecoveryTimeout: 0,
325
+ isWellnessBreakEnabled: false,
278
326
  };
279
327
 
280
328
  it('should register successfully and return agent profile', async () => {
@@ -300,7 +348,7 @@ describe('webex.cc', () => {
300
348
  const result = await webex.cc.register();
301
349
 
302
350
  // Verify logging calls
303
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting CC SDK registration', {
351
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting CC SDK registration', {
304
352
  module: CC_FILE,
305
353
  method: 'register',
306
354
  });
@@ -334,6 +382,10 @@ describe('webex.cc', () => {
334
382
  TASK_EVENTS.TASK_HYDRATE,
335
383
  expect.any(Function)
336
384
  );
385
+ expect(mockTaskManager.on).toHaveBeenCalledWith(
386
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
387
+ expect.any(Function)
388
+ );
337
389
  expect(mockWebSocketManager.on).toHaveBeenCalledWith('message', expect.any(Function));
338
390
  expect(webex.cc.services.rtdWebSocketManager.initWebSocket).toHaveBeenCalledWith({
339
391
  body: {
@@ -354,6 +406,20 @@ describe('webex.cc', () => {
354
406
  module: CC_FILE,
355
407
  method: 'connectWebsocket',
356
408
  });
409
+ expect(mockTaskManager.setConfigFlags).toHaveBeenCalledWith({
410
+ isEndTaskEnabled: mockAgentProfile.isEndTaskEnabled,
411
+ isEndConsultEnabled: mockAgentProfile.isEndConsultEnabled,
412
+ webRtcEnabled: mockAgentProfile.webRtcEnabled,
413
+ autoWrapup: mockAgentProfile.wrapUpData.wrapUpProps.autoWrapup ?? false,
414
+ aiFeature: mockAgentProfile.aiFeature,
415
+ consultTransfer: {
416
+ allowConsultToQueue: mockAgentProfile.allowConsultToQueue,
417
+ accessQueue: mockAgentProfile.accessQueue,
418
+ accessEntryPoint: mockAgentProfile.accessEntryPoint,
419
+ accessBuddyTeam: mockAgentProfile.accessBuddyTeam,
420
+ },
421
+ enableWxBetterTogether: false,
422
+ });
357
423
  expect(reloadSpy).toHaveBeenCalled();
358
424
  expect(result).toEqual(mockAgentProfile);
359
425
  expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
@@ -413,7 +479,7 @@ describe('webex.cc', () => {
413
479
 
414
480
  await expect(webex.cc.register()).rejects.toThrow('Error while performing register');
415
481
 
416
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting CC SDK registration', {
482
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting CC SDK registration', {
417
483
  module: CC_FILE,
418
484
  method: 'register',
419
485
  });
@@ -432,6 +498,25 @@ describe('webex.cc', () => {
432
498
  );
433
499
  });
434
500
 
501
+ it('clears wellness session context when registration fails after relogin', async () => {
502
+ const mockError = new Error('Error after silent relogin');
503
+ webex.cc.$config = {...webex.cc.$config, allowAutomatedRelogin: true};
504
+ jest.spyOn(webex.cc.services.config, 'getAgentConfig').mockResolvedValue({
505
+ ...mockAgentProfile,
506
+ webRtcEnabled: false,
507
+ aiFeature: undefined,
508
+ } as Profile);
509
+ mockWebSocketManager.initWebSocket.mockResolvedValue({agentId: 'agent123'});
510
+ jest.spyOn(webex.cc as any, 'silentRelogin').mockImplementation(async () => {
511
+ webex.cc['updateWellnessSession']('partial-session');
512
+ throw mockError;
513
+ });
514
+
515
+ await expect(webex.cc.register()).rejects.toThrow(mockError);
516
+
517
+ expect(webex.cc['currentAgentSessionId']).toBeUndefined();
518
+ });
519
+
435
520
  it('should log error if mercury connect fails but cc.register() should not fail', async () => {
436
521
  const mockError = new Error('Error while performing mercury connect');
437
522
  jest.spyOn(webex.internal.mercury, 'connect').mockRejectedValue(mockError);
@@ -482,6 +567,10 @@ describe('webex.cc', () => {
482
567
  TASK_EVENTS.TASK_HYDRATE,
483
568
  expect.any(Function)
484
569
  );
570
+ expect(mockTaskManager.on).toHaveBeenCalledWith(
571
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
572
+ expect.any(Function)
573
+ );
485
574
  expect(mockWebSocketManager.on).toHaveBeenCalledWith('message', expect.any(Function));
486
575
 
487
576
  expect(configSpy).toHaveBeenCalled();
@@ -493,6 +582,36 @@ describe('webex.cc', () => {
493
582
  expect(result).toEqual(mockAgentProfile);
494
583
  });
495
584
 
585
+ it('should skip mercury connection when disableWebRTCRegistration is enabled', async () => {
586
+ webex.cc.$config = {
587
+ ...webex.cc.$config,
588
+ allowAutomatedRelogin: false,
589
+ disableWebRTCRegistration: true,
590
+ };
591
+ mockAgentProfile.webRtcEnabled = true;
592
+ const mercurySpy = jest.spyOn(webex.internal.mercury, 'connect');
593
+ const connectWebsocketSpy = jest.spyOn(webex.cc, 'connectWebsocket');
594
+ const setupEventListenersSpy = jest.spyOn(webex.cc, 'setupEventListeners');
595
+ jest.spyOn(webex.cc.services.config, 'getAgentConfig').mockResolvedValue(mockAgentProfile);
596
+ mockWebSocketManager.initWebSocket.mockResolvedValue({
597
+ agentId: 'agent123',
598
+ });
599
+
600
+ const result = await webex.cc.register();
601
+
602
+ expect(connectWebsocketSpy).toHaveBeenCalled();
603
+ expect(setupEventListenersSpy).toHaveBeenCalled();
604
+ expect(mercurySpy).not.toHaveBeenCalled();
605
+ expect(LoggerProxy.info).toHaveBeenCalledWith(
606
+ 'Skipping Mobius registration because disableWebRTCRegistration is enabled',
607
+ {
608
+ module: CC_FILE,
609
+ method: 'connectWebsocket',
610
+ }
611
+ );
612
+ expect(result).toEqual(mockAgentProfile);
613
+ });
614
+
496
615
  it('should not attempt for mercury connection when webrtc is disabled', async () => {
497
616
  mockAgentProfile.webRtcEnabled = false;
498
617
  mockAgentProfile.aiFeature = {realtimeTranscripts: {enable: false}} as any;
@@ -631,7 +750,7 @@ describe('webex.cc', () => {
631
750
  expect(registerWebCallingLineSpy).toHaveBeenCalled();
632
751
  expect(stationLoginMock).toHaveBeenCalledWith({
633
752
  data: {
634
- dialNumber: 'agentId',
753
+ dialNumber: `${WEB_RTC_PREFIX}agentId`,
635
754
  teamId: 'teamId',
636
755
  deviceType: LoginOption.BROWSER,
637
756
  isExtension: false,
@@ -649,6 +768,7 @@ describe('webex.cc', () => {
649
768
  METRIC_EVENT_NAMES.STATION_LOGIN_FAILED,
650
769
  ]);
651
770
  expect(result).toEqual(responseMock);
771
+ expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.BROWSER);
652
772
 
653
773
  const onSpy = jest.spyOn(mockTaskManager, 'on');
654
774
  const emitSpy = jest.spyOn(webex.cc, 'trigger');
@@ -661,10 +781,8 @@ describe('webex.cc', () => {
661
781
 
662
782
  expect(emitSpy).toHaveBeenCalledTimes(1);
663
783
  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];
784
+ // Verify websocket message handling
785
+ const messageCallback = webex.cc['handleWebsocketMessage'];
668
786
  const agentStateChangeEventData = {
669
787
  type: CC_EVENTS.AGENT_STATE_CHANGE,
670
788
  data: {some: 'data'},
@@ -692,6 +810,59 @@ describe('webex.cc', () => {
692
810
  );
693
811
  });
694
812
 
813
+ it('should skip web calling line registration when disableWebRTCRegistration is enabled', async () => {
814
+ const options = {
815
+ teamId: 'teamId',
816
+ loginOption: LoginOption.BROWSER,
817
+ };
818
+
819
+ webex.cc.$config = {
820
+ ...webex.cc.$config,
821
+ disableWebRTCRegistration: true,
822
+ };
823
+ webex.cc.agentConfig = {
824
+ agentId: 'agentId',
825
+ webRtcEnabled: true,
826
+ loginVoiceOptions: ['BROWSER', 'EXTENSION', 'AGENT_DN'],
827
+ };
828
+
829
+ const registerWebCallingLineSpy = jest.spyOn(
830
+ webex.cc.webCallingService,
831
+ 'registerWebCallingLine'
832
+ );
833
+
834
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
835
+ data: {
836
+ loginOption: LoginOption.BROWSER,
837
+ agentId: 'agentId',
838
+ teamId: 'teamId',
839
+ siteId: 'siteId',
840
+ roles: [AGENT],
841
+ channelsMap: {
842
+ chat: [],
843
+ email: [],
844
+ social: [],
845
+ telephony: [],
846
+ },
847
+ },
848
+ trackingId: 'notifs_52628',
849
+ orgId: 'orgId',
850
+ type: 'StationLoginSuccess',
851
+ eventType: 'STATION_LOGIN',
852
+ } as unknown as StationLoginSuccess);
853
+
854
+ await webex.cc.stationLogin(options);
855
+
856
+ expect(registerWebCallingLineSpy).not.toHaveBeenCalled();
857
+ expect(LoggerProxy.info).toHaveBeenCalledWith(
858
+ 'Skipping web calling line registration because disableWebRTCRegistration is enabled',
859
+ {
860
+ module: CC_FILE,
861
+ method: 'stationLogin',
862
+ }
863
+ );
864
+ });
865
+
695
866
  it('should not attempt mobius registration for LoginOption.BROWSER if webrtc is disabled', async () => {
696
867
  const options = {
697
868
  teamId: 'teamId',
@@ -739,7 +910,7 @@ describe('webex.cc', () => {
739
910
  expect(registerWebCallingLineSpy).not.toHaveBeenCalled();
740
911
  expect(stationLoginSpy).toHaveBeenCalledWith({
741
912
  data: {
742
- dialNumber: 'agentId',
913
+ dialNumber: `${WEB_RTC_PREFIX}agentId`,
743
914
  teamId: 'teamId',
744
915
  deviceType: LoginOption.BROWSER,
745
916
  isExtension: false,
@@ -814,10 +985,13 @@ describe('webex.cc', () => {
814
985
  const result = await webex.cc.stationLogin(options);
815
986
 
816
987
  // Verify logging calls
817
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station login', {
818
- module: CC_FILE,
819
- method: 'stationLogin',
820
- });
988
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
989
+ `Starting agent station login | loginOption: ${options.loginOption} teamId: ${options.teamId}`,
990
+ {
991
+ module: CC_FILE,
992
+ method: 'stationLogin',
993
+ }
994
+ );
821
995
  expect(LoggerProxy.log).toHaveBeenCalledWith(
822
996
  `Agent station login completed successfully agentId: ${mockData.data.agentId} loginOption: ${mockData.data.loginOption} teamId: ${mockData.data.teamId}`,
823
997
  {
@@ -868,125 +1042,971 @@ describe('webex.cc', () => {
868
1042
 
869
1043
  await expect(webex.cc.stationLogin(options)).rejects.toThrow(error.details.data.reason);
870
1044
 
871
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station login', {
872
- module: CC_FILE,
873
- method: 'stationLogin',
874
- });
1045
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
1046
+ `Starting agent station login | loginOption: ${options.loginOption} teamId: ${options.teamId}`,
1047
+ {
1048
+ module: CC_FILE,
1049
+ method: 'stationLogin',
1050
+ }
1051
+ );
875
1052
  expect(LoggerProxy.error).toHaveBeenCalledWith(
876
1053
  `stationLogin failed with reason: ${error.details.data.reason}`,
877
1054
  {module: CC_FILE, method: 'stationLogin', trackingId: error.details.trackingId}
878
1055
  );
879
1056
  });
880
- });
881
1057
 
882
- describe('stationLogout', () => {
883
- it('should logout successfully', async () => {
884
- const data = {logoutReason: 'Logout reason'};
885
- const response = {};
1058
+ it('should publish usersub and subscribe Mercury on station login when enableWxBetterTogether is true at init', async () => {
1059
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1060
+ webex.internal.device = {
1061
+ userId: 'user-123',
1062
+ url: 'https://wdm.example.com/devices/dev-1',
1063
+ registered: true,
1064
+ register: jest.fn().mockResolvedValue(undefined),
1065
+ };
1066
+ webex.internal.mercury.connected = true;
1067
+ webex.internal.mercury.connect = jest.fn().mockResolvedValue(undefined);
1068
+ webex.cc.agentConfig = {
1069
+ agentId: 'agentId',
1070
+ webRtcEnabled: false,
1071
+ loginVoiceOptions: ['EXTENSION'],
1072
+ };
1073
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
886
1074
 
887
- const stationLogoutMock = jest
888
- .spyOn(webex.cc.services.agent, 'logout')
889
- .mockResolvedValue({} as StationLogoutResponse);
1075
+ const publishSpy = jest
1076
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1077
+ .mockResolvedValue(undefined);
1078
+ const subscribeSpy = jest
1079
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
1080
+ .mockImplementation(() => {});
1081
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
890
1082
 
891
- const result = await webex.cc.stationLogout(data);
1083
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1084
+ data: {
1085
+ agentId: 'agentId',
1086
+ teamId: 'teamId',
1087
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1088
+ },
1089
+ trackingId: 'track-1',
1090
+ } as StationLoginSuccess);
892
1091
 
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',
1092
+ await webex.cc.stationLogin({
1093
+ teamId: 'teamId',
1094
+ loginOption: LoginOption.EXTENSION,
1095
+ dialNumber: '1001',
901
1096
  });
902
1097
 
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
- ]);
1098
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
1099
+ expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
920
1100
  });
921
1101
 
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
- },
1102
+ it('should rollback wxApp config when post-station-login init fails', async () => {
1103
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1104
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1105
+ webex.cc.agentConfig = {
1106
+ agentId: 'agentId',
1107
+ webRtcEnabled: false,
1108
+ loginVoiceOptions: ['EXTENSION'],
931
1109
  };
1110
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1111
+ mockTaskManager.applyEnableWxBetterTogether(true);
932
1112
 
933
- jest.spyOn(webex.cc.services.agent, 'logout').mockRejectedValue(error);
1113
+ jest
1114
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
1115
+ .mockRejectedValue(new Error('usersub publish failed'));
1116
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
934
1117
 
935
- await expect(webex.cc.stationLogout(data)).rejects.toThrow(error.details.data.reason);
1118
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1119
+ data: {
1120
+ agentId: 'agentId',
1121
+ teamId: 'teamId',
1122
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1123
+ },
1124
+ trackingId: 'track-1',
1125
+ } as StationLoginSuccess);
936
1126
 
937
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
938
- module: CC_FILE,
939
- method: 'stationLogout',
1127
+ await webex.cc.stationLogin({
1128
+ teamId: 'teamId',
1129
+ loginOption: LoginOption.EXTENSION,
1130
+ dialNumber: '1001',
940
1131
  });
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
- );
1132
+
1133
+ expect(unsubscribeSpy).toHaveBeenCalled();
1134
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1135
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
945
1136
  });
946
- });
947
1137
 
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',
1138
+ it('should reset wxApp config when Mercury fails before usersub publish on station login', async () => {
1139
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1140
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1141
+ webex.cc.agentConfig = {
1142
+ agentId: 'agentId',
1143
+ webRtcEnabled: false,
1144
+ loginVoiceOptions: ['EXTENSION'],
955
1145
  };
1146
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1147
+ mockTaskManager.applyEnableWxBetterTogether(true);
1148
+
1149
+ const publishSpy = jest
1150
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1151
+ .mockResolvedValue(undefined);
1152
+ jest
1153
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
1154
+ .mockRejectedValue(new Error('mercury connect failed'));
1155
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1156
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1157
+ jest
1158
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1159
+ .mockResolvedValue(undefined);
1160
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
956
1161
 
957
- const setAgentStatusMock = jest
958
- .spyOn(webex.cc.services.agent, 'stateChange')
959
- .mockResolvedValue({data: expectedPayload});
960
-
961
- const result = await webex.cc.setAgentState(expectedPayload);
1162
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1163
+ data: {
1164
+ agentId: 'agentId',
1165
+ teamId: 'teamId',
1166
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1167
+ },
1168
+ trackingId: 'track-1',
1169
+ } as StationLoginSuccess);
962
1170
 
963
- // Verify logging calls
964
- expect(LoggerProxy.info).toHaveBeenCalledWith('Setting agent state', {
965
- module: CC_FILE,
966
- method: 'setAgentState',
1171
+ await webex.cc.stationLogin({
1172
+ teamId: 'teamId',
1173
+ loginOption: LoginOption.EXTENSION,
1174
+ dialNumber: '1001',
967
1175
  });
968
- expect(LoggerProxy.log).toHaveBeenCalledWith(
969
- `Agent state changed successfully to auxCodeId: ${expectedPayload.auxCodeId}`,
970
- {
971
- module: CC_FILE,
972
- method: 'setAgentState',
973
- }
974
- );
975
1176
 
976
- expect(setAgentStatusMock).toHaveBeenCalledWith({data: expectedPayload});
977
- expect(result).toEqual({data: expectedPayload});
978
- expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
979
- METRIC_EVENT_NAMES.AGENT_STATE_CHANGE_SUCCESS,
980
- METRIC_EVENT_NAMES.AGENT_STATE_CHANGE_FAILED,
981
- ]);
1177
+ expect(publishSpy).not.toHaveBeenCalled();
1178
+ expect(teardownSpy).not.toHaveBeenCalled();
1179
+ expect(unsubscribeSpy).toHaveBeenCalled();
1180
+ expect(metricSpy).toHaveBeenCalledWith(
1181
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1182
+ expect.objectContaining({skipReason: 'mercury_subscribe_failed'}),
1183
+ ['operational', 'behavioral']
1184
+ );
1185
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1186
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
982
1187
  });
983
1188
 
984
- it('should set agent status successfully when status is Meeting', async () => {
985
- const expectedPayload = {
986
- state: 'Meeting',
987
- auxCodeId: '12345',
988
- agentId: '123',
989
- lastStateChangeReason: 'Agent is in meeting',
1189
+ it('should record publish_failed when usersub publish fails after Mercury init', async () => {
1190
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1191
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1192
+ webex.cc.agentConfig = {
1193
+ agentId: 'agentId',
1194
+ webRtcEnabled: false,
1195
+ loginVoiceOptions: ['EXTENSION'],
1196
+ };
1197
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1198
+ mockTaskManager.applyEnableWxBetterTogether(true);
1199
+
1200
+ jest.spyOn(webex.cc as any, 'ensureWxAppMercuryConnected').mockResolvedValue(undefined);
1201
+ jest
1202
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
1203
+ .mockImplementation(() => {});
1204
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1205
+ jest
1206
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1207
+ .mockRejectedValue(new Error('publish failed'));
1208
+ jest
1209
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1210
+ .mockResolvedValue(undefined);
1211
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1212
+
1213
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1214
+ data: {
1215
+ agentId: 'agentId',
1216
+ teamId: 'teamId',
1217
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1218
+ },
1219
+ trackingId: 'track-1',
1220
+ } as StationLoginSuccess);
1221
+
1222
+ await webex.cc.stationLogin({
1223
+ teamId: 'teamId',
1224
+ loginOption: LoginOption.EXTENSION,
1225
+ dialNumber: '1001',
1226
+ });
1227
+
1228
+ expect(metricSpy).toHaveBeenCalledWith(
1229
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1230
+ expect.objectContaining({skipReason: 'publish_failed'}),
1231
+ ['operational', 'behavioral']
1232
+ );
1233
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1234
+ });
1235
+
1236
+ it('should register device before publishing usersub true on extension station login', async () => {
1237
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1238
+ webex.internal.device = {
1239
+ userId: 'user-123',
1240
+ registered: false,
1241
+ register: jest.fn().mockImplementation(async function (this: {
1242
+ url?: string;
1243
+ registered?: boolean;
1244
+ }) {
1245
+ this.url = 'https://wdm.example.com/devices/dev-1';
1246
+ this.registered = true;
1247
+ }),
1248
+ };
1249
+ webex.internal.mercury = {
1250
+ connected: false,
1251
+ connect: jest.fn().mockResolvedValue(undefined),
1252
+ };
1253
+ webex.cc.agentConfig = {
1254
+ agentId: 'agentId',
1255
+ webRtcEnabled: false,
1256
+ loginVoiceOptions: ['EXTENSION'],
1257
+ };
1258
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1259
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1260
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1261
+
1262
+ const callOrder: string[] = [];
1263
+ jest.spyOn(webex.internal.device, 'register').mockImplementation(async function (this: {
1264
+ url?: string;
1265
+ registered?: boolean;
1266
+ }) {
1267
+ callOrder.push('device.register');
1268
+ this.url = 'https://wdm.example.com/devices/dev-1';
1269
+ this.registered = true;
1270
+ });
1271
+ jest
1272
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1273
+ .mockImplementation(async (enable: boolean) => {
1274
+ if (enable === true && !webex.internal.device.url) {
1275
+ throw new Error('Device URL is unavailable for cross-client publish');
1276
+ }
1277
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
1278
+ });
1279
+
1280
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1281
+ data: {
1282
+ agentId: 'agentId',
1283
+ teamId: 'teamId',
1284
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1285
+ },
1286
+ trackingId: 'track-1',
1287
+ } as StationLoginSuccess);
1288
+
1289
+ await webex.cc.stationLogin({
1290
+ teamId: 'teamId',
1291
+ loginOption: LoginOption.EXTENSION,
1292
+ dialNumber: '1001',
1293
+ });
1294
+
1295
+ expect(callOrder).toEqual(['device.register', 'publish:true']);
1296
+ });
1297
+
1298
+ it('should register device before publishing forced usersub false when wxApp is disabled at station login', async () => {
1299
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1300
+ webex.internal.device = {
1301
+ userId: 'user-123',
1302
+ registered: false,
1303
+ register: jest.fn().mockImplementation(async function (this: {
1304
+ url?: string;
1305
+ registered?: boolean;
1306
+ }) {
1307
+ this.url = 'https://wdm.example.com/devices/dev-1';
1308
+ this.registered = true;
1309
+ }),
1310
+ };
1311
+ webex.internal.mercury = {
1312
+ connected: false,
1313
+ connect: jest.fn().mockResolvedValue(undefined),
1314
+ };
1315
+ webex.cc.agentConfig = {
1316
+ agentId: 'agentId',
1317
+ webRtcEnabled: false,
1318
+ loginVoiceOptions: ['EXTENSION'],
1319
+ };
1320
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1321
+ jest
1322
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1323
+ .mockResolvedValue(undefined);
1324
+
1325
+ const callOrder: string[] = [];
1326
+ jest.spyOn(webex.internal.device, 'register').mockImplementation(async function (this: {
1327
+ url?: string;
1328
+ registered?: boolean;
1329
+ }) {
1330
+ callOrder.push('device.register');
1331
+ this.url = 'https://wdm.example.com/devices/dev-1';
1332
+ this.registered = true;
1333
+ });
1334
+ jest
1335
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1336
+ .mockImplementation(async (enable: boolean) => {
1337
+ if (!webex.internal.device.url) {
1338
+ throw new Error('Device URL is unavailable for cross-client publish');
1339
+ }
1340
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
1341
+ });
1342
+
1343
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1344
+ data: {
1345
+ agentId: 'agentId',
1346
+ teamId: 'teamId',
1347
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1348
+ },
1349
+ trackingId: 'track-1',
1350
+ } as StationLoginSuccess);
1351
+
1352
+ await webex.cc.stationLogin({
1353
+ teamId: 'teamId',
1354
+ loginOption: LoginOption.EXTENSION,
1355
+ dialNumber: '1001',
1356
+ });
1357
+
1358
+ expect(callOrder).toEqual(['device.register', 'publish:false']);
1359
+ });
1360
+
1361
+ it('should retry forced false publish when wxApp is disabled at station login', async () => {
1362
+ jest.useFakeTimers();
1363
+ try {
1364
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1365
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1366
+ webex.cc.agentConfig = {
1367
+ agentId: 'agentId',
1368
+ webRtcEnabled: false,
1369
+ loginVoiceOptions: ['EXTENSION'],
1370
+ };
1371
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1372
+
1373
+ const publishSpy = jest
1374
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1375
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
1376
+ .mockResolvedValue(undefined);
1377
+ const scheduleRetrySpy = jest.spyOn(
1378
+ webex.cc as any,
1379
+ 'scheduleForcedFalsePublishRetry'
1380
+ );
1381
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1382
+ jest
1383
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1384
+ .mockResolvedValue(undefined);
1385
+
1386
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1387
+ data: {
1388
+ agentId: 'agentId',
1389
+ teamId: 'teamId',
1390
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1391
+ },
1392
+ trackingId: 'track-1',
1393
+ } as StationLoginSuccess);
1394
+
1395
+ await webex.cc.stationLogin({
1396
+ teamId: 'teamId',
1397
+ loginOption: LoginOption.EXTENSION,
1398
+ dialNumber: '1001',
1399
+ });
1400
+
1401
+ expect(publishSpy).toHaveBeenCalledTimes(1);
1402
+ expect(scheduleRetrySpy).toHaveBeenCalled();
1403
+ expect(teardownSpy).not.toHaveBeenCalled();
1404
+
1405
+ jest.advanceTimersByTime(30_000);
1406
+ await publishSpy.mock.results[1]?.value;
1407
+
1408
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1409
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1410
+ } finally {
1411
+ jest.useRealTimers();
1412
+ }
1413
+ });
1414
+
1415
+ it('should not publish usersub on BROWSER station login even when enableWxBetterTogether is true at init', async () => {
1416
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1417
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1418
+ webex.cc.agentConfig = {
1419
+ agentId: 'agentId',
1420
+ webRtcEnabled: true,
1421
+ loginVoiceOptions: ['BROWSER'],
1422
+ };
1423
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
1424
+
1425
+ const publishSpy = jest.spyOn(
1426
+ webex.cc['webexCrossClientService'],
1427
+ 'setManageWebexCallingInWxcc'
1428
+ );
1429
+
1430
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1431
+ data: {
1432
+ agentId: 'agentId',
1433
+ teamId: 'teamId',
1434
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1435
+ },
1436
+ trackingId: 'track-1',
1437
+ } as StationLoginSuccess);
1438
+
1439
+ await webex.cc.stationLogin({
1440
+ teamId: 'teamId',
1441
+ loginOption: LoginOption.BROWSER,
1442
+ });
1443
+
1444
+ expect(publishSpy).not.toHaveBeenCalled();
1445
+ });
1446
+
1447
+ it('should force-publish usersub false on station login when enableWxBetterTogether is false at init', async () => {
1448
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1449
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1450
+ webex.cc.agentConfig = {
1451
+ agentId: 'agentId',
1452
+ webRtcEnabled: false,
1453
+ loginVoiceOptions: ['EXTENSION'],
1454
+ };
1455
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1456
+
1457
+ const publishSpy = jest
1458
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1459
+ .mockResolvedValue(undefined);
1460
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1461
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1462
+
1463
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1464
+ data: {
1465
+ agentId: 'agentId',
1466
+ teamId: 'teamId',
1467
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1468
+ },
1469
+ trackingId: 'track-1',
1470
+ } as StationLoginSuccess);
1471
+
1472
+ await webex.cc.stationLogin({
1473
+ teamId: 'teamId',
1474
+ loginOption: LoginOption.EXTENSION,
1475
+ dialNumber: '1001',
1476
+ });
1477
+
1478
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1479
+ expect(teardownSpy).toHaveBeenCalled();
1480
+ expect(unsubscribeSpy).toHaveBeenCalled();
1481
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
1482
+ METRIC_EVENT_NAMES.WXAPP_SESSION_SKIPPED,
1483
+ expect.objectContaining({
1484
+ enableWxBetterTogether: false,
1485
+ skipReason: 'flag_disabled',
1486
+ }),
1487
+ ['operational', 'behavioral']
1488
+ );
1489
+ });
1490
+
1491
+ it('should not emit WXAPP_SESSION_INIT_FAILED when flag-disabled cleanup fails', async () => {
1492
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1493
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1494
+ webex.cc.agentConfig = {
1495
+ agentId: 'agentId',
1496
+ webRtcEnabled: false,
1497
+ loginVoiceOptions: ['EXTENSION'],
1498
+ };
1499
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1500
+
1501
+ jest
1502
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1503
+ .mockRejectedValue(new Error('forced false publish failed'));
1504
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1505
+
1506
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1507
+ data: {
1508
+ agentId: 'agentId',
1509
+ teamId: 'teamId',
1510
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1511
+ },
1512
+ trackingId: 'track-1',
1513
+ } as StationLoginSuccess);
1514
+
1515
+ await webex.cc.stationLogin({
1516
+ teamId: 'teamId',
1517
+ loginOption: LoginOption.EXTENSION,
1518
+ dialNumber: '1001',
1519
+ });
1520
+
1521
+ expect(metricSpy).not.toHaveBeenCalledWith(
1522
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1523
+ expect.anything(),
1524
+ expect.anything()
1525
+ );
1526
+ });
1527
+
1528
+ it('should still publish usersub but record session init failed when Mercury is unavailable', async () => {
1529
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1530
+ webex.internal.device = {
1531
+ userId: 'user-123',
1532
+ url: 'https://wdm.example.com/devices/dev-1',
1533
+ registered: true,
1534
+ register: jest.fn().mockResolvedValue(undefined),
1535
+ };
1536
+ delete webex.internal.mercury;
1537
+ webex.cc.agentConfig = {
1538
+ agentId: 'agentId',
1539
+ webRtcEnabled: false,
1540
+ loginVoiceOptions: ['EXTENSION'],
1541
+ };
1542
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1543
+ mockTaskManager.applyEnableWxBetterTogether(true);
1544
+
1545
+ const publishSpy = jest
1546
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1547
+ .mockResolvedValue(undefined);
1548
+ jest
1549
+ .spyOn(webex.cc['webexCrossClientService'], 'isAnswerCallsStateActive')
1550
+ .mockReturnValue(true);
1551
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1552
+
1553
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1554
+ data: {
1555
+ agentId: 'agentId',
1556
+ teamId: 'teamId',
1557
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1558
+ },
1559
+ trackingId: 'track-1',
1560
+ } as StationLoginSuccess);
1561
+
1562
+ await webex.cc.stationLogin({
1563
+ teamId: 'teamId',
1564
+ loginOption: LoginOption.EXTENSION,
1565
+ dialNumber: '1001',
1566
+ });
1567
+
1568
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
1569
+ expect(metricSpy).toHaveBeenCalledWith(
1570
+ METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_FAILED,
1571
+ expect.objectContaining({error: 'Mercury is unavailable for wxApp mute sync'}),
1572
+ ['operational', 'behavioral']
1573
+ );
1574
+ expect(metricSpy).not.toHaveBeenCalledWith(
1575
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_SUCCESS,
1576
+ expect.anything(),
1577
+ expect.anything()
1578
+ );
1579
+ expect(metricSpy).toHaveBeenCalledWith(
1580
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1581
+ expect.objectContaining({skipReason: 'mercury_not_subscribed'}),
1582
+ ['operational', 'behavioral']
1583
+ );
1584
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
1585
+ });
1586
+
1587
+ it('should record session init failed when userId is missing but not emit success', async () => {
1588
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1589
+ webex.internal.device = {
1590
+ url: 'https://wdm.example.com/devices/dev-1',
1591
+ registered: true,
1592
+ register: jest.fn().mockResolvedValue(undefined),
1593
+ };
1594
+ webex.internal.mercury = {
1595
+ connected: true,
1596
+ connect: jest.fn().mockResolvedValue(undefined),
1597
+ on: jest.fn(),
1598
+ off: jest.fn(),
1599
+ };
1600
+ webex.cc.agentConfig = {
1601
+ agentId: 'agentId',
1602
+ webRtcEnabled: false,
1603
+ loginVoiceOptions: ['EXTENSION'],
1604
+ };
1605
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1606
+ mockTaskManager.applyEnableWxBetterTogether(true);
1607
+
1608
+ const publishSpy = jest
1609
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1610
+ .mockResolvedValue(undefined);
1611
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1612
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1613
+
1614
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1615
+ data: {
1616
+ agentId: 'agentId',
1617
+ teamId: 'teamId',
1618
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1619
+ },
1620
+ trackingId: 'track-1',
1621
+ } as StationLoginSuccess);
1622
+
1623
+ await webex.cc.stationLogin({
1624
+ teamId: 'teamId',
1625
+ loginOption: LoginOption.EXTENSION,
1626
+ dialNumber: '1001',
1627
+ });
1628
+
1629
+ expect(publishSpy).not.toHaveBeenCalledWith(true, expect.anything());
1630
+ expect(metricSpy).not.toHaveBeenCalledWith(
1631
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_SUCCESS,
1632
+ expect.anything(),
1633
+ expect.anything()
1634
+ );
1635
+ expect(metricSpy).toHaveBeenCalledWith(
1636
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1637
+ expect.objectContaining({skipReason: 'usersub_not_published'}),
1638
+ ['operational', 'behavioral']
1639
+ );
1640
+ expect(metricSpy).toHaveBeenCalledWith(
1641
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
1642
+ expect.objectContaining({skipReason: 'user_id_unavailable'}),
1643
+ ['operational', 'behavioral']
1644
+ );
1645
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
1646
+ });
1647
+
1648
+ it('should clear stale usersub on login after prior session enabled wxApp then hard refresh', async () => {
1649
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1650
+ webex.cc.agentConfig = {
1651
+ agentId: 'agentId',
1652
+ webRtcEnabled: false,
1653
+ loginVoiceOptions: ['EXTENSION'],
1654
+ };
1655
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1656
+ webex.request = jest.fn().mockResolvedValue({body: {}});
1657
+
1658
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1659
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1660
+ data: {
1661
+ agentId: 'agentId',
1662
+ teamId: 'teamId',
1663
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1664
+ },
1665
+ trackingId: 'track-1',
1666
+ } as StationLoginSuccess);
1667
+
1668
+ await webex.cc.stationLogin({
1669
+ teamId: 'teamId',
1670
+ loginOption: LoginOption.EXTENSION,
1671
+ dialNumber: '1001',
1672
+ });
1673
+
1674
+ webex.cc['webexCrossClientService'].teardown();
1675
+ webex.cc['wxAppTelephonyMercurySync'].unsubscribe();
1676
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1677
+
1678
+ const publishSpy = jest
1679
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1680
+ .mockResolvedValue(undefined);
1681
+
1682
+ await webex.cc.stationLogin({
1683
+ teamId: 'teamId',
1684
+ loginOption: LoginOption.EXTENSION,
1685
+ dialNumber: '1001',
1686
+ });
1687
+
1688
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1689
+ });
1690
+ });
1691
+
1692
+ describe('stationLogout', () => {
1693
+ it('should logout successfully', async () => {
1694
+ const data = {logoutReason: 'Logout reason'};
1695
+ const response = {};
1696
+
1697
+ const stationLogoutMock = jest
1698
+ .spyOn(webex.cc.services.agent, 'logout')
1699
+ .mockResolvedValue({} as StationLogoutResponse);
1700
+
1701
+ const result = await webex.cc.stationLogout(data);
1702
+
1703
+ // Verify logging calls
1704
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
1705
+ module: CC_FILE,
1706
+ method: 'stationLogout',
1707
+ });
1708
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Agent station logout completed successfully', {
1709
+ module: CC_FILE,
1710
+ method: 'stationLogout',
1711
+ });
1712
+
1713
+ expect(stationLogoutMock).toHaveBeenCalledWith({data: data});
1714
+ // TODO: https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-626777 Implement the de-register method and close the listener there
1715
+ // expect(mockTaskManager.unregisterIncomingCallEvent).toHaveBeenCalledWith();
1716
+ // expect(mockTaskManager.off).toHaveBeenCalledWith(
1717
+ // TASK_EVENTS.TASK_INCOMING,
1718
+ // expect.any(Function)
1719
+ // );
1720
+ // expect(mockTaskManager.off).toHaveBeenCalledWith(
1721
+ // TASK_EVENTS.TASK_HYDRATE,
1722
+ // expect.any(Function)
1723
+ // );
1724
+ // expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1725
+ expect(result).toEqual(response);
1726
+ expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
1727
+ METRIC_EVENT_NAMES.STATION_LOGOUT_SUCCESS,
1728
+ METRIC_EVENT_NAMES.STATION_LOGOUT_FAILED,
1729
+ ]);
1730
+ });
1731
+
1732
+ it('should not publish usersub false on logout when wxApp was never enabled', async () => {
1733
+ webex.internal.device = {userId: 'user-123'};
1734
+ const publishSpy = jest
1735
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1736
+ .mockResolvedValue(undefined);
1737
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1738
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1739
+
1740
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1741
+ trackingId: 'track-1',
1742
+ } as StationLogoutResponse);
1743
+
1744
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1745
+
1746
+ expect(publishSpy).not.toHaveBeenCalled();
1747
+ expect(teardownSpy).toHaveBeenCalled();
1748
+ expect(unsubscribeSpy).toHaveBeenCalled();
1749
+ });
1750
+
1751
+ it('should publish usersub false and teardown wxApp sync on logout when wxApp was enabled', async () => {
1752
+ webex.internal.device = {
1753
+ userId: 'user-123',
1754
+ url: 'https://wdm.example.com/devices/dev-1',
1755
+ registered: false,
1756
+ register: jest.fn().mockResolvedValue(undefined),
1757
+ unregister: jest.fn().mockResolvedValue(undefined),
1758
+ };
1759
+ webex.internal.mercury = {
1760
+ connected: false,
1761
+ connect: jest.fn().mockResolvedValue(undefined),
1762
+ disconnect: jest.fn().mockResolvedValue(undefined),
1763
+ };
1764
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1765
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1766
+ webex.request = jest.fn().mockResolvedValue({body: {}});
1767
+ webex.cc['agentConfig'] = {
1768
+ agentId: 'agent-123',
1769
+ deviceType: LoginOption.EXTENSION,
1770
+ } as Profile;
1771
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1772
+
1773
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
1774
+
1775
+ const publishSpy = jest
1776
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1777
+ .mockResolvedValue(undefined);
1778
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1779
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1780
+
1781
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1782
+ trackingId: 'track-1',
1783
+ } as StationLogoutResponse);
1784
+
1785
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1786
+
1787
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1788
+ expect(teardownSpy).toHaveBeenCalled();
1789
+ expect(unsubscribeSpy).toHaveBeenCalled();
1790
+ });
1791
+
1792
+ it('should teardown wxApp state on logout even when usersub publish fails', async () => {
1793
+ webex.internal.device = {userId: 'user-123'};
1794
+ webex.cc['agentConfig'] = {
1795
+ agentId: 'agent-123',
1796
+ deviceType: LoginOption.EXTENSION,
1797
+ } as Profile;
1798
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1799
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1800
+ mockTaskManager.applyEnableWxBetterTogether(true);
1801
+
1802
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1803
+ trackingId: 'track-1',
1804
+ } as StationLogoutResponse);
1805
+ jest
1806
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
1807
+ .mockRejectedValue(new Error('usersub publish failed'));
1808
+ const scheduleRetrySpy = jest.spyOn(
1809
+ webex.cc as any,
1810
+ 'scheduleCompensatingFalsePublishRetry'
1811
+ );
1812
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1813
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1814
+
1815
+ const result = await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1816
+
1817
+ expect(result).toEqual({trackingId: 'track-1'});
1818
+ expect(teardownSpy).toHaveBeenCalled();
1819
+ expect(unsubscribeSpy).toHaveBeenCalled();
1820
+ expect(scheduleRetrySpy).toHaveBeenCalled();
1821
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
1822
+ });
1823
+
1824
+ it('should retry compensating false publish when teardown usersub publish fails on logout', async () => {
1825
+ jest.useFakeTimers();
1826
+ try {
1827
+ webex.internal.device = {
1828
+ userId: 'user-123',
1829
+ url: 'https://wdm.example.com/devices/dev-1',
1830
+ };
1831
+ webex.cc['agentConfig'] = {
1832
+ agentId: 'agent-123',
1833
+ deviceType: LoginOption.EXTENSION,
1834
+ } as Profile;
1835
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1836
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1837
+ mockTaskManager.applyEnableWxBetterTogether(true);
1838
+ webex.cc['webexCrossClientService'].answerCallsState = true;
1839
+
1840
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1841
+ trackingId: 'track-1',
1842
+ } as StationLogoutResponse);
1843
+
1844
+ const publishSpy = jest
1845
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1846
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
1847
+ .mockResolvedValue(undefined);
1848
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1849
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
1850
+ jest
1851
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1852
+ .mockResolvedValue(undefined);
1853
+
1854
+ const result = await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1855
+
1856
+ expect(result).toEqual({trackingId: 'track-1'});
1857
+ expect(publishSpy).toHaveBeenCalledTimes(1);
1858
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1859
+ expect(teardownSpy).toHaveBeenCalled();
1860
+ expect(unsubscribeSpy).toHaveBeenCalled();
1861
+
1862
+ jest.advanceTimersByTime(30_000);
1863
+ await publishSpy.mock.results[1]?.value;
1864
+
1865
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1866
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1867
+ } finally {
1868
+ jest.useRealTimers();
1869
+ }
1870
+ });
1871
+
1872
+ it('should cancel compensating retry when station login succeeds before retry fires', async () => {
1873
+ jest.useFakeTimers();
1874
+ try {
1875
+ webex.internal.device = {
1876
+ userId: 'user-123',
1877
+ url: 'https://wdm.example.com/devices/dev-1',
1878
+ registered: false,
1879
+ register: jest.fn().mockResolvedValue(undefined),
1880
+ unregister: jest.fn().mockResolvedValue(undefined),
1881
+ };
1882
+ webex.internal.mercury = {
1883
+ connected: false,
1884
+ connect: jest.fn().mockResolvedValue(undefined),
1885
+ disconnect: jest.fn().mockResolvedValue(undefined),
1886
+ };
1887
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1888
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1889
+ webex.cc['agentConfig'] = {
1890
+ agentId: 'agent-123',
1891
+ deviceType: LoginOption.EXTENSION,
1892
+ webRtcEnabled: false,
1893
+ loginVoiceOptions: ['EXTENSION'],
1894
+ } as Profile;
1895
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1896
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1897
+ mockTaskManager.applyEnableWxBetterTogether(true);
1898
+ webex.cc['webexCrossClientService'].answerCallsState = true;
1899
+
1900
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
1901
+ trackingId: 'track-1',
1902
+ } as StationLogoutResponse);
1903
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1904
+ data: {
1905
+ agentId: 'agent-123',
1906
+ teamId: 'team-123',
1907
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1908
+ },
1909
+ trackingId: 'track-2',
1910
+ } as StationLoginSuccess);
1911
+
1912
+ const publishSpy = jest
1913
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1914
+ .mockRejectedValueOnce(new Error('usersub publish failed'))
1915
+ .mockResolvedValue(undefined);
1916
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
1917
+ jest
1918
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1919
+ .mockResolvedValue(undefined);
1920
+
1921
+ await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1922
+ await webex.cc.stationLogin({
1923
+ teamId: 'team-123',
1924
+ loginOption: LoginOption.EXTENSION,
1925
+ dialNumber: '1001',
1926
+ });
1927
+
1928
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1929
+ expect(publishSpy).toHaveBeenNthCalledWith(1, false, {userId: 'user-123', trackPublishMetrics: true});
1930
+ expect(publishSpy).toHaveBeenNthCalledWith(2, true, {userId: 'user-123', trackPublishMetrics: true});
1931
+ expect(teardownSpy).toHaveBeenCalledTimes(1);
1932
+
1933
+ jest.advanceTimersByTime(30_000);
1934
+
1935
+ expect(publishSpy).toHaveBeenCalledTimes(2);
1936
+ expect(teardownSpy).toHaveBeenCalledTimes(1);
1937
+ } finally {
1938
+ jest.useRealTimers();
1939
+ }
1940
+ });
1941
+
1942
+ it('should handle error during stationLogout', async () => {
1943
+ const data = {logoutReason: 'Logout reason'};
1944
+ const error = {
1945
+ details: {
1946
+ trackingId: '1234',
1947
+ data: {
1948
+ reason: 'Error while performing station logout',
1949
+ },
1950
+ },
1951
+ };
1952
+
1953
+ jest.spyOn(webex.cc.services.agent, 'logout').mockRejectedValue(error);
1954
+
1955
+ await expect(webex.cc.stationLogout(data)).rejects.toThrow(error.details.data.reason);
1956
+
1957
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Starting agent station logout', {
1958
+ module: CC_FILE,
1959
+ method: 'stationLogout',
1960
+ });
1961
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
1962
+ `stationLogout failed with reason: ${error.details.data.reason}`,
1963
+ {module: CC_FILE, method: 'stationLogout', trackingId: error.details.trackingId}
1964
+ );
1965
+ });
1966
+ });
1967
+
1968
+ describe('setAgentStatus', () => {
1969
+ it('should set agent status successfully when status is Available', async () => {
1970
+ const expectedPayload = {
1971
+ state: 'Available',
1972
+ auxCodeId: '0',
1973
+ agentId: '123',
1974
+ lastStateChangeReason: 'Agent is available',
1975
+ };
1976
+
1977
+ const setAgentStatusMock = jest
1978
+ .spyOn(webex.cc.services.agent, 'stateChange')
1979
+ .mockResolvedValue({data: expectedPayload});
1980
+
1981
+ const result = await webex.cc.setAgentState(expectedPayload);
1982
+
1983
+ // Verify logging calls
1984
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Setting agent state', {
1985
+ module: CC_FILE,
1986
+ method: 'setAgentState',
1987
+ });
1988
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
1989
+ `Agent state changed successfully to auxCodeId: ${expectedPayload.auxCodeId}`,
1990
+ {
1991
+ module: CC_FILE,
1992
+ method: 'setAgentState',
1993
+ }
1994
+ );
1995
+
1996
+ expect(setAgentStatusMock).toHaveBeenCalledWith({data: expectedPayload});
1997
+ expect(result).toEqual({data: expectedPayload});
1998
+ expect(mockMetricsManager.timeEvent).toHaveBeenCalledWith([
1999
+ METRIC_EVENT_NAMES.AGENT_STATE_CHANGE_SUCCESS,
2000
+ METRIC_EVENT_NAMES.AGENT_STATE_CHANGE_FAILED,
2001
+ ]);
2002
+ });
2003
+
2004
+ it('should set agent status successfully when status is Meeting', async () => {
2005
+ const expectedPayload = {
2006
+ state: 'Meeting',
2007
+ auxCodeId: '12345',
2008
+ agentId: '123',
2009
+ lastStateChangeReason: 'Agent is in meeting',
990
2010
  };
991
2011
 
992
2012
  const setAgentStatusMock = jest
@@ -1071,6 +2091,227 @@ describe('webex.cc', () => {
1071
2091
  });
1072
2092
  });
1073
2093
 
2094
+ describe('Agent Wellness Break SDK contracts', () => {
2095
+ const wellbeingCode = {
2096
+ id: 'wellbeing-code',
2097
+ name: 'WellbeingBreak',
2098
+ isSystem: true,
2099
+ isDefault: false,
2100
+ };
2101
+
2102
+ beforeEach(() => {
2103
+ webex.cc.agentConfig = {
2104
+ agentId: 'agent-1',
2105
+ isWellnessBreakEnabled: true,
2106
+ } as Profile;
2107
+ webex.cc['updateWellnessSession']('session-1');
2108
+ });
2109
+
2110
+ it('fetches the system idle code once per registration', async () => {
2111
+ const lookup = jest
2112
+ .spyOn(webex.cc.services.config, 'getWellbeingBreakIdleCode')
2113
+ .mockResolvedValue(wellbeingCode);
2114
+
2115
+ await expect(webex.cc.getWellbeingBreakIdleCode()).resolves.toEqual(wellbeingCode);
2116
+ await expect(webex.cc.getWellbeingBreakIdleCode()).resolves.toEqual(wellbeingCode);
2117
+
2118
+ expect(lookup).toHaveBeenCalledTimes(1);
2119
+ expect(lookup).toHaveBeenCalledWith('mockOrgId');
2120
+ });
2121
+
2122
+ it('does not cache an idle-code lookup that completes after a new registration starts', async () => {
2123
+ let resolveLookup: (idleCode: typeof wellbeingCode) => void = () => undefined;
2124
+ jest
2125
+ .spyOn(webex.cc.services.config, 'getWellbeingBreakIdleCode')
2126
+ .mockImplementation(
2127
+ () =>
2128
+ new Promise((resolve) => {
2129
+ resolveLookup = resolve;
2130
+ })
2131
+ );
2132
+
2133
+ const lookupPromise = webex.cc.getWellbeingBreakIdleCode();
2134
+ const staleLookup = expect(lookupPromise).rejects.toThrow(
2135
+ 'WELLNESS_BREAK_REGISTRATION_CHANGED'
2136
+ );
2137
+ jest
2138
+ .spyOn(webex.cc, 'connectWebsocket')
2139
+ .mockRejectedValue(new Error('replacement registration failed'));
2140
+
2141
+ await expect(webex.cc.register()).rejects.toThrow('replacement registration failed');
2142
+ resolveLookup(wellbeingCode);
2143
+
2144
+ await staleLookup;
2145
+ expect(webex.cc['wellbeingBreakIdleCode']).toBeUndefined();
2146
+ });
2147
+
2148
+ it('rejects the system idle-code lookup when wellness is disabled', async () => {
2149
+ webex.cc.agentConfig.isWellnessBreakEnabled = false;
2150
+
2151
+ await expect(webex.cc.getWellbeingBreakIdleCode()).rejects.toThrow(
2152
+ 'WELLNESS_BREAK_NOT_ENABLED'
2153
+ );
2154
+ expect(webex.cc.services.config.getWellbeingBreakIdleCode).not.toHaveBeenCalled();
2155
+ });
2156
+
2157
+ it('invalidates the wellness session only for a current-session multi-login close', () => {
2158
+ webex.cc['handleWebsocketMessage'](
2159
+ JSON.stringify({
2160
+ type: CC_EVENTS.AGENT_MULTI_LOGIN,
2161
+ data: {
2162
+ type: 'AgentMultiLoginCloseSession',
2163
+ agentSessionId: 'another-session',
2164
+ },
2165
+ })
2166
+ );
2167
+ expect(webex.cc['currentAgentSessionId']).toBe('session-1');
2168
+
2169
+ webex.cc['handleWebsocketMessage'](
2170
+ JSON.stringify({
2171
+ type: CC_EVENTS.AGENT_MULTI_LOGIN,
2172
+ data: {
2173
+ type: 'AgentMultiLoginCloseSession',
2174
+ agentSessionId: 'session-1',
2175
+ },
2176
+ })
2177
+ );
2178
+ expect(webex.cc['currentAgentSessionId']).toBeUndefined();
2179
+ });
2180
+
2181
+ it('emits primary wellness notifications regardless of notification session metadata', () => {
2182
+ const emitSpy = jest.spyOn(webex.cc, 'emit');
2183
+ const payload = {
2184
+ type: 'Wellness_Break_Handler',
2185
+ orgId: 'mockOrgId',
2186
+ trackingId: 'data-notification-tracking',
2187
+ data: {
2188
+ agentId: 'agent-1',
2189
+ orgId: 'mockOrgId',
2190
+ notifType: 'Wellness_Break_Handler',
2191
+ notifDetails: {
2192
+ actionEvent: 'SUGGEST_WELLNESS_BREAK',
2193
+ actionText: 'How about a break?',
2194
+ },
2195
+ data: {
2196
+ orgId: 'mockOrgId',
2197
+ agentSessionId: 'notification-session',
2198
+ interactionId: 'interaction-1',
2199
+ },
2200
+ },
2201
+ };
2202
+
2203
+ webex.cc['handleWebsocketMessage'](JSON.stringify(payload));
2204
+
2205
+ expect(emitSpy).toHaveBeenCalledWith(CC_EVENTS.WELLNESS_BREAK, {
2206
+ agentId: 'agent-1',
2207
+ orgId: 'mockOrgId',
2208
+ agentSessionId: 'notification-session',
2209
+ actionEvent: 'SUGGEST_WELLNESS_BREAK',
2210
+ actionText: 'How about a break?',
2211
+ interactionId: 'interaction-1',
2212
+ trackingId: 'data-notification-tracking',
2213
+ });
2214
+ });
2215
+
2216
+ it.each([
2217
+ ['SUGGEST_WELLNESS_BREAK', 'interactionId'],
2218
+ ['WELLNESS_BREAK_NOT_ALLOWED', 'InteractionId'],
2219
+ ])('normalizes the %s action and either interaction-id spelling', (actionEvent, key) => {
2220
+ const emitSpy = jest.spyOn(webex.cc, 'emit');
2221
+ webex.cc['handleWebsocketMessage'](
2222
+ JSON.stringify({
2223
+ type: 'Wellness_Break_Handler',
2224
+ orgId: 'mockOrgId',
2225
+ data: {
2226
+ agentId: 'agent-1',
2227
+ orgId: 'mockOrgId',
2228
+ notifType: 'Wellness_Break_Handler',
2229
+ notifDetails: {actionEvent},
2230
+ data: {
2231
+ orgId: 'mockOrgId',
2232
+ agentSessionId: 'session-1',
2233
+ [key]: 'interaction-2',
2234
+ },
2235
+ },
2236
+ })
2237
+ );
2238
+
2239
+ expect(emitSpy).toHaveBeenCalledWith(
2240
+ CC_EVENTS.WELLNESS_BREAK,
2241
+ expect.objectContaining({actionEvent, interactionId: 'interaction-2'})
2242
+ );
2243
+ });
2244
+
2245
+ it('does not promote uncontracted notification text fields', () => {
2246
+ const emitSpy = jest.spyOn(webex.cc, 'emit');
2247
+ webex.cc['handleWebsocketMessage'](
2248
+ JSON.stringify({
2249
+ type: 'Wellness_Break_Handler',
2250
+ orgId: 'mockOrgId',
2251
+ trackingId: 'notification-tracking',
2252
+ data: {
2253
+ agentId: 'agent-1',
2254
+ orgId: 'mockOrgId',
2255
+ notifType: 'Wellness_Break_Handler',
2256
+ notifDetails: {
2257
+ actionEvent: 'SUGGEST_WELLNESS_BREAK',
2258
+ actionText2: 'Uncontracted text',
2259
+ },
2260
+ data: {
2261
+ orgId: 'mockOrgId',
2262
+ agentSessionId: 'session-1',
2263
+ InteractionId: 'interaction-qa',
2264
+ },
2265
+ },
2266
+ })
2267
+ );
2268
+
2269
+ expect(emitSpy).toHaveBeenCalledWith(CC_EVENTS.WELLNESS_BREAK, {
2270
+ agentId: 'agent-1',
2271
+ orgId: 'mockOrgId',
2272
+ agentSessionId: 'session-1',
2273
+ actionEvent: 'SUGGEST_WELLNESS_BREAK',
2274
+ interactionId: 'interaction-qa',
2275
+ trackingId: 'notification-tracking',
2276
+ });
2277
+ });
2278
+
2279
+ it('ignores wellness events when the effective flag is disabled or identity is missing', () => {
2280
+ const emitSpy = jest.spyOn(webex.cc, 'emit');
2281
+ const payload = {
2282
+ type: 'Wellness_Break_Handler',
2283
+ orgId: 'mockOrgId',
2284
+ data: {
2285
+ agentId: 'agent-1',
2286
+ orgId: 'mockOrgId',
2287
+ notifType: 'Wellness_Break_Handler',
2288
+ notifDetails: {actionEvent: 'SUGGEST_WELLNESS_BREAK'},
2289
+ data: {orgId: 'mockOrgId', agentSessionId: 'session-1'},
2290
+ },
2291
+ };
2292
+
2293
+ webex.cc.agentConfig.isWellnessBreakEnabled = false;
2294
+ webex.cc['handleWebsocketMessage'](JSON.stringify(payload));
2295
+ webex.cc.agentConfig.isWellnessBreakEnabled = true;
2296
+ const {orgId: omittedOrgId, ...payloadWithoutEnvelopeOrg} = payload;
2297
+ expect(omittedOrgId).toBe('mockOrgId');
2298
+ webex.cc['handleWebsocketMessage'](JSON.stringify(payloadWithoutEnvelopeOrg));
2299
+
2300
+ expect(emitSpy).not.toHaveBeenCalledWith(CC_EVENTS.WELLNESS_BREAK, expect.anything());
2301
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
2302
+ METRIC_EVENT_NAMES.AI_ASSISTANT_WELLNESS_EVENT_INVALID,
2303
+ {reason: 'wellness_disabled'},
2304
+ ['operational']
2305
+ );
2306
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
2307
+ METRIC_EVENT_NAMES.AI_ASSISTANT_WELLNESS_EVENT_INVALID,
2308
+ {reason: 'missing_wellness_identity'},
2309
+ ['operational']
2310
+ );
2311
+ });
2312
+
2313
+ });
2314
+
1074
2315
  describe('getBuddyAgents', () => {
1075
2316
  it('should return buddy agents response when successful', async () => {
1076
2317
  const data: BuddyAgents = {state: 'Available', mediaType: 'telephony'};
@@ -1134,6 +2375,41 @@ describe('webex.cc', () => {
1134
2375
  ]);
1135
2376
  });
1136
2377
 
2378
+ it('should map Transfer action to an Available backend state filter', async () => {
2379
+ webex.cc.agentConfig = {agentProfileID: 'test-agent-profile-id'};
2380
+ const buddyAgentsResponse = {data: {agentList: []}};
2381
+ const buddyAgentsSpy = jest
2382
+ .spyOn(webex.cc.services.agent, 'buddyAgents')
2383
+ .mockResolvedValue(buddyAgentsResponse);
2384
+
2385
+ await webex.cc.getBuddyAgents({action: 'Transfer', mediaType: 'chat'});
2386
+
2387
+ expect(buddyAgentsSpy).toHaveBeenCalledWith({
2388
+ data: {
2389
+ agentProfileId: 'test-agent-profile-id',
2390
+ mediaType: 'chat',
2391
+ state: 'Available',
2392
+ },
2393
+ });
2394
+ });
2395
+
2396
+ it('should omit the backend state filter for Consult and default media type to telephony', async () => {
2397
+ webex.cc.agentConfig = {agentProfileID: 'test-agent-profile-id'};
2398
+ const buddyAgentsResponse = {data: {agentList: []}};
2399
+ const buddyAgentsSpy = jest
2400
+ .spyOn(webex.cc.services.agent, 'buddyAgents')
2401
+ .mockResolvedValue(buddyAgentsResponse);
2402
+
2403
+ await webex.cc.getBuddyAgents({action: 'Consult'});
2404
+
2405
+ expect(buddyAgentsSpy).toHaveBeenCalledWith({
2406
+ data: {
2407
+ agentProfileId: 'test-agent-profile-id',
2408
+ mediaType: 'telephony',
2409
+ },
2410
+ });
2411
+ });
2412
+
1137
2413
  it('should handle error', async () => {
1138
2414
  const data: BuddyAgents = {state: 'Available', mediaType: 'telephony'};
1139
2415
  webex.cc.agentConfig = {
@@ -1210,11 +2486,11 @@ describe('webex.cc', () => {
1210
2486
  const webSocketManagerOnSpy = jest.spyOn(webex.cc.services.webSocketManager, 'on');
1211
2487
  await webex.cc['silentRelogin']();
1212
2488
 
1213
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2489
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1214
2490
  module: CC_FILE,
1215
2491
  method: 'silentRelogin',
1216
2492
  });
1217
- expect(LoggerProxy.info).toHaveBeenCalledWith(
2493
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
1218
2494
  'event=requestAutoStateChange | Requesting state change to available on socket reconnect',
1219
2495
  {module: CC_FILE, method: 'silentRelogin'}
1220
2496
  );
@@ -1259,7 +2535,7 @@ describe('webex.cc', () => {
1259
2535
 
1260
2536
  jest.spyOn(webex.cc.services.agent, 'reload').mockRejectedValue(error);
1261
2537
  await webex.cc['silentRelogin']();
1262
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2538
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1263
2539
  module: CC_FILE,
1264
2540
  method: 'silentRelogin',
1265
2541
  });
@@ -1274,7 +2550,7 @@ describe('webex.cc', () => {
1274
2550
  jest.spyOn(webex.cc.services.agent, 'reload').mockRejectedValue(error);
1275
2551
 
1276
2552
  await expect(webex.cc['silentRelogin']()).rejects.toThrow(error);
1277
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2553
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1278
2554
  module: CC_FILE,
1279
2555
  method: 'silentRelogin',
1280
2556
  });
@@ -1316,7 +2592,7 @@ describe('webex.cc', () => {
1316
2592
 
1317
2593
  await webex.cc['silentRelogin']();
1318
2594
 
1319
- expect(LoggerProxy.info).toHaveBeenCalledWith('Starting silent relogin process', {
2595
+ expect(LoggerProxy.log).toHaveBeenCalledWith('Starting silent relogin process', {
1320
2596
  module: CC_FILE,
1321
2597
  method: 'silentRelogin',
1322
2598
  });
@@ -1361,6 +2637,82 @@ describe('webex.cc', () => {
1361
2637
  expect(webex.cc.agentConfig.deviceType).toBe(LoginOption.AGENT_DN);
1362
2638
  expect(webex.cc.agentConfig.dn).toBe('67890');
1363
2639
  });
2640
+
2641
+ it('should publish usersub true on silent relogin when enableWxBetterTogether is true at init', async () => {
2642
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
2643
+ webex.internal.device = {
2644
+ userId: 'user-123',
2645
+ url: 'https://wdm.example.com/devices/dev-1',
2646
+ registered: true,
2647
+ register: jest.fn().mockResolvedValue(undefined),
2648
+ };
2649
+ webex.internal.mercury.connected = true;
2650
+ webex.internal.mercury.connect = jest.fn().mockResolvedValue(undefined);
2651
+ webex.cc.agentConfig = {
2652
+ agentId: 'agentId',
2653
+ agentProfileID: 'test-agent-profile-id',
2654
+ isAgentLoggedIn: false,
2655
+ } as Profile;
2656
+
2657
+ jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
2658
+ data: {
2659
+ auxCodeId: 'auxCodeId',
2660
+ agentId: 'agentId',
2661
+ deviceType: LoginOption.EXTENSION,
2662
+ dn: '1001',
2663
+ teamId: 'teamId',
2664
+ lastStateChangeTimestamp: 1738575135188,
2665
+ lastIdleCodeChangeTimestamp: 1738575135189,
2666
+ },
2667
+ });
2668
+
2669
+ const publishSpy = jest
2670
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2671
+ .mockResolvedValue(undefined);
2672
+ const subscribeSpy = jest
2673
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
2674
+ .mockImplementation(() => {});
2675
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
2676
+
2677
+ await webex.cc['silentRelogin']();
2678
+
2679
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
2680
+ expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
2681
+ });
2682
+
2683
+ it('should force-publish usersub false on silent relogin when enableWxBetterTogether is false at init', async () => {
2684
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
2685
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
2686
+ webex.cc.agentConfig = {
2687
+ agentId: 'agentId',
2688
+ agentProfileID: 'test-agent-profile-id',
2689
+ isAgentLoggedIn: false,
2690
+ } as Profile;
2691
+
2692
+ jest.spyOn(webex.cc.services.agent, 'reload').mockResolvedValue({
2693
+ data: {
2694
+ auxCodeId: 'auxCodeId',
2695
+ agentId: 'agentId',
2696
+ deviceType: LoginOption.EXTENSION,
2697
+ dn: '1001',
2698
+ teamId: 'teamId',
2699
+ lastStateChangeTimestamp: 1738575135188,
2700
+ lastIdleCodeChangeTimestamp: 1738575135189,
2701
+ },
2702
+ });
2703
+
2704
+ const publishSpy = jest
2705
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2706
+ .mockResolvedValue(undefined);
2707
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
2708
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
2709
+
2710
+ await webex.cc['silentRelogin']();
2711
+
2712
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
2713
+ expect(teardownSpy).toHaveBeenCalled();
2714
+ expect(unsubscribeSpy).toHaveBeenCalled();
2715
+ });
1364
2716
  });
1365
2717
 
1366
2718
  describe('setupEventListeners()', () => {
@@ -1500,6 +2852,27 @@ describe('webex.cc', () => {
1500
2852
  });
1501
2853
  });
1502
2854
 
2855
+ describe('getQueues', () => {
2856
+ it('delegates to the queue service when successful', async () => {
2857
+ const mockQueuesResponse = [{queueId: 'queue1', queueName: 'Queue 1'}];
2858
+ const queueSpy = jest
2859
+ .spyOn(webex.cc.queue, 'getQueues')
2860
+ .mockResolvedValue(mockQueuesResponse as any);
2861
+
2862
+ const result = await webex.cc.getQueues({page: 1});
2863
+
2864
+ expect(queueSpy).toHaveBeenCalledWith({page: 1});
2865
+ expect(result).toBe(mockQueuesResponse);
2866
+ });
2867
+
2868
+ it('propagates queue service errors', async () => {
2869
+ const error = new Error('Test error.');
2870
+ jest.spyOn(webex.cc.queue, 'getQueues').mockRejectedValue(error);
2871
+
2872
+ await expect(webex.cc.getQueues()).rejects.toThrow('Test error.');
2873
+ });
2874
+ });
2875
+
1503
2876
  describe('uploadLogs', () => {
1504
2877
  it('should upload logs successfully', async () => {
1505
2878
  const uploadLogsMock = jest.spyOn(webex.cc.webexRequest, 'uploadLogs').mockResolvedValue({
@@ -1529,6 +2902,7 @@ describe('webex.cc', () => {
1529
2902
 
1530
2903
  describe('unregister', () => {
1531
2904
  let mockWebSocketManager;
2905
+ let mockRTDWebSocketManager;
1532
2906
  let mercuryDisconnectSpy;
1533
2907
  let deviceUnregisterSpy;
1534
2908
 
@@ -1545,8 +2919,15 @@ describe('webex.cc', () => {
1545
2919
  off: jest.fn(),
1546
2920
  on: jest.fn(),
1547
2921
  };
2922
+ mockRTDWebSocketManager = {
2923
+ isSocketClosed: false,
2924
+ close: jest.fn(),
2925
+ off: jest.fn(),
2926
+ on: jest.fn(),
2927
+ };
1548
2928
 
1549
2929
  webex.cc.services.webSocketManager = mockWebSocketManager;
2930
+ webex.cc.services.rtdWebSocketManager = mockRTDWebSocketManager;
1550
2931
 
1551
2932
  webex.internal = webex.internal || {};
1552
2933
  webex.internal.mercury = {
@@ -1562,6 +2943,32 @@ describe('webex.cc', () => {
1562
2943
  deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
1563
2944
  });
1564
2945
 
2946
+ it('invalidates wellness context before deregistration cleanup can fail', async () => {
2947
+ const cleanupError = new Error('cleanup failed');
2948
+ let rejectCleanup: (error: Error) => void = () => undefined;
2949
+ const cleanup = new Promise((resolve, reject) => {
2950
+ rejectCleanup = reject;
2951
+ });
2952
+ webex.cc.agentConfig = {
2953
+ ...webex.cc.agentConfig,
2954
+ isWellnessBreakEnabled: true,
2955
+ };
2956
+ webex.cc['updateWellnessSession']('session-1');
2957
+ jest.spyOn(webex.cc as any, 'teardownWxAppLocalState').mockReturnValue(cleanup);
2958
+
2959
+ const deregistration = webex.cc.deregister();
2960
+ const rejection = expect(deregistration).rejects.toThrow(cleanupError);
2961
+
2962
+ expect(webex.cc['currentAgentSessionId']).toBeUndefined();
2963
+ await expect(webex.cc.apiAIAssistant.requestWellnessBreak()).rejects.toThrow(
2964
+ 'WELLNESS_BREAK_AGENT_SESSION_REQUIRED'
2965
+ );
2966
+
2967
+ rejectCleanup(cleanupError);
2968
+ await rejection;
2969
+ expect(webex.cc['currentAgentSessionId']).toBeUndefined();
2970
+ });
2971
+
1565
2972
  it('should unregister successfully and clean up all resources when webrtc is enabled', async () => {
1566
2973
  webex.cc.services.rtdWebSocketManager = {
1567
2974
  isSocketClosed: false,
@@ -1570,6 +2977,18 @@ describe('webex.cc', () => {
1570
2977
  on: jest.fn(),
1571
2978
  } as any;
1572
2979
 
2980
+ webex.internal.device = {
2981
+ ...webex.internal.device,
2982
+ userId: 'user-123',
2983
+ unregister: jest.fn().mockResolvedValue(undefined),
2984
+ };
2985
+ deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
2986
+ const publishSpy = jest
2987
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
2988
+ .mockResolvedValue(undefined);
2989
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
2990
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
2991
+
1573
2992
  await webex.cc.deregister();
1574
2993
 
1575
2994
  expect(mockTaskManager.off).toHaveBeenCalledWith(
@@ -1580,6 +2999,10 @@ describe('webex.cc', () => {
1580
2999
  TASK_EVENTS.TASK_HYDRATE,
1581
3000
  expect.any(Function)
1582
3001
  );
3002
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
3003
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
3004
+ expect.any(Function)
3005
+ );
1583
3006
  expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1584
3007
  expect(webex.cc.services.rtdWebSocketManager.off).toHaveBeenCalledWith(
1585
3008
  'message',
@@ -1636,17 +3059,57 @@ describe('webex.cc', () => {
1636
3059
  const [, hydrateCallback] = hydrateCalls[0];
1637
3060
  expect(hydrateCallback).toBe(webex.cc['handleTaskHydrate']);
1638
3061
 
3062
+ const multiLoginHydrateCalls = mockTaskManager.off.mock.calls.filter(
3063
+ ([evt]) => evt === TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE
3064
+ );
3065
+ expect(multiLoginHydrateCalls).toHaveLength(1);
3066
+ const [, multiLoginHydrateCallback] = multiLoginHydrateCalls[0];
3067
+ expect(multiLoginHydrateCallback).toBe(webex.cc['handleTaskMultiLoginHydrate']);
3068
+
1639
3069
  const messageCalls = mockWebSocketManager.off.mock.calls.filter(([evt]) => evt === 'message');
1640
3070
  expect(messageCalls).toHaveLength(1);
1641
3071
  const [, messageCallback] = messageCalls[0];
1642
3072
  expect(messageCallback).toBe(webex.cc['handleWebsocketMessage']);
1643
3073
 
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']);
3074
+ const connectionCalls = webex.cc.services.connectionService.off.mock.calls.filter(
3075
+ ([evt]) => evt === 'connectionLost'
3076
+ );
3077
+ expect(connectionCalls).toHaveLength(1);
3078
+ const [, connectionCallback] = connectionCalls[0];
3079
+ expect(connectionCallback).toBe(webex.cc['handleConnectionLost']);
3080
+
3081
+ expect(publishSpy).not.toHaveBeenCalled();
3082
+ expect(teardownSpy).toHaveBeenCalled();
3083
+ expect(unsubscribeSpy).toHaveBeenCalled();
3084
+ });
3085
+
3086
+ it('should publish usersub false before device unregister when wxApp is active on deregister', async () => {
3087
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3088
+ webex.cc['webexCrossClientService'].answerCallsState = true;
3089
+ webex.internal.device = {
3090
+ userId: 'user-123',
3091
+ url: 'https://wdm.example.com/devices/dev-1',
3092
+ unregister: jest.fn().mockResolvedValue(undefined),
3093
+ };
3094
+ deviceUnregisterSpy = jest.spyOn(webex.internal.device, 'unregister');
3095
+
3096
+ const callOrder: string[] = [];
3097
+ jest
3098
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3099
+ .mockImplementation(async (enable: boolean) => {
3100
+ if (enable === false && !webex.internal.device.url) {
3101
+ throw new Error('Device URL is unavailable for cross-client publish');
3102
+ }
3103
+ callOrder.push(enable ? 'publish:true' : 'publish:false');
3104
+ });
3105
+ deviceUnregisterSpy.mockImplementation(async () => {
3106
+ callOrder.push('device.unregister');
3107
+ webex.internal.device.url = undefined;
3108
+ });
3109
+
3110
+ await webex.cc.deregister();
3111
+
3112
+ expect(callOrder).toEqual(['publish:false', 'device.unregister']);
1650
3113
  });
1651
3114
 
1652
3115
  it('should skip webCallingService and internal cleanup when webrtc is disabled', async () => {
@@ -1661,6 +3124,10 @@ describe('webex.cc', () => {
1661
3124
  TASK_EVENTS.TASK_HYDRATE,
1662
3125
  expect.any(Function)
1663
3126
  );
3127
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
3128
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
3129
+ expect.any(Function)
3130
+ );
1664
3131
  expect(mockWebSocketManager.off).toHaveBeenCalledWith('message', expect.any(Function));
1665
3132
  expect(webex.cc.services.connectionService.off).toHaveBeenCalledWith(
1666
3133
  'connectionLost',
@@ -1670,6 +3137,7 @@ describe('webex.cc', () => {
1670
3137
  expect(webex.internal.mercury.off).not.toHaveBeenCalled();
1671
3138
  expect(mercuryDisconnectSpy).not.toHaveBeenCalled();
1672
3139
  expect(deviceUnregisterSpy).not.toHaveBeenCalled();
3140
+ expect(mockRTDWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
1673
3141
  });
1674
3142
 
1675
3143
  it('should skip internal mercury cleanup when loginVoiceOptions does not include BROWSER', async () => {
@@ -1686,10 +3154,140 @@ describe('webex.cc', () => {
1686
3154
  expect(mercuryDisconnectSpy).not.toHaveBeenCalled();
1687
3155
  expect(deviceUnregisterSpy).not.toHaveBeenCalled();
1688
3156
 
3157
+ expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
3158
+ expect(mockRTDWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
3159
+ expect(webex.cc.agentConfig).toBeNull();
3160
+ });
3161
+
3162
+ it('should teardown wxApp state on deregister even when usersub publish fails', async () => {
3163
+ webex.cc.agentConfig = {
3164
+ agentId: 'agentId',
3165
+ webRtcEnabled: false,
3166
+ loginVoiceOptions: [LoginOption.EXTENSION],
3167
+ };
3168
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3169
+ webex.cc['webexCrossClientService'].answerCallsState = true;
3170
+ webex.internal.device = {
3171
+ userId: 'user-123',
3172
+ unregister: jest.fn().mockResolvedValue(undefined),
3173
+ };
3174
+
3175
+ jest
3176
+ .spyOn(webex.cc as never, 'publishAnswerOnWebexCrossClientState' as never)
3177
+ .mockRejectedValue(new Error('usersub publish failed'));
3178
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
3179
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
3180
+
3181
+ await expect(webex.cc.deregister()).rejects.toThrow('usersub publish failed');
3182
+
3183
+ expect(teardownSpy).toHaveBeenCalled();
3184
+ expect(unsubscribeSpy).toHaveBeenCalled();
3185
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1689
3186
  expect(mockWebSocketManager.close).toHaveBeenCalledWith(false, 'Unregistering the SDK');
1690
3187
  expect(webex.cc.agentConfig).toBeNull();
1691
3188
  });
1692
3189
 
3190
+ it('should publish forced false during teardown when enable publish is still in flight', async () => {
3191
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3192
+ webex.internal.device = {
3193
+ userId: 'user-123',
3194
+ url: 'https://wdm.example.com/devices/dev-1',
3195
+ };
3196
+
3197
+ let resolveEnable: () => void = () => {};
3198
+ const enableGate = new Promise<void>((resolve) => {
3199
+ resolveEnable = resolve;
3200
+ });
3201
+
3202
+ const publishSpy = jest
3203
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3204
+ .mockImplementation(async (enable: boolean) => {
3205
+ if (enable) {
3206
+ await enableGate;
3207
+ }
3208
+ });
3209
+ jest
3210
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
3211
+ .mockResolvedValue(undefined);
3212
+
3213
+ expect(webex.cc['webexCrossClientService'].answerCallsState).toBe(false);
3214
+
3215
+ const enableInFlight = (webex.cc as any).publishAnswerOnWebexCrossClientState(true);
3216
+
3217
+ await (webex.cc as any).teardownWxAppLocalState();
3218
+
3219
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
3220
+
3221
+ resolveEnable();
3222
+ await enableInFlight;
3223
+ });
3224
+
3225
+ it('should release wxApp-owned Mercury and device on deregister', async () => {
3226
+ webex.cc.agentConfig = {
3227
+ agentId: 'agentId',
3228
+ webRtcEnabled: false,
3229
+ loginVoiceOptions: [LoginOption.EXTENSION],
3230
+ };
3231
+ webex.cc['wxAppMercuryConnectedByCc'] = true;
3232
+ webex.cc['wxAppDeviceRegisteredByCc'] = true;
3233
+ webex.internal.mercury.connected = true;
3234
+ webex.internal.device.registered = true;
3235
+
3236
+ const mercuryDisconnectSpy = jest
3237
+ .spyOn(webex.internal.mercury, 'disconnect')
3238
+ .mockResolvedValue(undefined);
3239
+ const deviceUnregisterSpy = jest
3240
+ .spyOn(webex.internal.device, 'unregister')
3241
+ .mockResolvedValue(undefined);
3242
+
3243
+ await webex.cc.deregister();
3244
+
3245
+ expect(mercuryDisconnectSpy).toHaveBeenCalled();
3246
+ expect(deviceUnregisterSpy).toHaveBeenCalled();
3247
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(false);
3248
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(false);
3249
+ });
3250
+
3251
+ it('should retain wxApp Mercury ownership when disconnect fails and retry cleanup', async () => {
3252
+ webex.cc['wxAppMercuryConnectedByCc'] = true;
3253
+ webex.internal.mercury.connected = true;
3254
+
3255
+ const mercuryDisconnectSpy = jest
3256
+ .spyOn(webex.internal.mercury, 'disconnect')
3257
+ .mockRejectedValueOnce(new Error('disconnect failed'))
3258
+ .mockResolvedValueOnce(undefined);
3259
+
3260
+ await webex.cc['releaseWxAppMercuryResources']();
3261
+
3262
+ expect(mercuryDisconnectSpy).toHaveBeenCalledTimes(1);
3263
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(true);
3264
+
3265
+ await webex.cc['releaseWxAppMercuryResources']();
3266
+
3267
+ expect(mercuryDisconnectSpy).toHaveBeenCalledTimes(2);
3268
+ expect(webex.cc['wxAppMercuryConnectedByCc']).toBe(false);
3269
+ });
3270
+
3271
+ it('should retain wxApp device ownership when unregister fails and retry cleanup', async () => {
3272
+ webex.cc['wxAppDeviceRegisteredByCc'] = true;
3273
+ webex.internal.device.registered = true;
3274
+
3275
+ const deviceUnregisterSpy = jest
3276
+ .spyOn(webex.internal.device, 'unregister')
3277
+ .mockRejectedValueOnce(new Error('unregister failed'))
3278
+ .mockResolvedValueOnce(undefined);
3279
+
3280
+ await webex.cc['releaseWxAppMercuryResources']();
3281
+
3282
+ expect(deviceUnregisterSpy).toHaveBeenCalledTimes(1);
3283
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(true);
3284
+
3285
+ await webex.cc['releaseWxAppMercuryResources']();
3286
+
3287
+ expect(deviceUnregisterSpy).toHaveBeenCalledTimes(2);
3288
+ expect(webex.cc['wxAppDeviceRegisteredByCc']).toBe(false);
3289
+ });
3290
+
1693
3291
  it('should handle errors during unregister and track metrics', async () => {
1694
3292
  const mockError = new Error('Failed to deregister device');
1695
3293
  webex.internal.device.unregister.mockRejectedValue(mockError);
@@ -1704,6 +3302,10 @@ describe('webex.cc', () => {
1704
3302
  TASK_EVENTS.TASK_HYDRATE,
1705
3303
  expect.any(Function)
1706
3304
  );
3305
+ expect(mockTaskManager.off).toHaveBeenCalledWith(
3306
+ TASK_EVENTS.TASK_MULTI_LOGIN_HYDRATE,
3307
+ expect.any(Function)
3308
+ );
1707
3309
 
1708
3310
  expect(LoggerProxy.error).toHaveBeenCalledWith(`Error during deregister: ${mockError}`, {
1709
3311
  module: CC_FILE,
@@ -1726,7 +3328,7 @@ describe('webex.cc', () => {
1726
3328
 
1727
3329
  beforeEach(() => {
1728
3330
  emitSpy = jest.spyOn(webex.cc, 'emit');
1729
- messageCallback = mockWebSocketManager.on.mock.calls.find((c) => c[0] === 'message')[1];
3331
+ messageCallback = webex.cc['handleWebsocketMessage'];
1730
3332
  });
1731
3333
 
1732
3334
  it('should emit AGENT_STATION_LOGIN_SUCCESS on CC_EVENTS.AGENT_STATION_LOGIN_SUCCESS with mapped payload', () => {
@@ -1936,6 +3538,55 @@ describe('webex.cc', () => {
1936
3538
  expect(result).toEqual(mockResp);
1937
3539
  });
1938
3540
 
3541
+ it('should preserve enableWxBetterTogether init flag across profile update logout/login', async () => {
3542
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
3543
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
3544
+ webex.cc.agentConfig = {
3545
+ ...webex.cc.agentConfig,
3546
+ agentId: 'agentId',
3547
+ webRtcEnabled: false,
3548
+ loginVoiceOptions: ['BROWSER', 'EXTENSION'],
3549
+ } as Profile;
3550
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
3551
+ webex.cc.webCallingService.setLoginOption = jest.fn((loginOption: LoginOption) => {
3552
+ webex.cc.webCallingService.loginOption = loginOption;
3553
+ });
3554
+ webex.cc['webexCrossClientService'].answerCallsState = true;
3555
+
3556
+ const publishSpy = jest
3557
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
3558
+ .mockResolvedValue(undefined);
3559
+ jest
3560
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
3561
+ .mockImplementation(() => {});
3562
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
3563
+ jest.spyOn(webex.cc as any, 'releaseWxAppMercuryResources').mockResolvedValue(undefined);
3564
+
3565
+ jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
3566
+ trackingId: 'track-logout',
3567
+ } as StationLogoutResponse);
3568
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
3569
+ data: {
3570
+ agentId: 'agentId',
3571
+ teamId: 'teamId',
3572
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
3573
+ },
3574
+ trackingId: 'track-login',
3575
+ } as StationLoginSuccess);
3576
+
3577
+ webex.internal.mercury.connected = true;
3578
+
3579
+ await webex.cc.updateAgentProfile({
3580
+ teamId: 'teamId',
3581
+ loginOption: LoginOption.EXTENSION,
3582
+ dialNumber: '1001',
3583
+ });
3584
+
3585
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
3586
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
3587
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
3588
+ });
3589
+
1939
3590
  it('should use provided teamId if passed in payload', async () => {
1940
3591
  const dataWithTeam = {
1941
3592
  teamId: 'newTeam',
@@ -2320,4 +3971,444 @@ describe('webex.cc', () => {
2320
3971
  expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'acceptPreviewContact', CC_FILE);
2321
3972
  });
2322
3973
  });
3974
+
3975
+ describe('skipPreviewContact', () => {
3976
+ const previewPayload = {
3977
+ interactionId: 'interaction-123',
3978
+ campaignId: 'campaign-456',
3979
+ };
3980
+
3981
+ it('should skip preview contact successfully', async () => {
3982
+ const mockResponse = {trackingId: 'track-123'} as AgentContact;
3983
+
3984
+ const skipPreviewContactMock = jest
3985
+ .spyOn(webex.cc.services.dialer, 'skipPreviewContact')
3986
+ .mockResolvedValue(mockResponse);
3987
+
3988
+ const result = await webex.cc.skipPreviewContact(previewPayload);
3989
+
3990
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Skipping campaign preview contact', {
3991
+ module: CC_FILE,
3992
+ method: 'skipPreviewContact',
3993
+ });
3994
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
3995
+ 'Campaign preview contact skipped successfully',
3996
+ {
3997
+ module: CC_FILE,
3998
+ method: 'skipPreviewContact',
3999
+ trackingId: 'track-123',
4000
+ interactionId: previewPayload.interactionId,
4001
+ }
4002
+ );
4003
+
4004
+ expect(skipPreviewContactMock).toHaveBeenCalledWith({data: previewPayload});
4005
+ expect(result).toEqual(mockResponse);
4006
+ });
4007
+
4008
+ it('should handle error during skipPreviewContact', async () => {
4009
+ getErrorDetailsSpy.mockRestore();
4010
+ getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
4011
+
4012
+ const error = {
4013
+ details: {
4014
+ trackingId: '1234',
4015
+ data: {
4016
+ reason: 'Error while performing skipPreviewContact',
4017
+ },
4018
+ },
4019
+ };
4020
+
4021
+ jest.spyOn(webex.cc.services.dialer, 'skipPreviewContact').mockRejectedValue(error);
4022
+
4023
+ await expect(webex.cc.skipPreviewContact(previewPayload)).rejects.toThrow(
4024
+ error.details.data.reason
4025
+ );
4026
+
4027
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Skipping campaign preview contact', {
4028
+ module: CC_FILE,
4029
+ method: 'skipPreviewContact',
4030
+ });
4031
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
4032
+ `skipPreviewContact failed with reason: ${error.details.data.reason}`,
4033
+ {module: CC_FILE, method: 'skipPreviewContact', trackingId: error.details.trackingId}
4034
+ );
4035
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'skipPreviewContact', CC_FILE);
4036
+ });
4037
+ });
4038
+
4039
+ describe('removePreviewContact', () => {
4040
+ const previewPayload = {
4041
+ interactionId: 'interaction-123',
4042
+ campaignId: 'campaign-456',
4043
+ };
4044
+
4045
+ it('should remove preview contact successfully', async () => {
4046
+ const mockResponse = {trackingId: 'track-123'} as AgentContact;
4047
+
4048
+ const removePreviewContactMock = jest
4049
+ .spyOn(webex.cc.services.dialer, 'removePreviewContact')
4050
+ .mockResolvedValue(mockResponse);
4051
+
4052
+ const result = await webex.cc.removePreviewContact(previewPayload);
4053
+
4054
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Removing campaign preview contact', {
4055
+ module: CC_FILE,
4056
+ method: 'removePreviewContact',
4057
+ });
4058
+ expect(LoggerProxy.log).toHaveBeenCalledWith(
4059
+ 'Campaign preview contact removed successfully',
4060
+ {
4061
+ module: CC_FILE,
4062
+ method: 'removePreviewContact',
4063
+ trackingId: 'track-123',
4064
+ interactionId: previewPayload.interactionId,
4065
+ }
4066
+ );
4067
+
4068
+ expect(removePreviewContactMock).toHaveBeenCalledWith({data: previewPayload});
4069
+ expect(result).toEqual(mockResponse);
4070
+ });
4071
+
4072
+ it('should handle error during removePreviewContact', async () => {
4073
+ getErrorDetailsSpy.mockRestore();
4074
+ getErrorDetailsSpy = jest.spyOn(Utils, 'getErrorDetails');
4075
+
4076
+ const error = {
4077
+ details: {
4078
+ trackingId: '1234',
4079
+ data: {
4080
+ reason: 'Error while performing removePreviewContact',
4081
+ },
4082
+ },
4083
+ };
4084
+
4085
+ jest.spyOn(webex.cc.services.dialer, 'removePreviewContact').mockRejectedValue(error);
4086
+
4087
+ await expect(webex.cc.removePreviewContact(previewPayload)).rejects.toThrow(
4088
+ error.details.data.reason
4089
+ );
4090
+
4091
+ expect(LoggerProxy.info).toHaveBeenCalledWith('Removing campaign preview contact', {
4092
+ module: CC_FILE,
4093
+ method: 'removePreviewContact',
4094
+ });
4095
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
4096
+ `removePreviewContact failed with reason: ${error.details.data.reason}`,
4097
+ {module: CC_FILE, method: 'removePreviewContact', trackingId: error.details.trackingId}
4098
+ );
4099
+ expect(getErrorDetailsSpy).toHaveBeenCalledWith(error, 'removePreviewContact', CC_FILE);
4100
+ });
4101
+ });
4102
+
4103
+ describe('private setManageWebexCallingInWxcc (runtime toggle impl)', () => {
4104
+ beforeEach(() => {
4105
+ webex.internal.device = {
4106
+ userId: 'user-123',
4107
+ registered: false,
4108
+ register: jest.fn().mockResolvedValue(undefined),
4109
+ unregister: jest.fn().mockResolvedValue(undefined),
4110
+ };
4111
+ webex.internal.mercury = {
4112
+ connected: false,
4113
+ connect: jest.fn().mockResolvedValue(undefined),
4114
+ disconnect: jest.fn().mockResolvedValue(undefined),
4115
+ };
4116
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
4117
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
4118
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
4119
+ webex.cc['agentConfig'] = {
4120
+ agentId: 'agent-123',
4121
+ deviceType: LoginOption.EXTENSION,
4122
+ } as Profile;
4123
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
4124
+ webex.cc.webCallingService.setLoginOption = jest.fn((loginOption: LoginOption) => {
4125
+ webex.cc.webCallingService.loginOption = loginOption;
4126
+ });
4127
+ });
4128
+
4129
+ afterEach(() => {
4130
+ jest.restoreAllMocks();
4131
+ });
4132
+
4133
+ it('should update config, task manager, and publish usersub when enabled', async () => {
4134
+ const publishSpy = jest
4135
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4136
+ .mockResolvedValue(undefined);
4137
+
4138
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
4139
+
4140
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
4141
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(true);
4142
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
4143
+ });
4144
+
4145
+ it('should publish false and update config when disabled after wxApp was enabled', async () => {
4146
+ webex.internal.device = {
4147
+ userId: 'user-123',
4148
+ url: 'https://wdm.example.com/devices/dev-1',
4149
+ };
4150
+ webex.request = jest.fn().mockResolvedValue({body: {}});
4151
+
4152
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
4153
+
4154
+ const publishSpy = jest
4155
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4156
+ .mockResolvedValue(undefined);
4157
+
4158
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
4159
+
4160
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4161
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
4162
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
4163
+ });
4164
+
4165
+ it('should skip usersub publish when disabling without prior wxApp publish', async () => {
4166
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
4167
+ const publishSpy = jest
4168
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4169
+ .mockResolvedValue(undefined);
4170
+
4171
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
4172
+
4173
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4174
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
4175
+ expect(publishSpy).not.toHaveBeenCalled();
4176
+ });
4177
+
4178
+ it('should throw when enabling before login', async () => {
4179
+ webex.internal.device = undefined;
4180
+
4181
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
4182
+ 'Cannot publish answer-calls-on-wxcc: user is not logged in'
4183
+ );
4184
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4185
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
4186
+ });
4187
+
4188
+ it('should throw when enabling on BROWSER (Desktop) login', async () => {
4189
+ webex.cc['agentConfig'] = {deviceType: LoginOption.BROWSER} as Profile;
4190
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
4191
+ const publishSpy = jest
4192
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4193
+ .mockResolvedValue(undefined);
4194
+
4195
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
4196
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
4197
+ );
4198
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4199
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
4200
+ expect(publishSpy).not.toHaveBeenCalled();
4201
+ });
4202
+
4203
+ it('should throw when enabling before station login', async () => {
4204
+ webex.cc['agentConfig'] = undefined as unknown as Profile;
4205
+ webex.cc.webCallingService.loginOption = undefined as unknown as LoginOption;
4206
+ const publishSpy = jest
4207
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4208
+ .mockResolvedValue(undefined);
4209
+
4210
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
4211
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
4212
+ );
4213
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4214
+ expect(mockTaskManager.applyEnableWxBetterTogether).not.toHaveBeenCalled();
4215
+ expect(publishSpy).not.toHaveBeenCalled();
4216
+ });
4217
+
4218
+ it('should throw when webCallingService is BROWSER even if agentConfig deviceType is stale', async () => {
4219
+ webex.cc['agentConfig'] = {deviceType: LoginOption.EXTENSION} as Profile;
4220
+ webex.cc.webCallingService.loginOption = LoginOption.BROWSER;
4221
+ const publishSpy = jest
4222
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4223
+ .mockResolvedValue(undefined);
4224
+
4225
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
4226
+ 'setManageWebexCallingInWxcc requires EXTENSION or AGENT_DN station login'
4227
+ );
4228
+ expect(publishSpy).not.toHaveBeenCalled();
4229
+ });
4230
+
4231
+ it('should publish usersub when enabling on AGENT_DN login', async () => {
4232
+ webex.cc['agentConfig'] = {
4233
+ agentId: 'agent-123',
4234
+ deviceType: LoginOption.AGENT_DN,
4235
+ } as Profile;
4236
+ const publishSpy = jest
4237
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4238
+ .mockResolvedValue(undefined);
4239
+
4240
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
4241
+
4242
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
4243
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
4244
+ });
4245
+
4246
+ it('should no-op usersub publish when disabling before login', async () => {
4247
+ webex.internal.device = undefined;
4248
+ const publishSpy = jest
4249
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4250
+ .mockResolvedValue(undefined);
4251
+
4252
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
4253
+
4254
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4255
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
4256
+ expect(publishSpy).not.toHaveBeenCalled();
4257
+ });
4258
+
4259
+ it('should subscribe to wxApp Mercury mute sync when enabled', async () => {
4260
+ webex.cc['agentConfig'] = {agentId: 'agent-123'} as Profile;
4261
+ webex.internal.device = {
4262
+ ...webex.internal.device,
4263
+ registered: true,
4264
+ register: jest.fn().mockResolvedValue(undefined),
4265
+ };
4266
+ webex.internal.mercury.connected = true;
4267
+ webex.internal.mercury.connect = jest.fn().mockResolvedValue(undefined);
4268
+ jest
4269
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4270
+ .mockResolvedValue(undefined);
4271
+ const subscribeSpy = jest
4272
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
4273
+ .mockImplementation(() => {});
4274
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
4275
+
4276
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
4277
+
4278
+ expect(subscribeSpy).toHaveBeenCalledWith('agent-123', expect.any(Function));
4279
+ });
4280
+
4281
+ it('should unsubscribe from wxApp Mercury mute sync when disabled', async () => {
4282
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
4283
+ jest
4284
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4285
+ .mockResolvedValue(undefined);
4286
+ const unsubscribeSpy = jest
4287
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe')
4288
+ .mockImplementation(() => {});
4289
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
4290
+
4291
+ await (webex.cc as any).setManageWebexCallingInWxcc(false);
4292
+
4293
+ expect(unsubscribeSpy).toHaveBeenCalled();
4294
+ });
4295
+
4296
+ it('should register device and connect mercury when enabling wxApp sync', async () => {
4297
+ webex.cc['agentConfig'] = {agentId: 'agent-123'} as Profile;
4298
+ webex.internal.device = {
4299
+ userId: 'user-123',
4300
+ registered: false,
4301
+ register: jest.fn().mockResolvedValue(undefined),
4302
+ };
4303
+ webex.internal.mercury = {
4304
+ connected: false,
4305
+ connect: jest.fn().mockResolvedValue(undefined),
4306
+ };
4307
+ jest
4308
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4309
+ .mockResolvedValue(undefined);
4310
+ const subscribeSpy = jest
4311
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
4312
+ .mockImplementation(() => {});
4313
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
4314
+
4315
+ await (webex.cc as any).setManageWebexCallingInWxcc(true);
4316
+
4317
+ expect(webex.internal.device.register).toHaveBeenCalled();
4318
+ expect(webex.internal.mercury.connect).toHaveBeenCalled();
4319
+ expect(subscribeSpy).toHaveBeenCalledWith('agent-123', expect.any(Function));
4320
+ });
4321
+
4322
+ it('should rollback config and task manager when usersub publish fails', async () => {
4323
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
4324
+ jest
4325
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4326
+ .mockRejectedValue(new Error('usersub failed'));
4327
+
4328
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow('usersub failed');
4329
+
4330
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4331
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(true);
4332
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenLastCalledWith(false);
4333
+ });
4334
+
4335
+ it('should rollback config and release Mercury resources when Mercury connect fails', async () => {
4336
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
4337
+ const publishSpy = jest
4338
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4339
+ .mockResolvedValue(undefined);
4340
+ jest
4341
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
4342
+ .mockRejectedValue(new Error('mercury connect failed'));
4343
+ const releaseSpy = jest
4344
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
4345
+ .mockResolvedValue(undefined);
4346
+ const unsubscribeSpy = jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe');
4347
+ const teardownSpy = jest.spyOn(webex.cc['webexCrossClientService'], 'teardown');
4348
+
4349
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
4350
+ 'mercury connect failed'
4351
+ );
4352
+
4353
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
4354
+ expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenLastCalledWith(false);
4355
+ expect(publishSpy).not.toHaveBeenCalled();
4356
+ expect(teardownSpy).not.toHaveBeenCalled();
4357
+ expect(unsubscribeSpy).toHaveBeenCalled();
4358
+ expect(releaseSpy).toHaveBeenCalled();
4359
+ });
4360
+
4361
+ it('should schedule compensating false retry when rollback publish fails after enable error', async () => {
4362
+ jest.useFakeTimers();
4363
+ try {
4364
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
4365
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
4366
+ webex.cc.agentConfig = {
4367
+ agentId: 'agent-123',
4368
+ deviceType: LoginOption.EXTENSION,
4369
+ };
4370
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
4371
+
4372
+ const publishSpy = jest
4373
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
4374
+ .mockResolvedValueOnce(undefined)
4375
+ .mockRejectedValueOnce(new Error('compensating false failed'))
4376
+ .mockResolvedValue(undefined);
4377
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
4378
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
4379
+ jest
4380
+ .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
4381
+ .mockResolvedValue(undefined);
4382
+ jest
4383
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
4384
+ .mockResolvedValue(undefined);
4385
+ jest
4386
+ .spyOn(mockTaskManager, 'syncWxAppMuteFromCallDetailsForAllTasks')
4387
+ .mockImplementation(() => {
4388
+ throw new Error('sync failed');
4389
+ });
4390
+ const scheduleRetrySpy = jest.spyOn(
4391
+ webex.cc as any,
4392
+ 'scheduleCompensatingFalsePublishRetry'
4393
+ );
4394
+
4395
+ await expect((webex.cc as any).setManageWebexCallingInWxcc(true)).rejects.toThrow(
4396
+ 'sync failed'
4397
+ );
4398
+
4399
+ expect(scheduleRetrySpy).toHaveBeenCalled();
4400
+ expect(publishSpy).toHaveBeenCalledTimes(2);
4401
+ expect(publishSpy).toHaveBeenNthCalledWith(1, true, {userId: 'user-123', trackPublishMetrics: true});
4402
+ expect(publishSpy).toHaveBeenNthCalledWith(2, false, {userId: 'user-123', trackPublishMetrics: true});
4403
+
4404
+ jest.advanceTimersByTime(30_000);
4405
+ await publishSpy.mock.results[2]?.value;
4406
+
4407
+ expect(publishSpy).toHaveBeenCalledTimes(3);
4408
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
4409
+ } finally {
4410
+ jest.useRealTimers();
4411
+ }
4412
+ });
4413
+ });
2323
4414
  });