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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/.sdd/manifest.json +5 -4
  2. package/ai-docs/ARCHITECTURE.md +1 -1
  3. package/ai-docs/CONTRACTS.md +6 -2
  4. package/ai-docs/SECURITY.md +1 -1
  5. package/ai-docs/contact-center-spec.md +22 -4
  6. package/ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md +362 -0
  7. package/dist/cc.js +440 -10
  8. package/dist/cc.js.map +1 -1
  9. package/dist/config.js +7 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +10 -2
  12. package/dist/constants.js.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/metrics/behavioral-events.js +13 -0
  15. package/dist/metrics/behavioral-events.js.map +1 -1
  16. package/dist/metrics/constants.js +2 -0
  17. package/dist/metrics/constants.js.map +1 -1
  18. package/dist/services/AddressBook.js +18 -15
  19. package/dist/services/AddressBook.js.map +1 -1
  20. package/dist/services/AnswerCallOnWebexService.js +174 -0
  21. package/dist/services/AnswerCallOnWebexService.js.map +1 -0
  22. package/dist/services/EntryPoint.js +46 -68
  23. package/dist/services/EntryPoint.js.map +1 -1
  24. package/dist/services/Queue.js +27 -22
  25. package/dist/services/Queue.js.map +1 -1
  26. package/dist/services/WebexCrossClientService.js +171 -0
  27. package/dist/services/WebexCrossClientService.js.map +1 -0
  28. package/dist/services/WxAppTelephonyMercurySync.js +93 -0
  29. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -0
  30. package/dist/services/config/Util.js +3 -0
  31. package/dist/services/config/Util.js.map +1 -1
  32. package/dist/services/config/constants.js +10 -6
  33. package/dist/services/config/constants.js.map +1 -1
  34. package/dist/services/config/types.js +4 -0
  35. package/dist/services/config/types.js.map +1 -1
  36. package/dist/services/core/Err.js.map +1 -1
  37. package/dist/services/core/WebexRequest.js +6 -2
  38. package/dist/services/core/WebexRequest.js.map +1 -1
  39. package/dist/services/core/aqm-reqs.js +28 -14
  40. package/dist/services/core/aqm-reqs.js.map +1 -1
  41. package/dist/services/core/types.js.map +1 -1
  42. package/dist/services/task/Task.js +73 -7
  43. package/dist/services/task/Task.js.map +1 -1
  44. package/dist/services/task/TaskFactory.js +8 -4
  45. package/dist/services/task/TaskFactory.js.map +1 -1
  46. package/dist/services/task/TaskManager.js +353 -21
  47. package/dist/services/task/TaskManager.js.map +1 -1
  48. package/dist/services/task/WebexCallingUtils.js +70 -0
  49. package/dist/services/task/WebexCallingUtils.js.map +1 -0
  50. package/dist/services/task/constants.js +4 -1
  51. package/dist/services/task/constants.js.map +1 -1
  52. package/dist/services/task/contact.js +29 -0
  53. package/dist/services/task/contact.js.map +1 -1
  54. package/dist/services/task/digital/Digital.js +3 -2
  55. package/dist/services/task/digital/Digital.js.map +1 -1
  56. package/dist/services/task/state-machine/TaskStateMachine.js +135 -37
  57. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  58. package/dist/services/task/state-machine/actions.js +6 -1
  59. package/dist/services/task/state-machine/actions.js.map +1 -1
  60. package/dist/services/task/state-machine/guards.js +38 -11
  61. package/dist/services/task/state-machine/guards.js.map +1 -1
  62. package/dist/services/task/state-machine/types.js.map +1 -1
  63. package/dist/services/task/state-machine/uiControlsComputer.js +118 -18
  64. package/dist/services/task/state-machine/uiControlsComputer.js.map +1 -1
  65. package/dist/services/task/types.js +16 -1
  66. package/dist/services/task/types.js.map +1 -1
  67. package/dist/services/task/voice/Voice.js +350 -14
  68. package/dist/services/task/voice/Voice.js.map +1 -1
  69. package/dist/services/task/voice/WebRTC.js +4 -1
  70. package/dist/services/task/voice/WebRTC.js.map +1 -1
  71. package/dist/services/task/voice/wxAppVoiceMethods.js +201 -0
  72. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -0
  73. package/dist/services/wxAppTelephonyUtils.js +19 -0
  74. package/dist/services/wxAppTelephonyUtils.js.map +1 -0
  75. package/dist/types/cc.d.ts +77 -4
  76. package/dist/types/config.d.ts +7 -0
  77. package/dist/types/constants.d.ts +8 -0
  78. package/dist/types/index.d.ts +3 -1
  79. package/dist/types/metrics/constants.d.ts +2 -0
  80. package/dist/types/services/AddressBook.d.ts +2 -1
  81. package/dist/types/services/AnswerCallOnWebexService.d.ts +37 -0
  82. package/dist/types/services/EntryPoint.d.ts +7 -5
  83. package/dist/types/services/Queue.d.ts +5 -3
  84. package/dist/types/services/WebexCrossClientService.d.ts +28 -0
  85. package/dist/types/services/WxAppTelephonyMercurySync.d.ts +28 -0
  86. package/dist/types/services/config/constants.d.ts +8 -5
  87. package/dist/types/services/config/types.d.ts +17 -4
  88. package/dist/types/services/core/Err.d.ts +2 -0
  89. package/dist/types/services/core/WebexRequest.d.ts +1 -0
  90. package/dist/types/services/core/types.d.ts +2 -0
  91. package/dist/types/services/task/Task.d.ts +20 -2
  92. package/dist/types/services/task/TaskFactory.d.ts +2 -1
  93. package/dist/types/services/task/WebexCallingUtils.d.ts +11 -0
  94. package/dist/types/services/task/constants.d.ts +3 -0
  95. package/dist/types/services/task/contact.d.ts +4 -0
  96. package/dist/types/services/task/digital/Digital.d.ts +2 -2
  97. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +260 -56
  98. package/dist/types/services/task/state-machine/guards.d.ts +6 -5
  99. package/dist/types/services/task/state-machine/types.d.ts +9 -1
  100. package/dist/types/services/task/types.d.ts +81 -4
  101. package/dist/types/services/task/voice/Voice.d.ts +48 -9
  102. package/dist/types/services/task/voice/WebRTC.d.ts +2 -2
  103. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +52 -0
  104. package/dist/types/services/wxAppTelephonyUtils.d.ts +5 -0
  105. package/dist/types/types.d.ts +14 -8
  106. package/dist/types/utils/PageCache.d.ts +20 -3
  107. package/dist/types.js +7 -2
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/PageCache.js +19 -5
  110. package/dist/utils/PageCache.js.map +1 -1
  111. package/dist/webex.js +1 -1
  112. package/package.json +9 -9
  113. package/src/cc.ts +506 -9
  114. package/src/config.ts +7 -0
  115. package/src/constants.ts +8 -0
  116. package/src/index.ts +2 -0
  117. package/src/metrics/ai-docs/metrics-spec.md +9 -3
  118. package/src/metrics/behavioral-events.ts +14 -0
  119. package/src/metrics/constants.ts +2 -0
  120. package/src/services/AddressBook.ts +17 -6
  121. package/src/services/AnswerCallOnWebexService.ts +206 -0
  122. package/src/services/EntryPoint.ts +59 -60
  123. package/src/services/Queue.ts +29 -12
  124. package/src/services/WebexCrossClientService.ts +212 -0
  125. package/src/services/WxAppTelephonyMercurySync.ts +115 -0
  126. package/src/services/ai-docs/AGENTS.md +10 -10
  127. package/src/services/ai-docs/services-spec.md +6 -1
  128. package/src/services/config/Util.ts +3 -0
  129. package/src/services/config/ai-docs/AGENTS.md +1 -1
  130. package/src/services/config/ai-docs/ARCHITECTURE.md +2 -2
  131. package/src/services/config/ai-docs/config-spec.md +6 -0
  132. package/src/services/config/constants.ts +10 -6
  133. package/src/services/config/types.ts +16 -4
  134. package/src/services/core/Err.ts +1 -0
  135. package/src/services/core/WebexRequest.ts +3 -1
  136. package/src/services/core/ai-docs/core-spec.md +5 -1
  137. package/src/services/core/aqm-reqs.ts +30 -15
  138. package/src/services/core/types.ts +2 -0
  139. package/src/services/task/Task.ts +79 -8
  140. package/src/services/task/TaskFactory.ts +8 -3
  141. package/src/services/task/TaskManager.ts +522 -16
  142. package/src/services/task/WebexCallingUtils.ts +136 -0
  143. package/src/services/task/ai-docs/ARCHITECTURE.md +1 -0
  144. package/src/services/task/ai-docs/task-spec.md +152 -2
  145. package/src/services/task/constants.ts +3 -0
  146. package/src/services/task/contact.ts +30 -0
  147. package/src/services/task/digital/Digital.ts +4 -2
  148. package/src/services/task/state-machine/TaskStateMachine.ts +210 -63
  149. package/src/services/task/state-machine/actions.ts +5 -2
  150. package/src/services/task/state-machine/ai-docs/AGENTS.md +8 -4
  151. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +17 -8
  152. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +56 -24
  153. package/src/services/task/state-machine/guards.ts +64 -11
  154. package/src/services/task/state-machine/types.ts +16 -1
  155. package/src/services/task/state-machine/uiControlsComputer.ts +206 -32
  156. package/src/services/task/types.ts +114 -4
  157. package/src/services/task/voice/Voice.ts +462 -12
  158. package/src/services/task/voice/WebRTC.ts +5 -1
  159. package/src/services/task/voice/wxAppVoiceMethods.ts +307 -0
  160. package/src/services/wxAppTelephonyUtils.ts +14 -0
  161. package/src/types.ts +33 -9
  162. package/src/utils/AGENTS.md +21 -10
  163. package/src/utils/PageCache.ts +38 -5
  164. package/src/utils/ai-docs/utils-spec.md +21 -11
  165. package/test/unit/spec/cc.ts +1274 -0
  166. package/test/unit/spec/metrics/behavioral-events.ts +18 -0
  167. package/test/unit/spec/services/AddressBook.ts +37 -6
  168. package/test/unit/spec/services/AnswerCallOnWebexService.ts +223 -0
  169. package/test/unit/spec/services/EntryPoint.ts +87 -40
  170. package/test/unit/spec/services/Queue.ts +123 -12
  171. package/test/unit/spec/services/WebexCrossClientService.ts +261 -0
  172. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +113 -0
  173. package/test/unit/spec/services/config/Util.ts +85 -0
  174. package/test/unit/spec/services/core/WebexRequest.ts +3 -1
  175. package/test/unit/spec/services/core/aqm-reqs.ts +113 -1
  176. package/test/unit/spec/services/task/Task.ts +200 -0
  177. package/test/unit/spec/services/task/TaskFactory.ts +39 -2
  178. package/test/unit/spec/services/task/TaskManager.ts +1294 -1
  179. package/test/unit/spec/services/task/WebexCallingUtils.ts +153 -0
  180. package/test/unit/spec/services/task/contact.ts +33 -0
  181. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +854 -72
  182. package/test/unit/spec/services/task/state-machine/guards.ts +210 -8
  183. package/test/unit/spec/services/task/state-machine/uiControlsComputer.ts +445 -7
  184. package/test/unit/spec/services/task/voice/Voice.ts +874 -0
  185. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +459 -0
  186. package/umd/contact-center.min.js +2 -2
  187. package/umd/contact-center.min.js.map +1 -1
@@ -143,7 +143,8 @@ describe('Queue', () => {
143
143
 
144
144
  expect(mockWebex.request).toHaveBeenCalledWith({
145
145
  service: 'wcc-api-gateway',
146
- resource: '/organization/test-org-id/v2/contact-service-queue?page=0&pageSize=100',
146
+ resource:
147
+ '/organization/test-org-id/v2/contact-service-queue?page=0&pageSize=100&filter=queueType%3D%3DINBOUND%3BchannelType%3D%3DTELEPHONY%3Bactive%3D%3Dtrue&sort=name%2CASC&desktopProfileFilter=true&agentView=true&firstLevelView=true',
147
148
  method: HTTP_METHODS.GET,
148
149
  });
149
150
 
@@ -156,7 +157,7 @@ describe('Queue', () => {
156
157
  statusCode: 200,
157
158
  recordCount: 2,
158
159
  totalRecords: 2,
159
- isSearchRequest: false,
160
+ isSearchRequest: true,
160
161
  isFirstPage: true,
161
162
  },
162
163
  ['behavioral', 'operational']
@@ -168,14 +169,15 @@ describe('Queue', () => {
168
169
  orgId: 'test-org-id',
169
170
  page: 0,
170
171
  pageSize: 100,
171
- isSearchRequest: false,
172
+ isSearchRequest: true,
172
173
  }),
173
174
  });
174
175
  expect(LoggerProxy.log).toHaveBeenCalledWith('Making API request to fetch contact service queues', {
175
176
  module: 'Queue',
176
177
  method: 'getQueues',
177
178
  data: expect.objectContaining({
178
- resource: '/organization/test-org-id/v2/contact-service-queue?page=0&pageSize=100',
179
+ resource:
180
+ '/organization/test-org-id/v2/contact-service-queue?page=0&pageSize=100&filter=queueType%3D%3DINBOUND%3BchannelType%3D%3DTELEPHONY%3Bactive%3D%3Dtrue&sort=name%2CASC&desktopProfileFilter=true&agentView=true&firstLevelView=true',
179
181
  service: 'wcc-api-gateway',
180
182
  }),
181
183
  });
@@ -201,7 +203,7 @@ describe('Queue', () => {
201
203
 
202
204
  expect(mockWebex.request).toHaveBeenCalledWith({
203
205
  service: 'wcc-api-gateway',
204
- resource: '/organization/test-org-id/v2/contact-service-queue?page=1&pageSize=25&filter=queueType%3D%3D%22INBOUND%22&attributes=id%2Cname%2CqueueType&search=support&sortBy=name&sortOrder=desc&desktopProfileFilter=true&provisioningView=false&singleObjectResponse=true',
206
+ resource: '/organization/test-org-id/v2/contact-service-queue?page=1&pageSize=25&filter=queueType%3D%3D%22INBOUND%22&attributes=id%2Cname%2CqueueType&search=support&sort=name%2CDESC&desktopProfileFilter=true&provisioningView=false&singleObjectResponse=true&agentView=true&firstLevelView=true',
205
207
  method: HTTP_METHODS.GET,
206
208
  });
207
209
 
@@ -215,6 +217,108 @@ describe('Queue', () => {
215
217
  );
216
218
  });
217
219
 
220
+ it('should default sortOrder-only requests to name and bypass cache', async () => {
221
+ (mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
222
+
223
+ await queueAPI.getQueues({sortOrder: 'desc'});
224
+ await queueAPI.getQueues({sortOrder: 'desc'});
225
+
226
+ expect(mockWebex.request).toHaveBeenCalledTimes(2);
227
+ expect(mockWebex.request).toHaveBeenLastCalledWith({
228
+ service: 'wcc-api-gateway',
229
+ resource:
230
+ '/organization/test-org-id/v2/contact-service-queue?page=0&pageSize=100&filter=queueType%3D%3DINBOUND%3BchannelType%3D%3DTELEPHONY%3Bactive%3D%3Dtrue&sort=name%2CDESC&desktopProfileFilter=true&agentView=true&firstLevelView=true',
231
+ method: HTTP_METHODS.GET,
232
+ });
233
+ });
234
+
235
+ it('should allow existing parameters to override the default queue policy', async () => {
236
+ (mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
237
+
238
+ const result = await queueAPI.getQueues({
239
+ page: 1,
240
+ pageSize: 25,
241
+ search: 'support',
242
+ filter: 'queueType==INBOUND;channelType==SOCIAL_CHANNEL;active==true',
243
+ });
244
+
245
+ expect(mockWebex.request).toHaveBeenCalledWith({
246
+ service: 'wcc-api-gateway',
247
+ resource:
248
+ '/organization/test-org-id/v2/contact-service-queue?page=1&pageSize=25&filter=queueType%3D%3DINBOUND%3BchannelType%3D%3DSOCIAL_CHANNEL%3Bactive%3D%3Dtrue&search=support&sort=name%2CASC&desktopProfileFilter=true&agentView=true&firstLevelView=true',
249
+ method: HTTP_METHODS.GET,
250
+ });
251
+ expect(result).toBe(mockResponse.body);
252
+ });
253
+
254
+ it('should preserve defaults when optional overrides are undefined', async () => {
255
+ (mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
256
+
257
+ await queueAPI.getQueues({
258
+ filter: undefined,
259
+ sortBy: undefined,
260
+ sortOrder: undefined,
261
+ desktopProfileFilter: undefined,
262
+ });
263
+
264
+ expect(mockWebex.request).toHaveBeenCalledWith({
265
+ service: 'wcc-api-gateway',
266
+ resource:
267
+ '/organization/test-org-id/v2/contact-service-queue?page=0&pageSize=100&filter=queueType%3D%3DINBOUND%3BchannelType%3D%3DTELEPHONY%3Bactive%3D%3Dtrue&sort=name%2CASC&desktopProfileFilter=true&agentView=true&firstLevelView=true',
268
+ method: HTTP_METHODS.GET,
269
+ });
270
+ });
271
+
272
+ it('should preserve an explicit empty filter override', async () => {
273
+ (mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
274
+
275
+ await queueAPI.getQueues({filter: ''});
276
+
277
+ expect(mockWebex.request).toHaveBeenCalledWith({
278
+ service: 'wcc-api-gateway',
279
+ resource:
280
+ '/organization/test-org-id/v2/contact-service-queue?page=0&pageSize=100&sort=name%2CASC&desktopProfileFilter=true&agentView=true&firstLevelView=true',
281
+ method: HTTP_METHODS.GET,
282
+ });
283
+ });
284
+
285
+ it('should bypass cache for the default queue policy', async () => {
286
+ (mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
287
+
288
+ await queueAPI.getQueues();
289
+ await queueAPI.getQueues();
290
+
291
+ expect(mockWebex.request).toHaveBeenCalledTimes(2);
292
+ expect(mockWebex.request).toHaveBeenLastCalledWith(
293
+ expect.objectContaining({
294
+ resource: expect.stringContaining('channelType%3D%3DTELEPHONY'),
295
+ })
296
+ );
297
+ });
298
+
299
+ it('should disable the internal agent views when desktop profile filtering is false', async () => {
300
+ (mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
301
+
302
+ await queueAPI.getQueues({
303
+ desktopProfileFilter: false,
304
+ provisioningView: false,
305
+ singleObjectResponse: false,
306
+ });
307
+ await queueAPI.getQueues({
308
+ desktopProfileFilter: false,
309
+ provisioningView: false,
310
+ singleObjectResponse: false,
311
+ });
312
+
313
+ expect(mockWebex.request).toHaveBeenCalledTimes(2);
314
+ expect(mockWebex.request).toHaveBeenLastCalledWith({
315
+ service: 'wcc-api-gateway',
316
+ resource:
317
+ '/organization/test-org-id/v2/contact-service-queue?page=0&pageSize=100&filter=queueType%3D%3DINBOUND%3BchannelType%3D%3DTELEPHONY%3Bactive%3D%3Dtrue&sort=name%2CASC&desktopProfileFilter=false&provisioningView=false&singleObjectResponse=false&agentView=false&firstLevelView=false',
318
+ method: HTTP_METHODS.GET,
319
+ });
320
+ });
321
+
218
322
  it('should handle API errors and track metrics', async () => {
219
323
  (mockWebex.request as jest.Mock).mockRejectedValue(new Error('Internal Server Error'));
220
324
 
@@ -222,7 +326,8 @@ describe('Queue', () => {
222
326
 
223
327
  expect(mockWebex.request).toHaveBeenCalledWith({
224
328
  service: 'wcc-api-gateway',
225
- resource: '/organization/test-org-id/v2/contact-service-queue?page=0&pageSize=100',
329
+ resource:
330
+ '/organization/test-org-id/v2/contact-service-queue?page=0&pageSize=100&filter=queueType%3D%3DINBOUND%3BchannelType%3D%3DTELEPHONY%3Bactive%3D%3Dtrue&sort=name%2CASC&desktopProfileFilter=true&agentView=true&firstLevelView=true',
226
331
  method: HTTP_METHODS.GET,
227
332
  });
228
333
 
@@ -231,7 +336,7 @@ describe('Queue', () => {
231
336
  {
232
337
  orgId: 'test-org-id',
233
338
  error: 'Internal Server Error',
234
- isSearchRequest: false,
339
+ isSearchRequest: true,
235
340
  page: 0,
236
341
  pageSize: 100,
237
342
  },
@@ -243,7 +348,7 @@ describe('Queue', () => {
243
348
  data: expect.objectContaining({
244
349
  orgId: 'test-org-id',
245
350
  error: 'Internal Server Error',
246
- isSearchRequest: false,
351
+ isSearchRequest: true,
247
352
  page: 0,
248
353
  pageSize: 100,
249
354
  }),
@@ -253,13 +358,17 @@ describe('Queue', () => {
253
358
 
254
359
 
255
360
 
256
- it('should not track metrics for subsequent pages in simple pagination', async () => {
361
+ it('should track metrics for subsequent pages under the default filtered policy', async () => {
257
362
  (mockWebex.request as jest.Mock).mockResolvedValue(mockResponse);
258
363
 
259
364
  const result = await queueAPI.getQueues({page: 2});
260
365
  expect(result).toEqual(mockResponse.body);
261
366
 
262
- expect(mockMetricsManager.trackEvent).not.toHaveBeenCalled();
367
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
368
+ METRIC_EVENT_NAMES.QUEUE_FETCH_SUCCESS,
369
+ expect.objectContaining({isSearchRequest: true, isFirstPage: false}),
370
+ ['behavioral', 'operational']
371
+ );
263
372
  });
264
373
 
265
374
  it('should track metrics for search requests on any page', async () => {
@@ -307,14 +416,16 @@ describe('Queue', () => {
307
416
  expect(result.meta.page).toBe(1);
308
417
  expect(mockWebex.request).toHaveBeenCalledWith({
309
418
  service: 'wcc-api-gateway',
310
- resource: '/organization/test-org-id/v2/contact-service-queue?page=1&pageSize=100',
419
+ resource:
420
+ '/organization/test-org-id/v2/contact-service-queue?page=1&pageSize=100&filter=queueType%3D%3DINBOUND%3BchannelType%3D%3DTELEPHONY%3Bactive%3D%3Dtrue&sort=name%2CASC&desktopProfileFilter=true&agentView=true&firstLevelView=true',
311
421
  method: HTTP_METHODS.GET,
312
422
  });
313
423
  expect(LoggerProxy.log).toHaveBeenCalledWith('Making API request to fetch contact service queues', {
314
424
  module: 'Queue',
315
425
  method: 'getQueues',
316
426
  data: expect.objectContaining({
317
- resource: '/organization/test-org-id/v2/contact-service-queue?page=1&pageSize=100',
427
+ resource:
428
+ '/organization/test-org-id/v2/contact-service-queue?page=1&pageSize=100&filter=queueType%3D%3DINBOUND%3BchannelType%3D%3DTELEPHONY%3Bactive%3D%3Dtrue&sort=name%2CASC&desktopProfileFilter=true&agentView=true&firstLevelView=true',
318
429
  service: 'wcc-api-gateway',
319
430
  }),
320
431
  });
@@ -0,0 +1,261 @@
1
+ import WebexCrossClientService from '../../../../src/services/WebexCrossClientService';
2
+
3
+ describe('WebexCrossClientService', () => {
4
+ const userId = 'user-123';
5
+ const deviceUrl = 'https://wdm.example.com/devices/device-abc';
6
+
7
+ let webex: {request: jest.Mock; internal: {device: {userId: string; url: string}}};
8
+ let service: WebexCrossClientService;
9
+
10
+ beforeEach(() => {
11
+ webex = {
12
+ request: jest.fn().mockResolvedValue({body: {}}),
13
+ internal: {
14
+ device: {userId, url: deviceUrl},
15
+ },
16
+ };
17
+ service = new WebexCrossClientService(webex as never);
18
+ });
19
+
20
+ afterEach(() => {
21
+ service.teardown();
22
+ });
23
+
24
+ it('publishes usersub with appName wxcc (Agent Desktop parity)', async () => {
25
+ await service.setManageWebexCallingInWxcc(true);
26
+
27
+ expect(webex.request).toHaveBeenCalledWith(
28
+ expect.objectContaining({
29
+ method: 'POST',
30
+ service: 'usersub',
31
+ resource: 'publish',
32
+ body: expect.objectContaining({
33
+ users: [userId],
34
+ compositions: [
35
+ expect.objectContaining({
36
+ type: 'cross-client-state',
37
+ ttl: 900,
38
+ composition: {
39
+ devices: [
40
+ {
41
+ deviceId: 'device-abc',
42
+ appName: 'wxcc',
43
+ state: {'answer-calls-on-wxcc': true},
44
+ },
45
+ ],
46
+ },
47
+ }),
48
+ ],
49
+ }),
50
+ })
51
+ );
52
+ });
53
+
54
+ it('publishes false when suppression is disabled', async () => {
55
+ await service.setManageWebexCallingInWxcc(false);
56
+
57
+ expect(webex.request).toHaveBeenCalledWith(
58
+ expect.objectContaining({
59
+ body: expect.objectContaining({
60
+ compositions: [
61
+ expect.objectContaining({
62
+ composition: {
63
+ devices: [
64
+ {
65
+ deviceId: 'device-abc',
66
+ appName: 'wxcc',
67
+ state: {'answer-calls-on-wxcc': false},
68
+ },
69
+ ],
70
+ },
71
+ }),
72
+ ],
73
+ }),
74
+ })
75
+ );
76
+ });
77
+
78
+ it('throws when user ID is unavailable', async () => {
79
+ webex.internal.device.userId = undefined as unknown as string;
80
+
81
+ await expect(service.setManageWebexCallingInWxcc(true)).rejects.toThrow(
82
+ 'User ID is unavailable for cross-client publish'
83
+ );
84
+ expect(webex.request).not.toHaveBeenCalled();
85
+ });
86
+
87
+ it('throws when device URL is unavailable', async () => {
88
+ webex.internal.device.url = undefined as unknown as string;
89
+
90
+ await expect(service.setManageWebexCallingInWxcc(true)).rejects.toThrow(
91
+ 'Device URL is unavailable for cross-client publish'
92
+ );
93
+ expect(webex.request).not.toHaveBeenCalled();
94
+ });
95
+
96
+ it('rejects when usersub publish fails', async () => {
97
+ const err = new Error('usersub failed');
98
+ webex.request = jest.fn().mockRejectedValue(err);
99
+
100
+ await expect(service.setManageWebexCallingInWxcc(true)).rejects.toThrow('usersub failed');
101
+ });
102
+
103
+ it('uses custom ttl from options', async () => {
104
+ await service.setManageWebexCallingInWxcc(true, {ttl: 1200});
105
+
106
+ expect(webex.request).toHaveBeenCalledWith(
107
+ expect.objectContaining({
108
+ body: expect.objectContaining({
109
+ compositions: [expect.objectContaining({ttl: 1200})],
110
+ }),
111
+ })
112
+ );
113
+ });
114
+
115
+ it('refreshes publish before ttl expiry while enabled', async () => {
116
+ jest.useFakeTimers();
117
+ try {
118
+ await service.setManageWebexCallingInWxcc(true, {ttl: 900});
119
+ expect(webex.request).toHaveBeenCalledTimes(1);
120
+
121
+ jest.advanceTimersByTime(840_000);
122
+
123
+ await Promise.resolve();
124
+
125
+ expect(webex.request).toHaveBeenCalledTimes(2);
126
+ expect(webex.request).toHaveBeenLastCalledWith(
127
+ expect.objectContaining({
128
+ body: expect.objectContaining({
129
+ compositions: [
130
+ expect.objectContaining({
131
+ composition: {
132
+ devices: [
133
+ {
134
+ deviceId: 'device-abc',
135
+ appName: 'wxcc',
136
+ state: {'answer-calls-on-wxcc': true},
137
+ },
138
+ ],
139
+ },
140
+ }),
141
+ ],
142
+ }),
143
+ })
144
+ );
145
+ } finally {
146
+ jest.useRealTimers();
147
+ }
148
+ });
149
+
150
+ it('retries failed refresh publish before ttl expiry', async () => {
151
+ jest.useFakeTimers();
152
+ try {
153
+ const setManageSpy = jest.spyOn(service, 'setManageWebexCallingInWxcc');
154
+ webex.request = jest
155
+ .fn()
156
+ .mockResolvedValueOnce({body: {}})
157
+ .mockRejectedValueOnce(new Error('refresh failed'))
158
+ .mockResolvedValueOnce({body: {}});
159
+
160
+ await service.setManageWebexCallingInWxcc(true, {ttl: 900});
161
+ expect(webex.request).toHaveBeenCalledTimes(1);
162
+
163
+ jest.advanceTimersByTime(840_000);
164
+ await expect(setManageSpy.mock.results[1].value).rejects.toThrow('refresh failed');
165
+ expect(webex.request).toHaveBeenCalledTimes(2);
166
+
167
+ jest.advanceTimersByTime(30_000);
168
+ await setManageSpy.mock.results[2].value;
169
+
170
+ expect(webex.request).toHaveBeenCalledTimes(3);
171
+ } finally {
172
+ jest.useRealTimers();
173
+ }
174
+ });
175
+
176
+ it('schedules bounded retry when scheduled refresh publish fails', async () => {
177
+ jest.useFakeTimers();
178
+ try {
179
+ const setManageSpy = jest.spyOn(service, 'setManageWebexCallingInWxcc');
180
+ webex.request = jest
181
+ .fn()
182
+ .mockResolvedValueOnce({body: {}})
183
+ .mockRejectedValueOnce(new Error('refresh failed'));
184
+
185
+ await service.setManageWebexCallingInWxcc(true, {ttl: 900});
186
+ const scheduleRefreshRetrySpy = jest.spyOn(service as any, 'scheduleRefreshRetry');
187
+
188
+ jest.advanceTimersByTime(840_000);
189
+ await expect(setManageSpy.mock.results[1].value).rejects.toThrow('refresh failed');
190
+ expect(scheduleRefreshRetrySpy).toHaveBeenCalledWith(userId, 900, 0, 0);
191
+ } finally {
192
+ jest.useRealTimers();
193
+ }
194
+ });
195
+
196
+ it('does not retry refresh after teardown bumps generation', async () => {
197
+ jest.useFakeTimers();
198
+ try {
199
+ const setManageSpy = jest.spyOn(service, 'setManageWebexCallingInWxcc');
200
+ webex.request = jest
201
+ .fn()
202
+ .mockResolvedValueOnce({body: {}})
203
+ .mockRejectedValueOnce(new Error('refresh failed'));
204
+
205
+ await service.setManageWebexCallingInWxcc(true, {ttl: 900});
206
+
207
+ jest.advanceTimersByTime(840_000);
208
+ await expect(setManageSpy.mock.results[1].value).rejects.toThrow('refresh failed');
209
+
210
+ service.teardown();
211
+
212
+ jest.advanceTimersByTime(30_000);
213
+ await Promise.resolve();
214
+
215
+ expect(webex.request).toHaveBeenCalledTimes(2);
216
+ } finally {
217
+ jest.useRealTimers();
218
+ }
219
+ });
220
+
221
+ it('teardown clears refresh timer so publish is not repeated', async () => {
222
+ jest.useFakeTimers();
223
+ try {
224
+ await service.setManageWebexCallingInWxcc(true, {ttl: 900});
225
+ service.teardown();
226
+
227
+ jest.advanceTimersByTime(840_000);
228
+ await Promise.resolve();
229
+
230
+ expect(webex.request).toHaveBeenCalledTimes(1);
231
+ } finally {
232
+ jest.useRealTimers();
233
+ }
234
+ });
235
+
236
+ it('does not schedule refresh when disabled', async () => {
237
+ jest.useFakeTimers();
238
+ try {
239
+ await service.setManageWebexCallingInWxcc(false);
240
+
241
+ jest.advanceTimersByTime(840_000);
242
+ await Promise.resolve();
243
+
244
+ expect(webex.request).toHaveBeenCalledTimes(1);
245
+ } finally {
246
+ jest.useRealTimers();
247
+ }
248
+ });
249
+
250
+ it('reports whether answer-calls-on-wxcc true was published', async () => {
251
+ expect(service.isAnswerCallsStateActive()).toBe(false);
252
+
253
+ await service.setManageWebexCallingInWxcc(true);
254
+
255
+ expect(service.isAnswerCallsStateActive()).toBe(true);
256
+
257
+ await service.setManageWebexCallingInWxcc(false);
258
+
259
+ expect(service.isAnswerCallsStateActive()).toBe(false);
260
+ });
261
+ });
@@ -0,0 +1,113 @@
1
+ import WxAppTelephonyMercurySync, {
2
+ TELEPHONY_CALL_MUTED,
3
+ TELEPHONY_CALL_UNMUTED,
4
+ } from '../../../../src/services/WxAppTelephonyMercurySync';
5
+
6
+ describe('WxAppTelephonyMercurySync', () => {
7
+ const agentId = 'agent-123';
8
+ let mercury: {on: jest.Mock; off: jest.Mock};
9
+ let webex: {internal: {mercury?: typeof mercury}};
10
+ let sync: WxAppTelephonyMercurySync;
11
+ let onMuteChange: jest.Mock;
12
+
13
+ beforeEach(() => {
14
+ mercury = {on: jest.fn(), off: jest.fn()};
15
+ webex = {internal: {mercury}};
16
+ sync = new WxAppTelephonyMercurySync(webex as never);
17
+ onMuteChange = jest.fn();
18
+ });
19
+
20
+ afterEach(() => {
21
+ sync.unsubscribe();
22
+ });
23
+
24
+ it('subscribes to muted and unmuted Mercury events', () => {
25
+ sync.subscribe(agentId, onMuteChange);
26
+
27
+ expect(mercury.on).toHaveBeenCalledWith(TELEPHONY_CALL_MUTED, expect.any(Function));
28
+ expect(mercury.on).toHaveBeenCalledWith(TELEPHONY_CALL_UNMUTED, expect.any(Function));
29
+ expect(sync.isSubscribed()).toBe(true);
30
+ });
31
+
32
+ it('invokes callback with decoded callId and muted when actorId matches agent', () => {
33
+ const callId = 'prefix:call-half-1';
34
+ const encodedCallId = btoa(callId);
35
+ const encodedActorId = btoa(`some-prefix-${agentId}`);
36
+
37
+ sync.subscribe(agentId, onMuteChange);
38
+ const muteHandler = mercury.on.mock.calls.find(([event]) => event === TELEPHONY_CALL_MUTED)![1];
39
+ muteHandler({data: {actorId: encodedActorId, callId: encodedCallId, muted: true}});
40
+
41
+ expect(onMuteChange).toHaveBeenCalledWith(callId, true);
42
+ });
43
+
44
+ it('ignores events when decoded actorId does not end with agentId', () => {
45
+ sync.subscribe(agentId, onMuteChange);
46
+ const muteHandler = mercury.on.mock.calls.find(([event]) => event === TELEPHONY_CALL_MUTED)![1];
47
+ muteHandler({data: {actorId: btoa('other-agent'), callId: btoa('call-1'), muted: true}});
48
+
49
+ expect(onMuteChange).not.toHaveBeenCalled();
50
+ });
51
+
52
+ it('ignores events with missing payload fields', () => {
53
+ sync.subscribe(agentId, onMuteChange);
54
+ const muteHandler = mercury.on.mock.calls.find(([event]) => event === TELEPHONY_CALL_MUTED)![1];
55
+ muteHandler({data: {actorId: btoa(agentId), callId: btoa('call-1')}});
56
+
57
+ expect(onMuteChange).not.toHaveBeenCalled();
58
+ });
59
+
60
+ it('unsubscribes and removes Mercury listeners', () => {
61
+ sync.subscribe(agentId, onMuteChange);
62
+ const muteHandler = mercury.on.mock.calls.find(([event]) => event === TELEPHONY_CALL_MUTED)![1];
63
+ const unmuteHandler = mercury.on.mock.calls.find(
64
+ ([event]) => event === TELEPHONY_CALL_UNMUTED
65
+ )![1];
66
+
67
+ sync.unsubscribe();
68
+
69
+ expect(mercury.off).toHaveBeenCalledWith(TELEPHONY_CALL_MUTED, muteHandler);
70
+ expect(mercury.off).toHaveBeenCalledWith(TELEPHONY_CALL_UNMUTED, unmuteHandler);
71
+ expect(sync.isSubscribed()).toBe(false);
72
+ });
73
+
74
+ it('does not subscribe when mercury is unavailable', () => {
75
+ sync = new WxAppTelephonyMercurySync({internal: {}} as never);
76
+ sync.subscribe(agentId, onMuteChange);
77
+
78
+ expect(sync.isSubscribed()).toBe(false);
79
+ expect(onMuteChange).not.toHaveBeenCalled();
80
+ });
81
+
82
+ it('invokes callback with muted false on unmuted Mercury event', () => {
83
+ const callId = 'prefix:call-half-1';
84
+ const encodedCallId = btoa(callId);
85
+ const encodedActorId = btoa(`some-prefix-${agentId}`);
86
+
87
+ sync.subscribe(agentId, onMuteChange);
88
+ const unmuteHandler = mercury.on.mock.calls.find(
89
+ ([event]) => event === TELEPHONY_CALL_UNMUTED
90
+ )![1];
91
+ unmuteHandler({data: {actorId: encodedActorId, callId: encodedCallId, muted: false}});
92
+
93
+ expect(onMuteChange).toHaveBeenCalledWith(callId, false);
94
+ });
95
+
96
+ it('resubscribe replaces previous Mercury listeners', () => {
97
+ sync.subscribe(agentId, onMuteChange);
98
+ const firstMuteHandler = mercury.on.mock.calls.find(
99
+ ([event]) => event === TELEPHONY_CALL_MUTED
100
+ )![1];
101
+ const firstUnmuteHandler = mercury.on.mock.calls.find(
102
+ ([event]) => event === TELEPHONY_CALL_UNMUTED
103
+ )![1];
104
+ expect(mercury.on).toHaveBeenCalledTimes(2);
105
+
106
+ const secondHandler = jest.fn();
107
+ sync.subscribe(agentId, secondHandler);
108
+
109
+ expect(mercury.off).toHaveBeenCalledWith(TELEPHONY_CALL_MUTED, firstMuteHandler);
110
+ expect(mercury.off).toHaveBeenCalledWith(TELEPHONY_CALL_UNMUTED, firstUnmuteHandler);
111
+ expect(mercury.on).toHaveBeenCalledTimes(4);
112
+ });
113
+ });
@@ -0,0 +1,85 @@
1
+ import {parseAgentConfigs} from '../../../../../src/services/config/Util';
2
+ import {AuxCode, DesktopProfileResponse} from '../../../../../src/services/config/types';
3
+
4
+ const baseAgentProfile: DesktopProfileResponse = {
5
+ timeoutDesktopInactivityCustomEnabled: false,
6
+ timeoutDesktopInactivityMins: 10,
7
+ accessWrapUpCode: 'ALL',
8
+ wrapUpCodes: [],
9
+ accessIdleCode: 'ALL',
10
+ idleCodes: [],
11
+ autoWrapUp: false,
12
+ autoWrapAfterSeconds: 30,
13
+ lastAgentRouting: false,
14
+ allowAutoWrapUpExtension: false,
15
+ outdialEnabled: true,
16
+ dialPlanEnabled: false,
17
+ agentAvailableAfterOutdial: true,
18
+ outdialEntryPointId: 'ep-1',
19
+ consultToQueue: false,
20
+ viewableStatistics: {agentStats: false},
21
+ addressBookId: 'ab-1',
22
+ outdialANIId: 'ani-1',
23
+ loginVoiceOptions: [],
24
+ dialPlans: [],
25
+ agentDNValidation: 'PROVISIONED_VALUE',
26
+ accessQueue: 'SPECIFIC',
27
+ accessEntryPoint: 'NONE',
28
+ accessBuddyTeam: 'ALL',
29
+ } as DesktopProfileResponse;
30
+
31
+ const baseProfileData = {
32
+ userData: {
33
+ ciUserId: 'agent-1',
34
+ firstName: 'Jane',
35
+ lastName: 'Agent',
36
+ email: 'jane@example.com',
37
+ agentProfileId: 'profile-1',
38
+ skillProfileId: 'skill-1',
39
+ siteId: 'site-1',
40
+ dbId: 'db-1',
41
+ deafultDialledNumber: '+15551234567',
42
+ id: 'user-1',
43
+ teamIds: ['team-1'],
44
+ },
45
+ teamData: [{id: 'team-1', name: 'Support'}],
46
+ tenantData: {
47
+ timeoutDesktopInactivityEnabled: false,
48
+ timeoutDesktopInactivityMins: 15,
49
+ forceDefaultDn: false,
50
+ dnDefaultRegex: '',
51
+ dnOtherRegex: '',
52
+ outdialEnabled: true,
53
+ endCallEnabled: true,
54
+ endConsultEnabled: true,
55
+ callVariablesSuppressed: false,
56
+ privacyShieldVisible: false,
57
+ },
58
+ orgInfoData: {
59
+ tenantId: 'tenant-1',
60
+ timezone: 'UTC',
61
+ environment: 'produs1',
62
+ },
63
+ auxCodes: [] as AuxCode[],
64
+ orgSettingsData: {
65
+ campaignManagerEnabled: false,
66
+ webRtcEnabled: true,
67
+ maskSensitiveData: false,
68
+ },
69
+ agentProfileData: baseAgentProfile,
70
+ dialPlanData: [],
71
+ urlMapping: [],
72
+ multimediaProfileId: 'mm-1',
73
+ aiFeatureFlags: {data: []},
74
+ };
75
+
76
+ describe('parseAgentConfigs collaboration access fields', () => {
77
+ it('maps Desktop Profile collaboration access flags onto Profile', () => {
78
+ const profile = parseAgentConfigs(baseProfileData);
79
+
80
+ expect(profile.allowConsultToQueue).toBe(false);
81
+ expect(profile.accessQueue).toBe('SPECIFIC');
82
+ expect(profile.accessEntryPoint).toBe('NONE');
83
+ expect(profile.accessBuddyTeam).toBe('ALL');
84
+ });
85
+ });